feat: Complete manager migration with conflict detection integration

This completes the integration of ComfyUI Manager migration features with enhanced conflict detection system. Key changes include:

## Manager Migration & Conflict Detection
- Integrated PR #4637 (4-state manager restart workflow) with PR #4654 (comprehensive conflict detection)
- Fixed conflict detection to properly check `latest_version` fields for registry API compatibility
- Added conflict detection to PackCardFooter and InfoPanelHeader for comprehensive warning coverage
- Merged missing English locale translations from main branch with proper conflict resolution

## Bug Fixes
- Fixed double API path issue (`/api/v2/v2/`) in manager service routes
- Corrected PackUpdateButton payload structure and service method calls
- Enhanced conflict detection system to handle both installed and registry package structures

## Technical Improvements
- Updated conflict detection composable to handle both installed and registry package structures
- Enhanced manager service with proper error handling and route corrections
- Improved type safety across manager components with proper TypeScript definitions
This commit is contained in:
bymyself
2025-09-01 02:53:26 -07:00
parent 183bba0724
commit fdf4ffd94a
14 changed files with 384 additions and 404 deletions

View File

@@ -93,15 +93,33 @@ import VerifiedIcon from '@/components/icons/VerifiedIcon.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { useComfyRegistryService } from '@/services/comfyRegistryService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import {
ManagerChannel,
ManagerDatabaseSource,
SelectedVersion
} from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
import { getJoinedConflictMessages } from '@/utils/conflictMessageUtil'
import { isSemVer } from '@/utils/formatUtil'
type ManagerChannel = ManagerComponents['schemas']['ManagerChannel']
type ManagerDatabaseSource =
ManagerComponents['schemas']['ManagerDatabaseSource']
type SelectedVersion = ManagerComponents['schemas']['SelectedVersion']
// Enum values for runtime use
const SelectedVersionValues = {
LATEST: 'latest' as SelectedVersion,
NIGHTLY: 'nightly' as SelectedVersion
}
const ManagerChannelValues = {
STABLE: 'stable' as ManagerChannel,
DEV: 'dev' as ManagerChannel
}
const ManagerDatabaseSourceValues = {
CACHE: 'cache' as ManagerDatabaseSource,
REMOTE: 'remote' as ManagerDatabaseSource,
LOCAL: 'local' as ManagerDatabaseSource
}
const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
}>()
@@ -118,19 +136,21 @@ const { checkNodeCompatibility } = useConflictDetection()
const isQueueing = ref(false)
const selectedVersion = ref<string>(SelectedVersion.LATEST)
const selectedVersion = ref<string>(SelectedVersionValues.LATEST)
onMounted(() => {
const initialVersion = getInitialSelectedVersion() ?? SelectedVersion.LATEST
const initialVersion =
getInitialSelectedVersion() ?? SelectedVersionValues.LATEST
selectedVersion.value =
// Use NIGHTLY when version is a Git hash
isSemVer(initialVersion) ? initialVersion : SelectedVersion.NIGHTLY
isSemVer(initialVersion) ? initialVersion : SelectedVersionValues.NIGHTLY
})
const getInitialSelectedVersion = () => {
if (!nodePack.id) return
// If unclaimed, set selected version to nightly
if (nodePack.publisher?.name === 'Unclaimed') return SelectedVersion.NIGHTLY
if (nodePack.publisher?.name === 'Unclaimed')
return SelectedVersionValues.NIGHTLY
// If node pack is installed, set selected version to the installed version
if (managerStore.isPackInstalled(nodePack.id))
@@ -180,7 +200,7 @@ const onNodePackChange = async () => {
// Add Latest option
const defaultVersions = [
{
value: SelectedVersion.LATEST,
value: SelectedVersionValues.LATEST,
label: latestLabel
}
]
@@ -188,7 +208,7 @@ const onNodePackChange = async () => {
// Add Nightly option if there is a non-empty `repository` field
if (nodePack.repository?.length) {
defaultVersions.push({
value: SelectedVersion.NIGHTLY,
value: SelectedVersionValues.NIGHTLY,
label: t('manager.nightlyVersion')
})
}
@@ -222,8 +242,8 @@ const handleSubmit = async () => {
await managerStore.installPack.call({
id: nodePack.id,
repository: nodePack.repository ?? '',
channel: ManagerChannel.DEFAULT,
mode: ManagerDatabaseSource.CACHE,
channel: ManagerChannelValues.STABLE,
mode: ManagerDatabaseSourceValues.CACHE,
version: actualVersion,
selected_version: selectedVersion.value
})

View File

@@ -38,8 +38,8 @@ import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgme
import { useDialogService } from '@/services/dialogService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
const TOGGLE_DEBOUNCE_MS = 256
@@ -108,8 +108,12 @@ const handleEnable = () => {
}
return enablePack.call({
id: nodePack.id,
version: version.value ?? ('latest' as ManagerComponents['schemas']['SelectedVersion']),
selected_version: version.value ?? ('latest' as ManagerComponents['schemas']['SelectedVersion']),
version:
version.value ??
('latest' as ManagerComponents['schemas']['SelectedVersion']),
selected_version:
version.value ??
('latest' as ManagerComponents['schemas']['SelectedVersion']),
repository: nodePack.repository ?? '',
channel: 'default' as ManagerComponents['schemas']['ManagerChannel'],
mode: 'cache' as ManagerComponents['schemas']['ManagerDatabaseSource'],
@@ -123,7 +127,9 @@ const handleDisable = () => {
}
return disablePack({
id: nodePack.id,
version: version.value ?? ('latest' as ManagerComponents['schemas']['SelectedVersion'])
version:
version.value ??
('latest' as ManagerComponents['schemas']['SelectedVersion'])
})
}

View File

@@ -33,11 +33,11 @@ import { useDialogService } from '@/services/dialogService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { ButtonSize } from '@/types/buttonTypes'
import type { components } from '@/types/comfyRegistryTypes'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
import {
type ConflictDetail,
ConflictDetectionResult
} from '@/types/conflictDetectionTypes'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
type NodePack = components['schemas']['Node']
@@ -123,7 +123,7 @@ const installAllPacks = async () => {
})
return
}
// No conflicts or conflicts acknowledged - proceed with installation
const uninstalledPacks = nodePacks.filter(
(pack) => !managerStore.isPackInstalled(pack.id)

View File

@@ -1,21 +1,26 @@
<template>
<PackActionButton
<IconTextButton
v-bind="$attrs"
variant="black"
type="transparent"
:label="$t('manager.updateAll')"
:loading="isUpdating"
:loading-message="$t('g.updating')"
@action="updateAllPacks"
/>
:border="true"
size="sm"
:disabled="isUpdating"
@click="updateAllPacks"
>
<template v-if="isUpdating" #icon>
<DotSpinner duration="1s" :size="12" />
</template>
</IconTextButton>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import DotSpinner from '@/components/common/DotSpinner.vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { components } from '@/types/comfyRegistryTypes'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
type NodePack = components['schemas']['Node']
@@ -27,16 +32,10 @@ const isUpdating = ref<boolean>(false)
const managerStore = useComfyManagerStore()
const createPayload = (
updateItem: NodePack
): ManagerComponents['schemas']['ManagerPackInfo'] => {
if (!updateItem.id) {
throw new Error('Node ID is required for update')
}
const createPayload = (updateItem: NodePack) => {
return {
id: updateItem.id,
version: updateItem.latest_version?.version || 'latest'
id: updateItem.id!,
version: updateItem.latest_version!.version!
}
}
@@ -53,21 +52,16 @@ const updateAllPacks = async () => {
console.warn('No packs provided for update')
return
}
isUpdating.value = true
const updatablePacks = nodePacks.filter((pack) =>
managerStore.isPackInstalled(pack.id)
)
if (!updatablePacks.length) {
console.info('No installed packs available for update')
isUpdating.value = false
return
}
console.info(`Starting update of ${updatablePacks.length} packs`)
try {
await Promise.all(updatablePacks.map(updatePack))
managerStore.updatePack.clear()

View File

@@ -28,7 +28,8 @@
size="md"
:is-installing="isInstalling"
:node-packs="nodePacks"
:has-conflict="hasConflict"
:has-conflict="hasConflict || computedHasConflict"
:conflict-info="conflictInfo"
/>
</slot>
</div>
@@ -48,8 +49,10 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import PackUninstallButton from '@/components/dialog/content/manager/button/PackUninstallButton.vue'
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/types/importFailedTypes'
const { nodePacks, hasConflict } = defineProps<{
@@ -79,4 +82,23 @@ const isInstalling = computed(() => {
if (!nodePacks?.length) return false
return nodePacks.some((pack) => managerStore.isPackInstalling(pack.id))
})
// Add conflict detection for install button dialog
const { checkNodeCompatibility } = useConflictDetection()
// Compute conflict info for all node packs
const conflictInfo = computed<ConflictDetail[]>(() => {
if (!nodePacks?.length) return []
const allConflicts: ConflictDetail[] = []
for (const nodePack of nodePacks) {
const compatibilityCheck = checkNodeCompatibility(nodePack)
if (compatibilityCheck.conflicts) {
allConflicts.push(...compatibilityCheck.conflicts)
}
}
return allConflicts
})
const computedHasConflict = computed(() => conflictInfo.value.length > 0)
</script>

View File

@@ -10,6 +10,8 @@
v-if="!isInstalled"
:node-packs="[nodePack]"
:is-installing="isInstalling"
:has-conflict="hasConflicts"
:conflict-info="conflictInfo"
/>
<PackEnableToggle v-else :node-pack="nodePack" />
</div>
@@ -21,9 +23,11 @@ import { useI18n } from 'vue-i18n'
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { IsInstallingKey } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
@@ -38,4 +42,16 @@ const { n } = useI18n()
const formattedDownloads = computed(() =>
nodePack.downloads ? n(nodePack.downloads) : ''
)
// Add conflict detection for the card button
const { checkNodeCompatibility } = useConflictDetection()
// Check for conflicts with this specific node pack
const conflictInfo = computed<ConflictDetail[]>(() => {
if (!nodePack) return []
const compatibilityCheck = checkNodeCompatibility(nodePack)
return compatibilityCheck.conflicts || []
})
const hasConflicts = computed(() => conflictInfo.value.length > 0)
</script>

View File

@@ -712,16 +712,27 @@ export function useConflictDetection() {
const conflicts: ConflictDetail[] = []
// Check OS compatibility using centralized function
if (node.supported_os && node.supported_os.length > 0) {
// First try latest_version (most accurate), then fallback to top level
const supportedOS =
('latest_version' in node ? node.latest_version?.supported_os : null) ||
node.supported_os
if (supportedOS && supportedOS.length > 0) {
const currentOS = systemStats.system?.os || 'unknown'
const osConflict = checkOSConflict(node.supported_os, currentOS)
const osConflict = checkOSConflict(supportedOS, currentOS)
if (osConflict) {
conflicts.push(osConflict)
}
}
// Check accelerator compatibility using centralized function
if (node.supported_accelerators && node.supported_accelerators.length > 0) {
// First try latest_version (most accurate), then fallback to top level
const supportedAccelerators =
('latest_version' in node
? node.latest_version?.supported_accelerators
: null) || node.supported_accelerators
if (supportedAccelerators && supportedAccelerators.length > 0) {
// Extract available accelerators from system stats
const acceleratorInfo = extractAcceleratorInfo(systemStats)
const availableAccelerators: Node['supported_accelerators'] = []
@@ -733,7 +744,7 @@ export function useConflictDetection() {
})
const acceleratorConflict = checkAcceleratorConflict(
node.supported_accelerators,
supportedAccelerators,
availableAccelerators
)
if (acceleratorConflict) {
@@ -742,13 +753,19 @@ export function useConflictDetection() {
}
// Check ComfyUI version compatibility
if (node.supported_comfyui_version) {
// First try latest_version (most accurate), then fallback to top level
const comfyuiVersionRequirement =
('latest_version' in node
? node.latest_version?.supported_comfyui_version
: null) || node.supported_comfyui_version
if (comfyuiVersionRequirement) {
const currentComfyUIVersion = systemStats.system?.comfyui_version
if (currentComfyUIVersion && currentComfyUIVersion !== 'unknown') {
const versionConflict = utilCheckVersionCompatibility(
'comfyui_version',
currentComfyUIVersion,
node.supported_comfyui_version
comfyuiVersionRequirement
)
if (versionConflict) {
conflicts.push(versionConflict)
@@ -757,13 +774,19 @@ export function useConflictDetection() {
}
// Check ComfyUI Frontend version compatibility
if (node.supported_comfyui_frontend_version) {
// First try latest_version (most accurate), then fallback to top level
const frontendVersionRequirement =
('latest_version' in node
? node.latest_version?.supported_comfyui_frontend_version
: null) || node.supported_comfyui_frontend_version
if (frontendVersionRequirement) {
const currentFrontendVersion = config.app_version
if (currentFrontendVersion && currentFrontendVersion !== 'unknown') {
const versionConflict = utilCheckVersionCompatibility(
'frontend_version',
currentFrontendVersion,
node.supported_comfyui_frontend_version
frontendVersionRequirement
)
if (versionConflict) {
conflicts.push(versionConflict)

View File

@@ -24,6 +24,7 @@
"confirmed": "Confirmed",
"reset": "Reset",
"resetAll": "Reset All",
"clearFilters": "Clear Filters",
"resetAllKeybindingsTooltip": "Reset all keybindings to default",
"customizeFolder": "Customize Folder",
"icon": "Icon",
@@ -206,11 +207,6 @@
"installAllMissingNodes": "Install All Missing Nodes",
"packsSelected": "packs selected",
"mixedSelectionMessage": "Cannot perform bulk action on mixed selection",
"gettingInfo": "Getting info...",
"legacyMenuNotAvailable": "Legacy menu not available",
"legacyManagerUI": "Legacy Manager UI",
"legacyManagerUIDescription": "Switch to the legacy ComfyUI Manager interface",
"updateAll": "Update All",
"notAvailable": "Not Available",
"status": {
"active": "Active",
@@ -235,7 +231,7 @@
},
"conflicts": {
"title": "Node Pack Issues Detected!",
"description": "Weve detected conflicts between some of your extensions and the new version of ComfyUI. By updating you risk breaking workflows that rely on those extensions.",
"description": "We've detected conflicts between some of your extensions and the new version of ComfyUI. By updating you risk breaking workflows that rely on those extensions.",
"info": "If you continue with the update, the conflicting extensions will be disabled automatically. You can review and manage them anytime in the ComfyUI Manager.",
"extensionAtRisk": "Extension at Risk",
"conflicts": "Conflicts",
@@ -482,20 +478,6 @@
"error": "Unable to start ComfyUI Desktop"
}
},
"shortcuts": {
"essentials": "Essential",
"viewControls": "View Controls",
"manageShortcuts": "Manage Shortcuts",
"noKeybinding": "No keybinding",
"keyboardShortcuts": "Keyboard Shortcuts",
"subcategories": {
"workflow": "Workflow",
"node": "Node",
"queue": "Queue",
"view": "View",
"panelControls": "Panel Controls"
}
},
"serverConfig": {
"modifiedConfigs": "You have modified the following server configurations. Restart to apply changes.",
"revertChanges": "Revert Changes",
@@ -508,6 +490,14 @@
"queue": "Queue",
"nodeLibrary": "Node Library",
"workflows": "Workflows",
"templates": "Templates",
"labels": {
"queue": "Queue",
"nodes": "Nodes",
"models": "Models",
"workflows": "Workflows",
"templates": "Templates"
},
"browseTemplates": "Browse example templates",
"openWorkflow": "Open workflow in local file system",
"newBlankWorkflow": "Create a new blank workflow",
@@ -604,7 +594,14 @@
"clipspace": "Open Clipspace",
"resetView": "Reset canvas view",
"clear": "Clear workflow",
"toggleBottomPanel": "Toggle Bottom Panel"
"toggleBottomPanel": "Toggle Bottom Panel",
"theme": "Theme",
"dark": "Dark",
"light": "Light",
"manageExtensions": "Manage Extensions",
"settings": "Settings",
"help": "Help",
"queue": "Queue Panel"
},
"tabMenu": {
"duplicateTab": "Duplicate Tab",
@@ -1009,6 +1006,25 @@
"Mask Opacity": "Mask Opacity",
"Image Layer": "Image Layer"
},
"commands": {
"runWorkflow": "Run workflow",
"runWorkflowFront": "Run workflow (Queue at front)",
"run": "Run",
"execute": "Execute",
"interrupt": "Cancel current run",
"refresh": "Refresh node definitions",
"clipspace": "Open Clipspace",
"resetView": "Reset canvas view",
"clear": "Clear workflow",
"toggleBottomPanel": "Toggle Bottom Panel",
"theme": "Theme",
"dark": "Dark",
"light": "Light",
"manageExtensions": "Manage Extensions",
"settings": "Settings",
"help": "Help",
"queue": "Queue Panel"
},
"menuLabels": {
"Workflow": "Workflow",
"Edit": "Edit",
@@ -1026,6 +1042,7 @@
"Quit": "Quit",
"Reinstall": "Reinstall",
"Restart": "Restart",
"Open 3D Viewer (Beta) for Selected Node": "Open 3D Viewer (Beta) for Selected Node",
"Browse Templates": "Browse Templates",
"Delete Selected Items": "Delete Selected Items",
"Fit view to selected nodes": "Fit view to selected nodes",
@@ -1056,8 +1073,10 @@
"Export (API)": "Export (API)",
"Give Feedback": "Give Feedback",
"Convert Selection to Subgraph": "Convert Selection to Subgraph",
"Exit Subgraph": "Exit Subgraph",
"Fit Group To Contents": "Fit Group To Contents",
"Group Selected Nodes": "Group Selected Nodes",
"Unpack the selected Subgraph": "Unpack the selected Subgraph",
"Convert selected nodes to group node": "Convert selected nodes to group node",
"Manage group nodes": "Manage group nodes",
"Ungroup selected group nodes": "Ungroup selected group nodes",
@@ -1080,6 +1099,7 @@
"Unload Models and Execution Cache": "Unload Models and Execution Cache",
"New": "New",
"Clipspace": "Clipspace",
"Manager": "Manager",
"Open": "Open",
"Queue Prompt": "Queue Prompt",
"Queue Prompt (Front)": "Queue Prompt (Front)",
@@ -1089,6 +1109,8 @@
"Save": "Save",
"Save As": "Save As",
"Show Settings Dialog": "Show Settings Dialog",
"Canvas Performance": "Canvas Performance",
"Help Center": "Help Center",
"Toggle Theme (Dark/Light)": "Toggle Theme (Dark/Light)",
"Undo": "Undo",
"Open Sign In Dialog": "Open Sign In Dialog",
@@ -1163,7 +1185,8 @@
"User": "User",
"Credits": "Credits",
"API Nodes": "API Nodes",
"Notification Preferences": "Notification Preferences"
"Notification Preferences": "Notification Preferences",
"3DViewer": "3DViewer"
},
"serverConfigItems": {
"listen": {
@@ -1515,12 +1538,31 @@
"depth": "Depth",
"lineart": "Lineart"
},
"upDirections": {
"original": "Original"
},
"startRecording": "Start Recording",
"stopRecording": "Stop Recording",
"exportRecording": "Export Recording",
"clearRecording": "Clear Recording",
"resizeNodeMatchOutput": "Resize Node to match output",
"loadingBackgroundImage": "Loading Background Image"
"loadingBackgroundImage": "Loading Background Image",
"cameraType": {
"perspective": "Perspective",
"orthographic": "Orthographic"
},
"viewer": {
"title": "3D Viewer (Beta)",
"apply": "Apply",
"cancel": "Cancel",
"cameraType": "Camera Type",
"sceneSettings": "Scene Settings",
"cameraSettings": "Camera Settings",
"lightSettings": "Light Settings",
"exportSettings": "Export Settings",
"modelSettings": "Model Settings"
},
"openIn3DViewer": "Open in 3D Viewer"
},
"toastMessages": {
"nothingToQueue": "Nothing to queue",
@@ -1558,7 +1600,8 @@
"useApiKeyTip": "Tip: Can't access normal login? Use the Comfy API Key option.",
"nothingSelected": "Nothing selected",
"cannotCreateSubgraph": "Cannot create subgraph",
"failedToConvertToSubgraph": "Failed to convert items to subgraph"
"failedToConvertToSubgraph": "Failed to convert items to subgraph",
"failedToInitializeLoad3dViewer": "Failed to initialize 3D Viewer"
},
"auth": {
"apiKey": {
@@ -1632,15 +1675,6 @@
"passwordUpdate": {
"success": "Password Updated",
"successDetail": "Your password has been updated successfully"
},
"deleteAccount": {
"deleteAccount": "Delete Account",
"confirmTitle": "Delete Account",
"confirmMessage": "Are you sure you want to delete your account? This action cannot be undone and will permanently remove all your data.",
"confirm": "Delete Account",
"cancel": "Cancel",
"success": "Account Deleted",
"successDetail": "Your account has been successfully deleted."
}
},
"validation": {
@@ -1649,6 +1683,7 @@
"minLength": "Must be at least {length} characters",
"maxLength": "Must be no more than {length} characters",
"prefix": "Must start with {prefix}",
"descriptionRequired": "Description is required",
"length": "Must be {length} characters",
"password": {
"requirements": "Password requirements",
@@ -1720,5 +1755,32 @@
"whatsNewPopup": {
"learnMore": "Learn more",
"noReleaseNotes": "No release notes available."
},
"breadcrumbsMenu": {
"duplicate": "Duplicate",
"clearWorkflow": "Clear Workflow",
"deleteWorkflow": "Delete Workflow",
"enterNewName": "Enter new name"
},
"shortcuts": {
"essentials": "Essential",
"viewControls": "View Controls",
"manageShortcuts": "Manage Shortcuts",
"noKeybinding": "No keybinding",
"keyboardShortcuts": "Keyboard Shortcuts",
"subcategories": {
"workflow": "Workflow",
"node": "Node",
"queue": "Queue",
"view": "View",
"panelControls": "Panel Controls"
}
},
"minimap": {
"nodeColors": "Node Colors",
"showLinks": "Show Links",
"showGroups": "Show Frames/Groups",
"renderBypassState": "Render Bypass State",
"renderErrorState": "Render Error State"
}
}

View File

@@ -20,18 +20,18 @@ const GENERIC_SECURITY_ERR_MSG =
* API routes for ComfyUI Manager
*/
enum ManagerRoute {
START_QUEUE = 'v2/manager/queue/start',
RESET_QUEUE = 'v2/manager/queue/reset',
QUEUE_STATUS = 'v2/manager/queue/status',
UPDATE_ALL = 'v2/manager/queue/update_all',
LIST_INSTALLED = 'v2/customnode/installed',
GET_NODES = 'v2/customnode/getmappings',
IMPORT_FAIL_INFO = 'v2/customnode/import_fail_info',
IMPORT_FAIL_INFO_BULK = 'v2/customnode/import_fail_info_bulk',
REBOOT = 'v2/manager/reboot',
IS_LEGACY_MANAGER_UI = 'v2/manager/is_legacy_manager_ui',
TASK_HISTORY = 'v2/manager/queue/history',
QUEUE_TASK = 'v2/manager/queue/task'
START_QUEUE = 'manager/queue/start',
RESET_QUEUE = 'manager/queue/reset',
QUEUE_STATUS = 'manager/queue/status',
UPDATE_ALL = 'manager/queue/update_all',
LIST_INSTALLED = 'customnode/installed',
GET_NODES = 'customnode/getmappings',
IMPORT_FAIL_INFO = 'customnode/import_fail_info',
IMPORT_FAIL_INFO_BULK = 'customnode/import_fail_info_bulk',
REBOOT = 'manager/reboot',
IS_LEGACY_MANAGER_UI = 'manager/is_legacy_manager_ui',
TASK_HISTORY = 'manager/queue/history',
QUEUE_TASK = 'manager/queue/task'
}
const managerApiClient = axios.create({

View File

@@ -3,9 +3,6 @@ import type { InjectionKey, Ref } from 'vue'
import type { AlgoliaNodePack } from '@/types/algoliaTypes'
import type { components } from '@/types/comfyRegistryTypes'
import type { components as managerComponents } from '@/types/generatedManagerTypes'
import type { SearchMode } from '@/types/searchServiceTypes'
type WorkflowNodeProperties = ComfyWorkflowJSON['nodes'][0]['properties']
export type RegistryPack = components['schemas']['Node']
export type MergedNodePack = RegistryPack & AlgoliaNodePack
@@ -80,7 +77,6 @@ export enum SortableAlgoliaField {
Name = 'name'
}
export interface ManagerState {
selectedTabId: ManagerTab
searchQuery: string

View File

@@ -197,9 +197,9 @@ describe('ManagerProgressFooter', () => {
// Setup queue running state
mockComfyManagerStore.uncompletedCount = 3
mockTaskLogs.push(
{ taskName: 'Installing pack1', logs: [] },
{ taskName: 'Installing pack2', logs: [] },
{ taskName: 'Installing pack3', logs: [] }
{ taskName: 'Installing pack1', taskId: '1', logs: [] },
{ taskName: 'Installing pack2', taskId: '2', logs: [] },
{ taskName: 'Installing pack3', taskId: '3', logs: [] }
)
const wrapper = mountComponent()
@@ -223,7 +223,7 @@ describe('ManagerProgressFooter', () => {
it('should toggle expansion when expand button is clicked', async () => {
mockComfyManagerStore.uncompletedCount = 1
mockTaskLogs.push({ taskName: 'Installing', logs: [] })
mockTaskLogs.push({ taskName: 'Installing', taskId: '1', logs: [] })
const wrapper = mountComponent()
@@ -239,8 +239,8 @@ describe('ManagerProgressFooter', () => {
// Setup tasks completed state
mockComfyManagerStore.uncompletedCount = 0
mockTaskLogs.push(
{ taskName: 'Installed pack1', logs: [] },
{ taskName: 'Installed pack2', logs: [] }
{ taskName: 'Installed pack1', taskId: '1', logs: [] },
{ taskName: 'Installed pack2', taskId: '2', logs: [] }
)
mockComfyManagerStore.allTasksDone = true

View File

@@ -3,8 +3,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import type { InstalledPacksResponse } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
import type { components as ManagerComponents } from '@/types/generatedManagerTypes'
type InstalledPacksResponse =
ManagerComponents['schemas']['InstalledPacksResponse']
// Mock dependencies
vi.mock('@/scripts/api', () => ({

View File

@@ -1,39 +1,49 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { ref } from 'vue'
import { useManagerQueue } from '@/composables/useManagerQueue'
import { api } from '@/scripts/api'
import { components } from '@/types/generatedManagerTypes'
vi.mock('@/scripts/api', () => ({
api: {
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn()
// Mock dialog service
vi.mock('@/services/dialogService', () => ({
useDialogService: () => ({
showManagerProgressDialog: vi.fn()
})
}))
// Mock the app API
vi.mock('@/scripts/app', () => ({
app: {
api: {
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
clientId: 'test-client-id'
}
}
}))
type ManagerTaskHistory = Record<
string,
components['schemas']['TaskHistoryItem']
>
type ManagerTaskQueue = components['schemas']['TaskStateMessage']
describe('useManagerQueue', () => {
const createMockTask = (result: any = 'result') => ({
task: vi.fn().mockResolvedValue(result),
onComplete: vi.fn()
})
let taskHistory: any
let taskQueue: any
let installedPacks: any
const createQueueWithMockTask = () => {
const queue = useManagerQueue()
const mockTask = createMockTask()
queue.enqueueTask(mockTask)
return { queue, mockTask }
}
const getEventListenerCallback = () =>
vi.mocked(api.addEventListener).mock.calls[0][1]
const simulateServerStatus = async (status: 'all-done' | 'in_progress') => {
const event = new CustomEvent('cm-queue-status', {
detail: { status }
const createManagerQueue = () => {
taskHistory = ref<ManagerTaskHistory>({})
taskQueue = ref<ManagerTaskQueue>({
history: {},
running_queue: [],
pending_queue: [],
installed_packs: {}
})
getEventListenerCallback()!(event as any)
await nextTick()
installedPacks = ref({})
return useManagerQueue(taskHistory, taskQueue, installedPacks)
}
beforeEach(() => {
@@ -45,285 +55,111 @@ describe('useManagerQueue', () => {
})
describe('initialization', () => {
it('should initialize with empty queue and DONE status', () => {
const queue = useManagerQueue()
it('should initialize with empty state', () => {
const queue = createManagerQueue()
expect(queue.queueLength.value).toBe(0)
expect(queue.statusMessage.value).toBe('all-done')
expect(queue.currentQueueLength.value).toBe(0)
expect(queue.allTasksDone.value).toBe(true)
expect(queue.isProcessing.value).toBe(false)
expect(queue.historyCount.value).toBe(0)
})
})
describe('queue management', () => {
it('should add tasks to the queue', () => {
const queue = useManagerQueue()
const mockTask = createMockTask()
describe('task state management', () => {
it('should track task queue length', () => {
const queue = createManagerQueue()
queue.enqueueTask(mockTask)
// Add tasks to queue
taskQueue.value.running_queue = [
{
ui_id: 'task1',
client_id: 'test-client-id',
task_name: 'Installing pack1'
}
]
taskQueue.value.pending_queue = [
{
ui_id: 'task2',
client_id: 'test-client-id',
task_name: 'Installing pack2'
}
]
expect(queue.queueLength.value).toBe(1)
expect(queue.currentQueueLength.value).toBe(2)
expect(queue.allTasksDone.value).toBe(false)
})
it('should clear the queue when clearQueue is called', () => {
const queue = useManagerQueue()
it('should handle empty queues', () => {
const queue = createManagerQueue()
// Add some tasks
queue.enqueueTask(createMockTask())
queue.enqueueTask(createMockTask())
taskQueue.value.running_queue = []
taskQueue.value.pending_queue = []
expect(queue.queueLength.value).toBe(2)
// Clear the queue
queue.clearQueue()
expect(queue.queueLength.value).toBe(0)
expect(queue.currentQueueLength.value).toBe(0)
expect(queue.allTasksDone.value).toBe(true)
})
})
describe('server status handling', () => {
it('should update server status when receiving websocket events', async () => {
const queue = useManagerQueue()
describe('task history management', () => {
it('should track task history count', () => {
const queue = createManagerQueue()
await simulateServerStatus('in_progress')
expect(queue.statusMessage.value).toBe('in_progress')
expect(queue.allTasksDone.value).toBe(false)
})
it('should handle invalid status values gracefully', async () => {
const queue = useManagerQueue()
// Simulate an invalid status
const event = new CustomEvent('cm-queue-status', {
detail: null as any
})
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 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()
// Start the task
await simulateServerStatus('all-done')
expect(mockTask.task).toHaveBeenCalled()
// Simulate task completion
await mockTask.task.mock.results[0].value
// Simulate server cycle (in_progress -> done)
await simulateServerStatus('in_progress')
expect(mockTask.onComplete).not.toHaveBeenCalled()
await simulateServerStatus('all-done')
expect(mockTask.onComplete).toHaveBeenCalled()
})
it('should handle tasks without onComplete callback', async () => {
const queue = useManagerQueue()
const mockTask = { task: vi.fn().mockResolvedValue('result') }
queue.enqueueTask(mockTask)
// Start the task
await simulateServerStatus('all-done')
expect(mockTask.task).toHaveBeenCalled()
// Simulate task completion
await mockTask.task.mock.results[0].value
// Simulate server cycle
await simulateServerStatus('in_progress')
await simulateServerStatus('all-done')
// Should not throw errors even without onComplete
expect(queue.allTasksDone.value).toBe(true)
})
it('should process multiple tasks in sequence', async () => {
const queue = useManagerQueue()
const mockTask1 = createMockTask('result1')
const mockTask2 = createMockTask('result2')
// Add tasks to the queue
queue.enqueueTask(mockTask1)
queue.enqueueTask(mockTask2)
expect(queue.queueLength.value).toBe(2)
// Process first task
await simulateServerStatus('all-done')
expect(mockTask1.task).toHaveBeenCalled()
expect(queue.queueLength.value).toBe(1)
// Complete first task
await mockTask1.task.mock.results[0].value
await simulateServerStatus('in_progress')
await simulateServerStatus('all-done')
expect(mockTask1.onComplete).toHaveBeenCalled()
// Process second task
expect(mockTask2.task).toHaveBeenCalled()
expect(queue.queueLength.value).toBe(0)
// Complete second task
await mockTask2.task.mock.results[0].value
await simulateServerStatus('in_progress')
await simulateServerStatus('all-done')
expect(mockTask2.onComplete).toHaveBeenCalled()
// Queue should be empty and all tasks done
expect(queue.queueLength.value).toBe(0)
expect(queue.allTasksDone.value).toBe(true)
})
it('should handle task that returns rejected promise', async () => {
const queue = useManagerQueue()
const mockTask = {
task: vi.fn().mockRejectedValue(new Error('Task failed')),
onComplete: vi.fn()
taskHistory.value = {
task1: {
ui_id: 'task1',
client_id: 'test-client-id',
status: { status_str: 'success', completed: true }
},
task2: {
ui_id: 'task2',
client_id: 'test-client-id',
status: { status_str: 'success', completed: true }
}
}
queue.enqueueTask(mockTask)
expect(queue.historyCount.value).toBe(2)
})
// Start the task
await simulateServerStatus('all-done')
expect(mockTask.task).toHaveBeenCalled()
it('should filter tasks by client ID', () => {
const queue = createManagerQueue()
// Let the promise rejection happen
try {
await mockTask.task()
} catch (e) {
// Ignore the error
const mockState = {
history: {
task1: {
ui_id: 'task1',
client_id: 'test-client-id', // This client
kind: 'install',
timestamp: '2024-01-01T00:00:00Z',
result: 'success',
status: {
status_str: 'success' as const,
completed: true,
messages: []
}
},
task2: {
ui_id: 'task2',
client_id: 'other-client-id', // Different client
kind: 'install',
timestamp: '2024-01-01T00:00:00Z',
result: 'success',
status: {
status_str: 'success' as const,
completed: true,
messages: []
}
}
},
running_queue: [],
pending_queue: [],
installed_packs: {}
}
// Simulate server cycle
await simulateServerStatus('in_progress')
await simulateServerStatus('all-done')
queue.updateTaskState(mockState)
// onComplete should still be called for failed tasks
expect(mockTask.onComplete).toHaveBeenCalled()
})
it('should handle multiple multiple tasks enqueued at once while server busy', async () => {
const queue = useManagerQueue()
const mockTask1 = createMockTask()
const mockTask2 = createMockTask()
const mockTask3 = createMockTask()
// Three tasks enqueued at once
await simulateServerStatus('in_progress')
await Promise.all([
queue.enqueueTask(mockTask1),
queue.enqueueTask(mockTask2),
queue.enqueueTask(mockTask3)
])
// Task 1
await simulateServerStatus('all-done')
expect(mockTask1.task).toHaveBeenCalled()
// Verify state of onComplete callbacks
expect(mockTask1.onComplete).toHaveBeenCalled()
expect(mockTask2.onComplete).not.toHaveBeenCalled()
expect(mockTask3.onComplete).not.toHaveBeenCalled()
// Verify state of queue
expect(queue.queueLength.value).toBe(2)
expect(queue.allTasksDone.value).toBe(false)
// Task 2
await simulateServerStatus('in_progress')
await simulateServerStatus('all-done')
expect(mockTask2.task).toHaveBeenCalled()
// Verify state of onComplete callbacks
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')
// Verify state of onComplete callbacks
expect(mockTask3.task).toHaveBeenCalled()
expect(mockTask3.onComplete).toHaveBeenCalled()
// Verify state of queue
expect(queue.queueLength.value).toBe(0)
expect(queue.allTasksDone.value).toBe(true)
})
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
await simulateServerStatus('in_progress')
await simulateServerStatus('all-done')
await simulateServerStatus('in_progress')
await simulateServerStatus('all-done')
// Should not cause any errors
expect(queue.allTasksDone.value).toBe(true)
// Should only include task from this client
expect(taskHistory.value).toHaveProperty('task1')
expect(taskHistory.value).not.toHaveProperty('task2')
})
})
})

View File

@@ -4,12 +4,14 @@ import { nextTick, ref } from 'vue'
import { useComfyManagerService } from '@/services/comfyManagerService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import {
InstalledPacksResponse,
ManagerChannel,
ManagerDatabaseSource,
ManagerPackInstalled
} from '@/types/comfyManagerTypes'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
type InstalledPacksResponse =
ManagerComponents['schemas']['InstalledPacksResponse']
type ManagerChannel = ManagerComponents['schemas']['ManagerChannel']
type ManagerDatabaseSource =
ManagerComponents['schemas']['ManagerDatabaseSource']
type ManagerPackInstalled = ManagerComponents['schemas']['ManagerPackInstalled']
vi.mock('@/services/comfyManagerService', () => ({
useComfyManagerService: vi.fn()
@@ -82,8 +84,8 @@ describe('useComfyManagerStore', () => {
isLoading: ref(false),
error: ref(null),
startQueue: vi.fn().mockResolvedValue(null),
resetQueue: vi.fn().mockResolvedValue(null),
getQueueStatus: vi.fn().mockResolvedValue(null),
getTaskHistory: vi.fn().mockResolvedValue(null),
listInstalledPacks: vi.fn().mockResolvedValue({}),
getImportFailInfo: vi.fn().mockResolvedValue(null),
getImportFailInfoBulk: vi.fn().mockResolvedValue({}),
@@ -367,8 +369,8 @@ describe('useComfyManagerStore', () => {
await store.installPack.call({
id: 'test-pack',
repository: 'https://github.com/test/test-pack',
channel: ManagerChannel.DEV,
mode: ManagerDatabaseSource.CACHE,
channel: 'dev' as ManagerChannel,
mode: 'cache' as ManagerDatabaseSource,
selected_version: 'latest',
version: 'latest'
})
@@ -384,8 +386,8 @@ describe('useComfyManagerStore', () => {
await store.installPack.call({
id: 'test-pack',
repository: 'https://github.com/test/test-pack',
channel: ManagerChannel.DEV,
mode: ManagerDatabaseSource.CACHE,
channel: 'dev' as ManagerChannel,
mode: 'cache' as ManagerDatabaseSource,
selected_version: 'latest',
version: 'latest'
})
@@ -397,8 +399,8 @@ describe('useComfyManagerStore', () => {
await store.installPack.call({
id: 'another-pack',
repository: 'https://github.com/test/another-pack',
channel: ManagerChannel.DEV,
mode: ManagerDatabaseSource.CACHE,
channel: 'dev' as ManagerChannel,
mode: 'cache' as ManagerDatabaseSource,
selected_version: 'latest',
version: 'latest'
})
@@ -415,8 +417,8 @@ describe('useComfyManagerStore', () => {
await store.installPack.call({
id: 'pack-1',
repository: 'https://github.com/test/pack-1',
channel: ManagerChannel.DEV,
mode: ManagerDatabaseSource.CACHE,
channel: 'dev' as ManagerChannel,
mode: 'cache' as ManagerDatabaseSource,
selected_version: 'latest',
version: 'latest'
})
@@ -425,8 +427,8 @@ describe('useComfyManagerStore', () => {
await store.installPack.call({
id: 'pack-2',
repository: 'https://github.com/test/pack-2',
channel: ManagerChannel.DEV,
mode: ManagerDatabaseSource.CACHE,
channel: 'dev' as ManagerChannel,
mode: 'cache' as ManagerDatabaseSource,
selected_version: 'latest',
version: 'latest'
})