mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-04 15:10:06 +00:00
fix: Restore correct interfaces from PR #3367
- Restore original useManagerQueue, useServerLogs, and comfyManagerService interfaces - Restore original component implementations for ManagerProgressDialogContent and ManagerProgressHeader - Fix all TypeScript interface compatibility issues by using original PR implementations - Remove duplicate setting that was causing runtime errors This fixes merge errors where interfaces were incorrectly mixed between old and new implementations.
This commit is contained in:
@@ -18,6 +18,18 @@ This document tracks backup branches created during the manager migration recove
|
||||
- Updated type definitions and store interfaces
|
||||
- Resolved merge conflicts and formatting fixes
|
||||
|
||||
### `manager-migration-clean-tested`
|
||||
- **Created**: 2025-08-30
|
||||
- **Source Branch**: `manager-migration-clean`
|
||||
- **Source Commit**: `380f335bf` - "feat: Integrate ComfyUI Manager migration with v2 API and enhanced UI"
|
||||
- **Purpose**: Backup before manual testing via dev server
|
||||
- **Contains**:
|
||||
- Single squashed commit with complete manager migration
|
||||
- All recovered functionality from PR #3367
|
||||
- v2 API integration and enhanced UI components
|
||||
- Resolved TypeScript issues and quality checks passed
|
||||
- Clean, production-ready state ready for manual testing
|
||||
|
||||
### `manager-migration-upstream-backup`
|
||||
- **Created**: Earlier in recovery process
|
||||
- **Purpose**: Backup of upstream state before major changes
|
||||
|
||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -198,6 +198,9 @@ importers:
|
||||
'@types/fs-extra':
|
||||
specifier: ^11.0.4
|
||||
version: 11.0.4
|
||||
'@types/lodash':
|
||||
specifier: ^4.17.20
|
||||
version: 4.17.20
|
||||
'@types/node':
|
||||
specifier: ^20.14.8
|
||||
version: 20.14.10
|
||||
@@ -232,8 +235,8 @@ importers:
|
||||
specifier: ^5.2.6
|
||||
version: 5.2.6(eslint-config-prettier@10.1.2(eslint@9.12.0(jiti@2.5.1)))(eslint@9.12.0(jiti@2.5.1))(prettier@3.3.2)
|
||||
eslint-plugin-storybook:
|
||||
specifier: ^9.1.1
|
||||
version: 9.1.1(eslint@9.12.0(jiti@2.5.1))(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(terser@5.39.2)))(typescript@5.9.2)
|
||||
specifier: ^9.1.3
|
||||
version: 9.1.3(eslint@9.12.0(jiti@2.5.1))(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(terser@5.39.2)))(typescript@5.9.2)
|
||||
eslint-plugin-unused-imports:
|
||||
specifier: ^4.1.4
|
||||
version: 4.1.4(@typescript-eslint/eslint-plugin@8.0.0(@typescript-eslint/parser@8.0.0(eslint@9.12.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.12.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.12.0(jiti@2.5.1))
|
||||
@@ -1953,6 +1956,9 @@ packages:
|
||||
'@types/linkify-it@5.0.0':
|
||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||
|
||||
'@types/lodash@4.17.20':
|
||||
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
|
||||
|
||||
'@types/markdown-it@13.0.9':
|
||||
resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==}
|
||||
|
||||
@@ -3085,12 +3091,12 @@ packages:
|
||||
eslint-config-prettier:
|
||||
optional: true
|
||||
|
||||
eslint-plugin-storybook@9.1.1:
|
||||
resolution: {integrity: sha512-g4/i9yW6cl4TCEMzYyALNvO3d/jB6TDvSs/Pmye7dHDrra2B7dgZJGzmEWILD62brVrLVHNoXgy2dNPtx80kmw==}
|
||||
eslint-plugin-storybook@9.1.3:
|
||||
resolution: {integrity: sha512-CR576JrlvxLY2ebJIyR6z/YWy6+iyVsB7ORjPrwM3a9SshlRnAntdEn6hyMYbQmFoPIv7kYcRiDznDXBQ/jitA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
peerDependencies:
|
||||
eslint: '>=8'
|
||||
storybook: ^9.1.1
|
||||
storybook: ^9.1.3
|
||||
|
||||
eslint-plugin-unused-imports@4.1.4:
|
||||
resolution: {integrity: sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==}
|
||||
@@ -7670,8 +7676,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@mdx-js/react': 3.1.0(@types/react@19.1.9)(react@19.1.1)
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(terser@5.39.2)))
|
||||
'@storybook/icons': 1.4.0(react-dom@19.1.1(react@18.3.1))(react@19.1.1)
|
||||
'@storybook/react-dom-shim': 9.1.1(react-dom@19.1.1(react@18.3.1))(react@19.1.1)(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(terser@5.39.2)))
|
||||
'@storybook/icons': 1.4.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@storybook/react-dom-shim': 9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(terser@5.39.2)))
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
storybook: 9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(terser@5.39.2))
|
||||
@@ -7693,12 +7699,12 @@ snapshots:
|
||||
|
||||
'@storybook/global@5.0.0': {}
|
||||
|
||||
'@storybook/icons@1.4.0(react-dom@19.1.1(react@18.3.1))(react@19.1.1)':
|
||||
'@storybook/icons@1.4.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
|
||||
dependencies:
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
|
||||
'@storybook/react-dom-shim@9.1.1(react-dom@19.1.1(react@18.3.1))(react@19.1.1)(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(terser@5.39.2)))':
|
||||
'@storybook/react-dom-shim@9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(terser@5.39.2)))':
|
||||
dependencies:
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
@@ -7964,6 +7970,8 @@ snapshots:
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
'@types/lodash@4.17.20': {}
|
||||
|
||||
'@types/markdown-it@13.0.9':
|
||||
dependencies:
|
||||
'@types/linkify-it': 3.0.5
|
||||
@@ -9212,7 +9220,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
eslint-config-prettier: 10.1.2(eslint@9.12.0(jiti@2.5.1))
|
||||
|
||||
eslint-plugin-storybook@9.1.1(eslint@9.12.0(jiti@2.5.1))(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(terser@5.39.2)))(typescript@5.9.2):
|
||||
eslint-plugin-storybook@9.1.3(eslint@9.12.0(jiti@2.5.1))(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(terser@5.39.2)))(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.39.0(eslint@9.12.0(jiti@2.5.1))(typescript@5.9.2)
|
||||
eslint: 9.12.0(jiti@2.5.1)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
'max-h-0': !isExpanded
|
||||
}"
|
||||
>
|
||||
<div v-for="(log, index) in focusedLogs" :key="index">
|
||||
<div v-for="(panel, index) in taskPanels" :key="index">
|
||||
<Panel
|
||||
:expanded="collapsedPanels[index] || false"
|
||||
toggleable
|
||||
@@ -27,7 +27,7 @@
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between w-full py-2">
|
||||
<div class="flex flex-col text-sm font-medium leading-normal">
|
||||
<span>{{ log.taskName }}</span>
|
||||
<span>{{ panel.taskName }}</span>
|
||||
<span class="text-muted">
|
||||
{{
|
||||
isInProgress(index)
|
||||
@@ -52,24 +52,24 @@
|
||||
</template>
|
||||
<div
|
||||
:ref="
|
||||
index === focusedLogs.length - 1
|
||||
index === taskPanels.length - 1
|
||||
? (el) => (lastPanelRef = el as HTMLElement)
|
||||
: undefined
|
||||
"
|
||||
class="overflow-y-auto h-64 rounded-lg bg-black"
|
||||
:class="{
|
||||
'h-64': index !== focusedLogs.length - 1,
|
||||
'flex-grow': index === focusedLogs.length - 1
|
||||
'h-64': index !== taskPanels.length - 1,
|
||||
'flex-grow': index === taskPanels.length - 1
|
||||
}"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div class="h-full">
|
||||
<div
|
||||
v-for="(logLine, logIndex) in log.logs"
|
||||
v-for="(log, logIndex) in panel.logs"
|
||||
:key="logIndex"
|
||||
class="text-neutral-400 dark-theme:text-muted"
|
||||
>
|
||||
<pre class="whitespace-pre-wrap break-words">{{ logLine }}</pre>
|
||||
<pre class="whitespace-pre-wrap break-words">{{ log }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,23 +90,17 @@ import {
|
||||
useManagerProgressDialogStore
|
||||
} from '@/stores/comfyManagerStore'
|
||||
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const { taskLogs } = useComfyManagerStore()
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const isInProgress = (index: number) =>
|
||||
index === comfyManagerStore.managerQueue.historyCount - 1 &&
|
||||
comfyManagerStore.isLoading
|
||||
index === taskPanels.value.length - 1 && managerStore.uncompletedCount > 0
|
||||
|
||||
const taskPanels = computed(() => taskLogs)
|
||||
const isExpanded = computed(() => progressDialogContent.isExpanded)
|
||||
const isCollapsed = computed(() => !isExpanded.value)
|
||||
|
||||
const focusedLogs = computed(() => {
|
||||
if (progressDialogContent.getActiveTabIndex() === 0) {
|
||||
return comfyManagerStore.succeededTasksLogs
|
||||
}
|
||||
return comfyManagerStore.failedTasksLogs
|
||||
})
|
||||
|
||||
const collapsedPanels = ref<Record<number, boolean>>({})
|
||||
const togglePanel = (index: number) => {
|
||||
collapsedPanels.value[index] = !collapsedPanels.value[index]
|
||||
@@ -121,7 +115,7 @@ const { y: scrollY } = useScroll(sectionsContainerRef, {
|
||||
|
||||
const lastPanelRef = ref<HTMLElement | null>(null)
|
||||
const isUserScrolling = ref(false)
|
||||
const lastPanelLogs = computed(() => focusedLogs.value?.at(-1)?.logs)
|
||||
const lastPanelLogs = computed(() => taskPanels.value?.at(-1)?.logs)
|
||||
|
||||
const isAtBottom = (el: HTMLElement | null) => {
|
||||
if (!el) return false
|
||||
|
||||
@@ -18,40 +18,16 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import TabMenu from 'primevue/tabmenu'
|
||||
import { computed } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import {
|
||||
useComfyManagerStore,
|
||||
useManagerProgressDialogStore
|
||||
} from '@/stores/comfyManagerStore'
|
||||
import { useManagerProgressDialogStore } from '@/stores/comfyManagerStore'
|
||||
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const activeTabIndex = computed({
|
||||
get: () => progressDialogContent.getActiveTabIndex(),
|
||||
set: (value: number) => progressDialogContent.setActiveTabIndex(value)
|
||||
})
|
||||
const activeTabIndex = ref(0)
|
||||
const { t } = useI18n()
|
||||
|
||||
const failedCount = computed(() => comfyManagerStore.failedTasksIds.length)
|
||||
|
||||
const queueSuffix = computed(() => {
|
||||
const queueLength = comfyManagerStore.managerQueue.queueLength
|
||||
if (queueLength === 0) {
|
||||
return ''
|
||||
}
|
||||
return ` (${queueLength})`
|
||||
})
|
||||
const failedSuffix = computed(() => {
|
||||
if (failedCount.value === 0) {
|
||||
return ''
|
||||
}
|
||||
return ` (${failedCount.value})`
|
||||
})
|
||||
|
||||
const tabs = computed(() => [
|
||||
{ label: t('manager.installationQueue') + queueSuffix.value },
|
||||
{ label: t('manager.failed') + failedSuffix.value }
|
||||
])
|
||||
const tabs = [
|
||||
{ label: t('manager.installationQueue') },
|
||||
{ label: t('manager.failed', { count: 0 }) }
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -1,34 +1,24 @@
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
|
||||
import { LogsWsMessage } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
|
||||
const LOGS_MESSAGE_TYPE = 'logs'
|
||||
const MANAGER_WS_TASK_DONE_NAME = 'cm-task-completed'
|
||||
const MANAGER_WS_TASK_STARTED_NAME = 'cm-task-started'
|
||||
|
||||
type ManagerWsTaskDoneMsg = components['schemas']['MessageTaskDone']
|
||||
type ManagerWsTaskStartedMsg = components['schemas']['MessageTaskStarted']
|
||||
|
||||
interface UseServerLogsOptions {
|
||||
ui_id: string
|
||||
immediate?: boolean
|
||||
messageFilter?: (message: string) => boolean
|
||||
}
|
||||
|
||||
export const useServerLogs = (options: UseServerLogsOptions) => {
|
||||
export const useServerLogs = (options: UseServerLogsOptions = {}) => {
|
||||
const {
|
||||
immediate = false,
|
||||
messageFilter = (msg: string) => Boolean(msg.trim())
|
||||
} = options
|
||||
|
||||
const logs = ref<string[]>([])
|
||||
const isTaskStarted = ref(false)
|
||||
let stopLogs: ReturnType<typeof useEventListener> | null = null
|
||||
let stopTaskDone: ReturnType<typeof useEventListener> | null = null
|
||||
let stopTaskStarted: ReturnType<typeof useEventListener> | null = null
|
||||
let stop: ReturnType<typeof useEventListener> | null = null
|
||||
|
||||
const isValidLogEvent = (event: CustomEvent<LogsWsMessage>) =>
|
||||
event?.type === LOGS_MESSAGE_TYPE && event.detail?.entries?.length > 0
|
||||
@@ -37,81 +27,34 @@ export const useServerLogs = (options: UseServerLogsOptions) => {
|
||||
event.detail.entries.map((e) => e.m).filter(messageFilter)
|
||||
|
||||
const handleLogMessage = (event: CustomEvent<LogsWsMessage>) => {
|
||||
// Only capture logs if this task has started
|
||||
if (!isTaskStarted.value) return
|
||||
|
||||
if (isValidLogEvent(event)) {
|
||||
const messages = parseLogMessage(event)
|
||||
if (messages.length > 0) {
|
||||
logs.value.push(...messages)
|
||||
}
|
||||
logs.value.push(...parseLogMessage(event))
|
||||
}
|
||||
}
|
||||
|
||||
const handleTaskStarted = (event: CustomEvent<ManagerWsTaskStartedMsg>) => {
|
||||
if (event?.type === MANAGER_WS_TASK_STARTED_NAME) {
|
||||
// Check if this is our task starting
|
||||
const isOurTask = event.detail.ui_id === options.ui_id
|
||||
if (isOurTask) {
|
||||
isTaskStarted.value = true
|
||||
void stopTaskStarted?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleTaskDone = (event: CustomEvent<ManagerWsTaskDoneMsg>) => {
|
||||
if (event?.type === MANAGER_WS_TASK_DONE_NAME) {
|
||||
const { state } = event.detail
|
||||
// Check if our task is now in the history (completed)
|
||||
const isOurTaskDone = state.history[options.ui_id]
|
||||
if (isOurTaskDone) {
|
||||
isTaskStarted.value = false
|
||||
void stopListening()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const startListening = async () => {
|
||||
const start = async () => {
|
||||
await api.subscribeLogs(true)
|
||||
stopLogs = useEventListener(api, LOGS_MESSAGE_TYPE, handleLogMessage)
|
||||
stopTaskStarted = useEventListener(
|
||||
api,
|
||||
MANAGER_WS_TASK_STARTED_NAME,
|
||||
handleTaskStarted
|
||||
)
|
||||
stopTaskDone = useEventListener(
|
||||
api,
|
||||
MANAGER_WS_TASK_DONE_NAME,
|
||||
handleTaskDone
|
||||
)
|
||||
stop = useEventListener(api, LOGS_MESSAGE_TYPE, handleLogMessage)
|
||||
}
|
||||
|
||||
const stopListening = async () => {
|
||||
stopLogs?.()
|
||||
stopTaskStarted?.()
|
||||
stopTaskDone?.()
|
||||
stopLogs = null
|
||||
stopTaskStarted = null
|
||||
stopTaskDone = null
|
||||
// TODO: move subscribe/unsubscribe logs to useManagerQueue. Subscribe when task starts if not already subscribed.
|
||||
// Unsubscribe ONLY when there are no tasks running or queued up and the only remaining task finishes.
|
||||
// await api.subscribeLogs(false)
|
||||
stop?.()
|
||||
stop = null
|
||||
await api.subscribeLogs(false)
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
void startListening()
|
||||
void start()
|
||||
}
|
||||
|
||||
const cleanup = async () => {
|
||||
onUnmounted(async () => {
|
||||
await stopListening()
|
||||
logs.value = []
|
||||
isTaskStarted.value = false
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
logs,
|
||||
startListening,
|
||||
stopListening,
|
||||
cleanup
|
||||
startListening: start,
|
||||
stopListening
|
||||
}
|
||||
}
|
||||
|
||||
@@ -935,12 +935,5 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
name: 'Release seen timestamp',
|
||||
type: 'hidden',
|
||||
defaultValue: 0
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Memory.AllowManualUnload',
|
||||
name: 'Allow manual unload of models and execution cache via user command',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.18.0'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import axios, { AxiosError, AxiosResponse } from 'axios'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
import {
|
||||
type InstallPackParams,
|
||||
type InstalledPacksResponse,
|
||||
type ManagerPackInfo,
|
||||
type ManagerQueueStatus,
|
||||
SelectedVersion,
|
||||
type UpdateAllPacksParams
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import { isAbortError } from '@/utils/typeGuardUtil'
|
||||
|
||||
type ManagerQueueStatus = components['schemas']['QueueStatus']
|
||||
type InstallPackParams = components['schemas']['InstallPackParams']
|
||||
type InstalledPacksResponse = components['schemas']['InstalledPacksResponse']
|
||||
type UpdateAllPacksParams = components['schemas']['UpdateAllPacksParams']
|
||||
type ManagerTaskHistory = components['schemas']['HistoryResponse']
|
||||
type QueueTaskItem = components['schemas']['QueueTaskItem']
|
||||
|
||||
const GENERIC_SECURITY_ERR_MSG =
|
||||
'Forbidden: A security error has occurred. Please check the terminal logs'
|
||||
|
||||
@@ -33,9 +32,7 @@ enum ManagerRoute {
|
||||
LIST_INSTALLED = 'customnode/installed',
|
||||
IMPORT_FAIL_INFO = 'customnode/import_fail_info',
|
||||
REBOOT = 'manager/reboot',
|
||||
IS_LEGACY_MANAGER_UI = 'manager/is_legacy_manager_ui',
|
||||
TASK_HISTORY = 'manager/queue/history',
|
||||
QUEUE_TASK = 'manager/queue/task'
|
||||
IS_LEGACY_MANAGER_UI = 'manager/is_legacy_manager_ui'
|
||||
}
|
||||
|
||||
const managerApiClient = axios.create({
|
||||
@@ -52,6 +49,7 @@ const managerApiClient = axios.create({
|
||||
export const useComfyManagerService = () => {
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const didStartQueue = ref(false)
|
||||
|
||||
const handleRequestError = (
|
||||
err: unknown,
|
||||
@@ -112,21 +110,19 @@ export const useComfyManagerService = () => {
|
||||
201: 'Created: ComfyUI-Manager job queue is already running'
|
||||
}
|
||||
|
||||
didStartQueue.value = true
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.get(ManagerRoute.START_QUEUE, { signal }),
|
||||
{ errorContext, routeSpecificErrors }
|
||||
)
|
||||
}
|
||||
|
||||
const getQueueStatus = async (client_id?: string, signal?: AbortSignal) => {
|
||||
const getQueueStatus = async (signal?: AbortSignal) => {
|
||||
const errorContext = 'Getting ComfyUI-Manager queue status'
|
||||
|
||||
return executeRequest<ManagerQueueStatus>(
|
||||
() =>
|
||||
managerApiClient.get(ManagerRoute.QUEUE_STATUS, {
|
||||
params: client_id ? { client_id } : undefined,
|
||||
signal
|
||||
}),
|
||||
() => managerApiClient.get(ManagerRoute.QUEUE_STATUS, { signal }),
|
||||
{ errorContext }
|
||||
)
|
||||
}
|
||||
@@ -158,66 +154,73 @@ export const useComfyManagerService = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const queueTask = async (
|
||||
kind: QueueTaskItem['kind'],
|
||||
params: QueueTaskItem['params'],
|
||||
ui_id?: string,
|
||||
const installPack = async (
|
||||
params: InstallPackParams,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const task: QueueTaskItem = {
|
||||
kind,
|
||||
params,
|
||||
ui_id: ui_id || uuidv4(),
|
||||
client_id: api.clientId ?? api.initialClientId ?? 'unknown'
|
||||
}
|
||||
|
||||
const errorContext = `Queueing ${task.kind} task`
|
||||
const errorContext = `Installing pack ${params.id}`
|
||||
const routeSpecificErrors = {
|
||||
403: GENERIC_SECURITY_ERR_MSG,
|
||||
404: `Not Found: Task could not be queued`
|
||||
404:
|
||||
params.selected_version === SelectedVersion.NIGHTLY
|
||||
? `Not Found: Node pack ${params.id} does not provide nightly version`
|
||||
: GENERIC_SECURITY_ERR_MSG
|
||||
}
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.post(ManagerRoute.QUEUE_TASK, task, { signal }),
|
||||
() => managerApiClient.post(ManagerRoute.INSTALL, params, { signal }),
|
||||
{ errorContext, routeSpecificErrors, isQueueOperation: true }
|
||||
)
|
||||
}
|
||||
|
||||
const installPack = async (
|
||||
params: InstallPackParams,
|
||||
ui_id?: string,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
return queueTask('install', params, ui_id, signal)
|
||||
}
|
||||
|
||||
const uninstallPack = async (
|
||||
params: components['schemas']['UninstallPackParams'],
|
||||
ui_id?: string,
|
||||
params: ManagerPackInfo,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
return queueTask('uninstall', params, ui_id, signal)
|
||||
const errorContext = `Uninstalling pack ${params.id}`
|
||||
const routeSpecificErrors = {
|
||||
403: GENERIC_SECURITY_ERR_MSG
|
||||
}
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.post(ManagerRoute.UNINSTALL, params, { signal }),
|
||||
{ errorContext, routeSpecificErrors, isQueueOperation: true }
|
||||
)
|
||||
}
|
||||
|
||||
const disablePack = async (
|
||||
params: components['schemas']['DisablePackParams'],
|
||||
ui_id?: string,
|
||||
params: ManagerPackInfo,
|
||||
signal?: AbortSignal
|
||||
): Promise<null> => {
|
||||
return queueTask('disable', params, ui_id, signal)
|
||||
const errorContext = `Disabling pack ${params.id}`
|
||||
const routeSpecificErrors = {
|
||||
404: `Pack ${params.id} not found or not installed`,
|
||||
409: `Pack ${params.id} is already disabled`
|
||||
}
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.post(ManagerRoute.DISABLE, params, { signal }),
|
||||
{ errorContext, routeSpecificErrors, isQueueOperation: true }
|
||||
)
|
||||
}
|
||||
|
||||
const updatePack = async (
|
||||
params: components['schemas']['UpdatePackParams'],
|
||||
ui_id?: string,
|
||||
params: ManagerPackInfo,
|
||||
signal?: AbortSignal
|
||||
): Promise<null> => {
|
||||
return queueTask('update', params, ui_id, signal)
|
||||
const errorContext = `Updating pack ${params.id}`
|
||||
const routeSpecificErrors = {
|
||||
403: GENERIC_SECURITY_ERR_MSG
|
||||
}
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.post(ManagerRoute.UPDATE, params, { signal }),
|
||||
{ errorContext, routeSpecificErrors, isQueueOperation: true }
|
||||
)
|
||||
}
|
||||
|
||||
const updateAllPacks = async (
|
||||
params: UpdateAllPacksParams = {},
|
||||
ui_id?: string,
|
||||
params?: UpdateAllPacksParams,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const errorContext = 'Updating all packs'
|
||||
@@ -226,18 +229,8 @@ export const useComfyManagerService = () => {
|
||||
401: 'Unauthorized: ComfyUI-Manager job queue is busy'
|
||||
}
|
||||
|
||||
const queryParams = {
|
||||
mode: params.mode,
|
||||
client_id: api.clientId ?? api.initialClientId ?? 'unknown',
|
||||
ui_id: ui_id || uuidv4()
|
||||
}
|
||||
|
||||
return executeRequest<null>(
|
||||
() =>
|
||||
managerApiClient.get(ManagerRoute.UPDATE_ALL, {
|
||||
params: queryParams,
|
||||
signal
|
||||
}),
|
||||
() => managerApiClient.get(ManagerRoute.UPDATE_ALL, { params, signal }),
|
||||
{ errorContext, routeSpecificErrors, isQueueOperation: true }
|
||||
)
|
||||
}
|
||||
@@ -263,27 +256,6 @@ export const useComfyManagerService = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const getTaskHistory = async (
|
||||
options: {
|
||||
ui_id?: string
|
||||
max_items?: number
|
||||
client_id?: string
|
||||
offset?: number
|
||||
} = {},
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const errorContext = 'Getting ComfyUI-Manager task history'
|
||||
|
||||
return executeRequest<ManagerTaskHistory>(
|
||||
() =>
|
||||
managerApiClient.get(ManagerRoute.TASK_HISTORY, {
|
||||
params: options,
|
||||
signal
|
||||
}),
|
||||
{ errorContext }
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
isLoading,
|
||||
@@ -293,7 +265,6 @@ export const useComfyManagerService = () => {
|
||||
startQueue,
|
||||
resetQueue,
|
||||
getQueueStatus,
|
||||
getTaskHistory,
|
||||
|
||||
// Pack management
|
||||
listInstalledPacks,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCachedRequest } from '@/composables/useCachedRequest'
|
||||
@@ -33,9 +33,8 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
const isStale = ref(true)
|
||||
const taskLogs = ref<TaskLog[]>([])
|
||||
|
||||
const managerQueue = useManagerQueue()
|
||||
const { statusMessage, allTasksDone, enqueueTask, uncompletedCount } =
|
||||
managerQueue
|
||||
useManagerQueue()
|
||||
|
||||
const setStale = () => {
|
||||
isStale.value = true
|
||||
@@ -213,11 +212,6 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
taskLogs.value = []
|
||||
}
|
||||
|
||||
// Computed properties for UI components
|
||||
const succeededTasksLogs = computed(() => taskLogs.value)
|
||||
const failedTasksLogs = computed(() => [])
|
||||
const failedTasksIds = computed(() => [])
|
||||
|
||||
return {
|
||||
// Manager state
|
||||
isLoading: managerService.isLoading,
|
||||
@@ -244,15 +238,7 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
updatePack,
|
||||
updateAllPacks,
|
||||
disablePack,
|
||||
enablePack: installPack, // Enable is done via install endpoint with a disabled pack
|
||||
|
||||
// Manager queue
|
||||
managerQueue,
|
||||
|
||||
// UI properties for progress dialog
|
||||
succeededTasksLogs,
|
||||
failedTasksLogs,
|
||||
failedTasksIds
|
||||
enablePack: installPack // Enable is done via install endpoint with a disabled pack
|
||||
}
|
||||
})
|
||||
|
||||
@@ -265,7 +251,6 @@ export const useManagerProgressDialogStore = defineStore(
|
||||
'managerProgressDialog',
|
||||
() => {
|
||||
const isExpanded = ref(false)
|
||||
const activeTabIndex = ref(0)
|
||||
|
||||
const toggle = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
@@ -278,19 +263,11 @@ export const useManagerProgressDialogStore = defineStore(
|
||||
const expand = () => {
|
||||
isExpanded.value = true
|
||||
}
|
||||
|
||||
const getActiveTabIndex = () => activeTabIndex.value
|
||||
const setActiveTabIndex = (index: number) => {
|
||||
activeTabIndex.value = index
|
||||
}
|
||||
|
||||
return {
|
||||
isExpanded,
|
||||
toggle,
|
||||
collapse,
|
||||
expand,
|
||||
getActiveTabIndex,
|
||||
setActiveTabIndex
|
||||
expand
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -24,22 +24,22 @@ describe('useServerLogs', () => {
|
||||
})
|
||||
|
||||
it('should initialize with empty logs array', () => {
|
||||
const { logs } = useServerLogs({ ui_id: 'test-ui-id' })
|
||||
const { logs } = useServerLogs()
|
||||
expect(logs.value).toEqual([])
|
||||
})
|
||||
|
||||
it('should not subscribe to logs by default', () => {
|
||||
useServerLogs({ ui_id: 'test-ui-id' })
|
||||
useServerLogs()
|
||||
expect(api.subscribeLogs).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should subscribe to logs when immediate is true', () => {
|
||||
useServerLogs({ ui_id: 'test-ui-id', immediate: true })
|
||||
useServerLogs({ immediate: true })
|
||||
expect(api.subscribeLogs).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should start listening when startListening is called', async () => {
|
||||
const { startListening } = useServerLogs({ ui_id: 'test-ui-id' })
|
||||
const { startListening } = useServerLogs()
|
||||
|
||||
await startListening()
|
||||
|
||||
@@ -47,21 +47,16 @@ describe('useServerLogs', () => {
|
||||
})
|
||||
|
||||
it('should stop listening when stopListening is called', async () => {
|
||||
const { startListening, stopListening } = useServerLogs({
|
||||
ui_id: 'test-ui-id'
|
||||
})
|
||||
const { startListening, stopListening } = useServerLogs()
|
||||
|
||||
await startListening()
|
||||
await stopListening()
|
||||
|
||||
// TODO: Update this test when subscribeLogs(false) is re-enabled
|
||||
// Currently commented out in useServerLogs to prevent logs from stopping
|
||||
// after 1st of multiple queue tasks
|
||||
expect(api.subscribeLogs).toHaveBeenCalledWith(true)
|
||||
expect(api.subscribeLogs).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should register event listener when starting', async () => {
|
||||
const { startListening } = useServerLogs({ ui_id: 'test-ui-id' })
|
||||
const { startListening } = useServerLogs()
|
||||
|
||||
await startListening()
|
||||
|
||||
@@ -73,30 +68,16 @@ describe('useServerLogs', () => {
|
||||
})
|
||||
|
||||
it('should handle log messages correctly', async () => {
|
||||
const { logs, startListening } = useServerLogs({ ui_id: 'test-ui-id' })
|
||||
const { logs, startListening } = useServerLogs()
|
||||
|
||||
await startListening()
|
||||
|
||||
// Get the callbacks that were registered with useEventListener
|
||||
const mockCalls = vi.mocked(useEventListener).mock.calls
|
||||
const logsCallback = mockCalls.find((call) => call[1] === 'logs')?.[2] as (
|
||||
// Get the callback that was registered with useEventListener
|
||||
const eventCallback = vi.mocked(useEventListener).mock.calls[0][2] as (
|
||||
event: CustomEvent<LogsWsMessage>
|
||||
) => void
|
||||
const taskStartedCallback = mockCalls.find(
|
||||
(call) => call[1] === 'cm-task-started'
|
||||
)?.[2] as (event: CustomEvent<any>) => void
|
||||
|
||||
// First, simulate task started event
|
||||
const taskStartedEvent = new CustomEvent('cm-task-started', {
|
||||
detail: {
|
||||
type: 'cm-task-started',
|
||||
ui_id: 'test-ui-id'
|
||||
}
|
||||
})
|
||||
taskStartedCallback(taskStartedEvent)
|
||||
await nextTick()
|
||||
|
||||
// Now simulate receiving a log event
|
||||
// Simulate receiving a log event
|
||||
const mockEvent = new CustomEvent('logs', {
|
||||
detail: {
|
||||
type: 'logs',
|
||||
@@ -104,7 +85,7 @@ describe('useServerLogs', () => {
|
||||
} as unknown as LogsWsMessage
|
||||
}) as CustomEvent<LogsWsMessage>
|
||||
|
||||
logsCallback(mockEvent)
|
||||
eventCallback(mockEvent)
|
||||
await nextTick()
|
||||
|
||||
expect(logs.value).toEqual(['Log message 1', 'Log message 2'])
|
||||
@@ -112,32 +93,15 @@ describe('useServerLogs', () => {
|
||||
|
||||
it('should use the message filter if provided', async () => {
|
||||
const { logs, startListening } = useServerLogs({
|
||||
ui_id: 'test-ui-id',
|
||||
messageFilter: (msg) => msg !== 'remove me'
|
||||
})
|
||||
|
||||
await startListening()
|
||||
|
||||
// Get the callbacks that were registered with useEventListener
|
||||
const mockCalls = vi.mocked(useEventListener).mock.calls
|
||||
const logsCallback = mockCalls.find((call) => call[1] === 'logs')?.[2] as (
|
||||
const eventCallback = vi.mocked(useEventListener).mock.calls[0][2] as (
|
||||
event: CustomEvent<LogsWsMessage>
|
||||
) => void
|
||||
const taskStartedCallback = mockCalls.find(
|
||||
(call) => call[1] === 'cm-task-started'
|
||||
)?.[2] as (event: CustomEvent<any>) => void
|
||||
|
||||
// First, simulate task started event
|
||||
const taskStartedEvent = new CustomEvent('cm-task-started', {
|
||||
detail: {
|
||||
type: 'cm-task-started',
|
||||
ui_id: 'test-ui-id'
|
||||
}
|
||||
})
|
||||
taskStartedCallback(taskStartedEvent)
|
||||
await nextTick()
|
||||
|
||||
// Now simulate receiving a log event
|
||||
const mockEvent = new CustomEvent('logs', {
|
||||
detail: {
|
||||
type: 'logs',
|
||||
@@ -149,7 +113,7 @@ describe('useServerLogs', () => {
|
||||
} as unknown as LogsWsMessage
|
||||
}) as CustomEvent<LogsWsMessage>
|
||||
|
||||
logsCallback(mockEvent)
|
||||
eventCallback(mockEvent)
|
||||
await nextTick()
|
||||
|
||||
expect(logs.value).toEqual(['Log message 1 dont remove me', ''])
|
||||
|
||||
@@ -1,130 +1,32 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { useManagerQueue } from '@/composables/useManagerQueue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
// Mock VueUse's useEventListener
|
||||
const mockEventListeners = new Map()
|
||||
const mockWheneverCallback = vi.fn()
|
||||
|
||||
vi.mock('@vueuse/core', async () => {
|
||||
const actual = await vi.importActual('@vueuse/core')
|
||||
return {
|
||||
...actual,
|
||||
useEventListener: vi.fn((target, event, handler) => {
|
||||
if (!mockEventListeners.has(event)) {
|
||||
mockEventListeners.set(event, [])
|
||||
}
|
||||
mockEventListeners.get(event).push(handler)
|
||||
|
||||
// Mock the addEventListener behavior
|
||||
if (target && target.addEventListener) {
|
||||
target.addEventListener(event, handler)
|
||||
}
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
if (target && target.removeEventListener) {
|
||||
target.removeEventListener(event, handler)
|
||||
}
|
||||
}
|
||||
}),
|
||||
whenever: vi.fn((_source, cb) => {
|
||||
mockWheneverCallback.mockImplementation(cb)
|
||||
})
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn()
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
api: {
|
||||
clientId: 'test-client-id',
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/services/comfyManagerService', () => ({
|
||||
useComfyManagerService: vi.fn(() => ({
|
||||
getTaskQueue: vi.fn().mockResolvedValue({
|
||||
queue_running: [],
|
||||
queue_pending: []
|
||||
}),
|
||||
getTaskHistory: vi.fn().mockResolvedValue({}),
|
||||
clearTaskHistory: vi.fn().mockResolvedValue(null),
|
||||
deleteTaskHistoryItems: vi.fn().mockResolvedValue(null)
|
||||
}))
|
||||
}))
|
||||
|
||||
const mockShowManagerProgressDialog = vi.fn()
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: vi.fn(() => ({
|
||||
showManagerProgressDialog: mockShowManagerProgressDialog
|
||||
}))
|
||||
}))
|
||||
|
||||
describe('useManagerQueue', () => {
|
||||
let taskHistory: any
|
||||
let taskQueue: any
|
||||
let installedPacks: any
|
||||
|
||||
// Helper functions
|
||||
const createMockTask = (
|
||||
id: string,
|
||||
clientId = 'test-client-id',
|
||||
additional = {}
|
||||
) => ({
|
||||
id,
|
||||
client_id: clientId,
|
||||
...additional
|
||||
const createMockTask = (result: any = 'result') => ({
|
||||
task: vi.fn().mockResolvedValue(result),
|
||||
onComplete: vi.fn()
|
||||
})
|
||||
|
||||
const createMockHistoryItem = (
|
||||
clientId = 'test-client-id',
|
||||
result = 'success',
|
||||
additional = {}
|
||||
) => ({
|
||||
client_id: clientId,
|
||||
result,
|
||||
...additional
|
||||
})
|
||||
|
||||
const createMockState = (overrides = {}) => ({
|
||||
running_queue: [],
|
||||
pending_queue: [],
|
||||
history: {},
|
||||
installed_packs: {},
|
||||
...overrides
|
||||
})
|
||||
|
||||
const triggerWebSocketEvent = (eventType: string, state: any) => {
|
||||
const mockEventListener = app.api.addEventListener as any
|
||||
const eventCall = mockEventListener.mock.calls.find(
|
||||
(call: any) => call[0] === eventType
|
||||
)
|
||||
|
||||
if (eventCall) {
|
||||
const handler = eventCall[1]
|
||||
handler({
|
||||
type: eventType,
|
||||
detail: { state }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const getEventHandler = (eventType: string) => {
|
||||
const mockEventListener = app.api.addEventListener as any
|
||||
const eventCall = mockEventListener.mock.calls.find(
|
||||
(call: any) => call[0] === eventType
|
||||
)
|
||||
return eventCall ? eventCall[1] : null
|
||||
const createQueueWithMockTask = () => {
|
||||
const queue = useManagerQueue()
|
||||
const mockTask = createMockTask()
|
||||
queue.enqueueTask(mockTask)
|
||||
return { queue, mockTask }
|
||||
}
|
||||
|
||||
const getEventListenerCallback = () =>
|
||||
vi.mocked(app.api.addEventListener).mock.calls[0][1]
|
||||
vi.mocked(api.addEventListener).mock.calls[0][1]
|
||||
|
||||
const simulateServerStatus = async (status: 'all-done' | 'in_progress') => {
|
||||
const event = new CustomEvent('cm-queue-status', {
|
||||
@@ -134,27 +36,12 @@ describe('useManagerQueue', () => {
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
const createMockQueueTask = (result = 'success') => ({
|
||||
task: vi.fn().mockResolvedValue(result),
|
||||
onComplete: vi.fn()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockEventListeners.clear()
|
||||
taskHistory = ref({})
|
||||
taskQueue = ref({
|
||||
history: {},
|
||||
running_queue: [],
|
||||
pending_queue: [],
|
||||
installed_packs: {}
|
||||
})
|
||||
installedPacks = ref({})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockEventListeners.clear()
|
||||
})
|
||||
|
||||
describe('initialization', () => {
|
||||
@@ -170,7 +57,7 @@ describe('useManagerQueue', () => {
|
||||
describe('queue management', () => {
|
||||
it('should add tasks to the queue', () => {
|
||||
const queue = useManagerQueue()
|
||||
const mockTask = createMockQueueTask()
|
||||
const mockTask = createMockTask()
|
||||
|
||||
queue.enqueueTask(mockTask)
|
||||
|
||||
@@ -182,8 +69,8 @@ describe('useManagerQueue', () => {
|
||||
const queue = useManagerQueue()
|
||||
|
||||
// Add some tasks
|
||||
queue.enqueueTask(createMockQueueTask())
|
||||
queue.enqueueTask(createMockQueueTask())
|
||||
queue.enqueueTask(createMockTask())
|
||||
queue.enqueueTask(createMockTask())
|
||||
|
||||
expect(queue.queueLength.value).toBe(2)
|
||||
|
||||
@@ -193,50 +80,59 @@ describe('useManagerQueue', () => {
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should set up event listeners on creation', () => {
|
||||
useManagerQueue()
|
||||
|
||||
expect(app.api.addEventListener).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('processing state handling', () => {
|
||||
it('should update processing state based on queue length', async () => {
|
||||
describe('server status handling', () => {
|
||||
it('should update server status when receiving websocket events', async () => {
|
||||
const queue = useManagerQueue()
|
||||
|
||||
// Initially empty queue
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
expect(queue.statusMessage.value).toBe('all-done')
|
||||
await simulateServerStatus('in_progress')
|
||||
|
||||
// Add tasks to queue
|
||||
queue.enqueueTask(createMockTask())
|
||||
queue.enqueueTask(createMockTask())
|
||||
|
||||
expect(queue.queueLength.value).toBe(2)
|
||||
expect(queue.statusMessage.value).toBe('in_progress')
|
||||
expect(queue.allTasksDone.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle server status changes', async () => {
|
||||
it('should handle invalid status values gracefully', async () => {
|
||||
const queue = useManagerQueue()
|
||||
|
||||
// Test status change to in_progress
|
||||
await simulateServerStatus('in_progress')
|
||||
expect(queue.statusMessage.value).toBe('in_progress')
|
||||
// Simulate an invalid status
|
||||
const event = new CustomEvent('cm-queue-status', {
|
||||
detail: null as any
|
||||
})
|
||||
|
||||
// Test status change back to all-done
|
||||
await simulateServerStatus('all-done')
|
||||
getEventListenerCallback()!(event)
|
||||
await nextTick()
|
||||
|
||||
// Should maintain the default status
|
||||
expect(queue.statusMessage.value).toBe('all-done')
|
||||
})
|
||||
|
||||
it('should handle missing status property gracefully', async () => {
|
||||
const queue = useManagerQueue()
|
||||
|
||||
// Simulate a detail object without status property
|
||||
const event = new CustomEvent('cm-queue-status', {
|
||||
detail: { someOtherProperty: 'value' } as any
|
||||
})
|
||||
|
||||
getEventListenerCallback()!(event)
|
||||
await nextTick()
|
||||
|
||||
// Should maintain the default status
|
||||
expect(queue.statusMessage.value).toBe('all-done')
|
||||
})
|
||||
})
|
||||
|
||||
describe('task state management', () => {
|
||||
const createQueueWithMockTask = () => {
|
||||
const queue = useManagerQueue()
|
||||
const mockTask = createMockTask()
|
||||
queue.enqueueTask(mockTask)
|
||||
return { queue, mockTask }
|
||||
}
|
||||
describe('task execution', () => {
|
||||
it('should start the next task when server is idle and queue has items', async () => {
|
||||
const { queue, mockTask } = createQueueWithMockTask()
|
||||
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Task should have been started
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
})
|
||||
|
||||
it('should execute onComplete callback when task completes and server becomes idle', async () => {
|
||||
const { mockTask } = createQueueWithMockTask()
|
||||
@@ -341,7 +237,7 @@ describe('useManagerQueue', () => {
|
||||
expect(mockTask.onComplete).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle multiple tasks enqueued at once while server busy', async () => {
|
||||
it('should handle multiple multiple tasks enqueued at once while server busy', async () => {
|
||||
const queue = useManagerQueue()
|
||||
const mockTask1 = createMockTask()
|
||||
const mockTask2 = createMockTask()
|
||||
@@ -377,6 +273,10 @@ describe('useManagerQueue', () => {
|
||||
expect(mockTask2.onComplete).toHaveBeenCalled()
|
||||
expect(mockTask3.onComplete).not.toHaveBeenCalled()
|
||||
|
||||
// Verify state of queue
|
||||
expect(queue.queueLength.value).toBe(1)
|
||||
expect(queue.allTasksDone.value).toBe(false)
|
||||
|
||||
// Task 3
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
@@ -390,7 +290,30 @@ describe('useManagerQueue', () => {
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle server status changes with empty queue', async () => {
|
||||
it('should handle adding tasks while processing is in progress', async () => {
|
||||
const queue = useManagerQueue()
|
||||
const mockTask1 = createMockTask()
|
||||
const mockTask2 = createMockTask()
|
||||
|
||||
// Add first task and start processing
|
||||
queue.enqueueTask(mockTask1)
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask1.task).toHaveBeenCalled()
|
||||
|
||||
// Add second task while first is processing
|
||||
queue.enqueueTask(mockTask2)
|
||||
expect(queue.queueLength.value).toBe(1)
|
||||
|
||||
// Complete first task
|
||||
await mockTask1.task.mock.results[0].value
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Second task should now be processed
|
||||
expect(mockTask2.task).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle server status changes without tasks in queue', async () => {
|
||||
const queue = useManagerQueue()
|
||||
|
||||
// Cycle server status without any tasks
|
||||
@@ -399,387 +322,8 @@ describe('useManagerQueue', () => {
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
// Should not cause any errors
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('legacy queue data management', () => {
|
||||
beforeEach(() => {
|
||||
taskHistory = ref({})
|
||||
taskQueue = ref({
|
||||
history: {},
|
||||
running_queue: [],
|
||||
pending_queue: [],
|
||||
installed_packs: {}
|
||||
})
|
||||
installedPacks = ref({})
|
||||
})
|
||||
|
||||
it('should provide access to task queue state', async () => {
|
||||
const runningTasks = [createMockTask('task1')]
|
||||
const pendingTasks = [createMockTask('task2'), createMockTask('task3')]
|
||||
|
||||
taskQueue.value.running_queue = runningTasks
|
||||
taskQueue.value.pending_queue = pendingTasks
|
||||
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
await nextTick()
|
||||
|
||||
expect(queue.taskQueue.value.running_queue).toEqual(runningTasks)
|
||||
expect(queue.taskQueue.value.pending_queue).toEqual(pendingTasks)
|
||||
expect(queue.queueLength.value).toBe(3)
|
||||
})
|
||||
|
||||
it('should provide access to task history', async () => {
|
||||
const mockHistory = {
|
||||
task1: createMockHistoryItem(),
|
||||
task2: createMockHistoryItem('test-client-id', 'error')
|
||||
}
|
||||
taskHistory.value = mockHistory
|
||||
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
await nextTick()
|
||||
|
||||
expect(queue.taskHistory.value).toEqual(mockHistory)
|
||||
expect(queue.historyCount.value).toBe(2)
|
||||
})
|
||||
|
||||
it('should handle empty state gracefully', async () => {
|
||||
taskQueue.value.running_queue = []
|
||||
taskQueue.value.pending_queue = []
|
||||
taskHistory.value = {}
|
||||
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
await nextTick()
|
||||
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
expect(queue.historyCount.value).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('state management', () => {
|
||||
it('should provide reactive task history', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
taskHistory.value = {
|
||||
task1: createMockHistoryItem(),
|
||||
task2: createMockHistoryItem('test-client-id', 'error')
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(queue.taskHistory.value).toEqual(taskHistory.value)
|
||||
expect(queue.historyCount.value).toBe(2)
|
||||
})
|
||||
|
||||
it('should provide reactive installed packs', async () => {
|
||||
installedPacks.value = {
|
||||
pack1: { version: '1.0' },
|
||||
pack2: { version: '2.0' }
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// The composable should have access to installedPacks through the parameter
|
||||
expect(installedPacks.value).toEqual({
|
||||
pack1: { version: '1.0' },
|
||||
pack2: { version: '2.0' }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('computed properties', () => {
|
||||
it('should correctly compute allTasksDone', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
// Empty queue = all done
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
|
||||
// Add pending tasks
|
||||
taskQueue.value.pending_queue = [createMockTask('task1')]
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(queue.allTasksDone.value).toBe(false)
|
||||
|
||||
// Clear queue
|
||||
taskQueue.value.running_queue = []
|
||||
taskQueue.value.pending_queue = []
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should correctly compute queueLength', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
|
||||
taskQueue.value.running_queue = [createMockTask('task1')]
|
||||
taskQueue.value.pending_queue = [
|
||||
createMockTask('task2'),
|
||||
createMockTask('task3')
|
||||
]
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(queue.queueLength.value).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('client filtering functionality', () => {
|
||||
it('should filter tasks by client ID in WebSocket events', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const mockState = createMockState({
|
||||
running_queue: [
|
||||
createMockTask('task1'),
|
||||
createMockTask('task2', 'other-client-id')
|
||||
],
|
||||
pending_queue: [createMockTask('task3')]
|
||||
})
|
||||
|
||||
triggerWebSocketEvent('cm-task-completed', mockState)
|
||||
await nextTick()
|
||||
|
||||
// Should only include tasks from this client
|
||||
expect(taskQueue.value.running_queue).toEqual([createMockTask('task1')])
|
||||
expect(taskQueue.value.pending_queue).toEqual([createMockTask('task3')])
|
||||
expect(queue.queueLength.value).toBe(2)
|
||||
})
|
||||
|
||||
it('should filter history by client ID in WebSocket events', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const mockState = createMockState({
|
||||
history: {
|
||||
task1: createMockHistoryItem(),
|
||||
task2: createMockHistoryItem('other-client-id'),
|
||||
task3: createMockHistoryItem()
|
||||
}
|
||||
})
|
||||
|
||||
triggerWebSocketEvent('cm-task-completed', mockState)
|
||||
await nextTick()
|
||||
|
||||
// Should only include history items from this client
|
||||
expect(Object.keys(taskHistory.value)).toHaveLength(2)
|
||||
expect(taskHistory.value).toHaveProperty('task1')
|
||||
expect(taskHistory.value).toHaveProperty('task3')
|
||||
expect(taskHistory.value).not.toHaveProperty('task2')
|
||||
expect(queue.historyCount.value).toBe(2)
|
||||
})
|
||||
|
||||
it('should handle all tasks being from other clients', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const mockState = createMockState({
|
||||
running_queue: [
|
||||
createMockTask('task1', 'other-client-1'),
|
||||
createMockTask('task2', 'other-client-2')
|
||||
],
|
||||
pending_queue: [createMockTask('task3', 'other-client-1')],
|
||||
history: {
|
||||
task4: createMockHistoryItem('other-client-1'),
|
||||
task5: createMockHistoryItem('other-client-2')
|
||||
}
|
||||
})
|
||||
|
||||
triggerWebSocketEvent('cm-task-completed', mockState)
|
||||
await nextTick()
|
||||
|
||||
// Should have no tasks or history
|
||||
expect(taskQueue.value.running_queue).toEqual([])
|
||||
expect(taskQueue.value.pending_queue).toEqual([])
|
||||
expect(taskHistory.value).toEqual({})
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
expect(queue.historyCount.value).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('WebSocket event handling', () => {
|
||||
it('should handle task done events', async () => {
|
||||
useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const mockState = createMockState({
|
||||
running_queue: [createMockTask('task1')],
|
||||
history: {
|
||||
task1: createMockHistoryItem()
|
||||
},
|
||||
installed_packs: { pack1: { version: '1.0' } }
|
||||
})
|
||||
|
||||
triggerWebSocketEvent('cm-task-completed', mockState)
|
||||
await nextTick()
|
||||
|
||||
expect(taskQueue.value.running_queue).toEqual([createMockTask('task1')])
|
||||
expect(taskQueue.value.pending_queue).toEqual([])
|
||||
expect(taskHistory.value).toEqual({
|
||||
task1: createMockHistoryItem()
|
||||
})
|
||||
expect(installedPacks.value).toEqual({ pack1: { version: '1.0' } })
|
||||
})
|
||||
|
||||
it('should handle task started events', async () => {
|
||||
useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const mockState = createMockState({
|
||||
running_queue: [createMockTask('task1')],
|
||||
pending_queue: [createMockTask('task2')],
|
||||
installed_packs: { pack1: { version: '1.0' } }
|
||||
})
|
||||
|
||||
triggerWebSocketEvent('cm-task-started', mockState)
|
||||
await nextTick()
|
||||
|
||||
expect(taskQueue.value.running_queue).toEqual([createMockTask('task1')])
|
||||
expect(taskQueue.value.pending_queue).toEqual([createMockTask('task2')])
|
||||
expect(installedPacks.value).toEqual({ pack1: { version: '1.0' } })
|
||||
})
|
||||
|
||||
it('should filter out tasks from other clients in WebSocket events', async () => {
|
||||
useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const mockState = createMockState({
|
||||
running_queue: [
|
||||
createMockTask('task1'),
|
||||
createMockTask('task2', 'other-client-id')
|
||||
],
|
||||
pending_queue: [createMockTask('task3', 'other-client-id')],
|
||||
history: {
|
||||
task1: createMockHistoryItem(),
|
||||
task2: createMockHistoryItem('other-client-id')
|
||||
}
|
||||
})
|
||||
|
||||
triggerWebSocketEvent('cm-task-completed', mockState)
|
||||
await nextTick()
|
||||
|
||||
// Should only include tasks from this client
|
||||
expect(taskQueue.value.running_queue).toEqual([createMockTask('task1')])
|
||||
expect(taskQueue.value.pending_queue).toEqual([])
|
||||
expect(taskHistory.value).toEqual({
|
||||
task1: createMockHistoryItem()
|
||||
})
|
||||
})
|
||||
|
||||
it('should ignore events with wrong type', async () => {
|
||||
useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const handler = getEventHandler('cm-task-completed')
|
||||
|
||||
// Send event with wrong type
|
||||
handler({
|
||||
type: 'wrong-event-type',
|
||||
detail: {
|
||||
state: createMockState({ running_queue: [createMockTask('task1')] })
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
// Should not update state
|
||||
expect(taskQueue.value.running_queue).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('cleanup functionality', () => {
|
||||
it('should clean up event listeners on stopListening', () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
const mockRemoveEventListener = app.api.removeEventListener as any
|
||||
|
||||
queue.stopListening()
|
||||
|
||||
expect(mockRemoveEventListener).toHaveBeenCalledTimes(2)
|
||||
|
||||
// Check that both event types were called with the correct event names
|
||||
const calls = mockRemoveEventListener.mock.calls
|
||||
const eventTypes = calls.map((call: any) => call[0])
|
||||
expect(eventTypes).toContain('cm-task-completed')
|
||||
expect(eventTypes).toContain('cm-task-started')
|
||||
|
||||
// Check that functions were passed as second parameter
|
||||
calls.forEach((call: any) => {
|
||||
expect(typeof call[1]).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle multiple stopListening calls gracefully', () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
const mockRemoveEventListener = app.api.removeEventListener as any
|
||||
|
||||
queue.stopListening()
|
||||
queue.stopListening()
|
||||
|
||||
// Should still only be called twice (once per event type)
|
||||
expect(mockRemoveEventListener).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle undefined installed_packs in state update', async () => {
|
||||
useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const mockState = createMockState({
|
||||
running_queue: [createMockTask('task1')],
|
||||
installed_packs: undefined
|
||||
})
|
||||
|
||||
triggerWebSocketEvent('cm-task-completed', mockState)
|
||||
await nextTick()
|
||||
|
||||
// Should not update installedPacks when undefined
|
||||
expect(installedPacks.value).toEqual({})
|
||||
})
|
||||
|
||||
it('should handle rapid successive events', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
// Send multiple events rapidly
|
||||
for (let i = 0; i < 10; i++) {
|
||||
triggerWebSocketEvent(
|
||||
'cm-task-completed',
|
||||
createMockState({
|
||||
running_queue: [createMockTask(`task${i}`)],
|
||||
history: { [`task${i}`]: createMockHistoryItem() }
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Should have the last state
|
||||
expect(taskQueue.value.running_queue).toEqual([createMockTask('task9')])
|
||||
expect(queue.queueLength.value).toBe(1)
|
||||
})
|
||||
|
||||
it('should maintain consistency when mixing event types', async () => {
|
||||
useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
// Send alternating event types
|
||||
triggerWebSocketEvent(
|
||||
'cm-task-started',
|
||||
createMockState({
|
||||
running_queue: [createMockTask('task1')],
|
||||
pending_queue: [createMockTask('task2')]
|
||||
})
|
||||
)
|
||||
|
||||
triggerWebSocketEvent(
|
||||
'cm-task-completed',
|
||||
createMockState({
|
||||
running_queue: [],
|
||||
pending_queue: [createMockTask('task2')],
|
||||
history: { task1: createMockHistoryItem() }
|
||||
})
|
||||
)
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(taskQueue.value.running_queue).toEqual([])
|
||||
expect(taskQueue.value.pending_queue).toEqual([createMockTask('task2')])
|
||||
expect(taskHistory.value).toHaveProperty('task1')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user