diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index d6790aa90..fb844b448 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -22,7 +22,6 @@ import { type ShowDialogOptions, useDialogStore } from '@/stores/dialogStore' -import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes' import ManagerProgressDialogContent from '@/workbench/extensions/manager/components/ManagerProgressDialogContent.vue' import ManagerProgressFooter from '@/workbench/extensions/manager/components/ManagerProgressFooter.vue' import ManagerProgressHeader from '@/workbench/extensions/manager/components/ManagerProgressHeader.vue' @@ -31,6 +30,7 @@ import ManagerHeader from '@/workbench/extensions/manager/components/manager/Man import NodeConflictDialogContent from '@/workbench/extensions/manager/components/manager/NodeConflictDialogContent.vue' import NodeConflictFooter from '@/workbench/extensions/manager/components/manager/NodeConflictFooter.vue' import NodeConflictHeader from '@/workbench/extensions/manager/components/manager/NodeConflictHeader.vue' +import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes' export type ConfirmationDialogType = | 'default' diff --git a/src/utils/versionUtil.ts b/src/utils/versionUtil.ts deleted file mode 100644 index 423d52b5f..000000000 --- a/src/utils/versionUtil.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { clean, satisfies } from 'semver' - -import type { - ConflictDetail, - ConflictType -} from '@/types/conflictDetectionTypes' - -/** - * Cleans a version string by removing common prefixes and normalizing format - * @param version Raw version string (e.g., "v1.2.3", "1.2.3-alpha") - * @returns Cleaned version string or original if cleaning fails - */ -export function cleanVersion(version: string): string { - return clean(version) || version -} - -/** - * Checks if a version satisfies a version range - * @param version Current version - * @param range Version range (e.g., ">=1.0.0", "^1.2.0", "1.0.0 - 2.0.0") - * @returns true if version satisfies the range - */ -export function satisfiesVersion(version: string, range: string): boolean { - try { - const cleanedVersion = cleanVersion(version) - return satisfies(cleanedVersion, range) - } catch { - return false - } -} - -/** - * Checks version compatibility and returns conflict details. - * Supports all semver ranges including >=, <=, >, <, ~, ^ operators. - * @param type Conflict type (e.g., 'comfyui_version', 'frontend_version') - * @param currentVersion Current version string - * @param supportedVersion Required version range string - * @returns ConflictDetail object if incompatible, null if compatible - */ -export function utilCheckVersionCompatibility( - type: ConflictType, - currentVersion: string, - supportedVersion: string -): ConflictDetail | null { - // If current version is unknown, assume compatible (no conflict) - if (!currentVersion || currentVersion === 'unknown') { - return null - } - - // If no version requirement specified, assume compatible (no conflict) - if (!supportedVersion || supportedVersion.trim() === '') { - return null - } - - try { - // Clean the current version using semver utilities - const cleanCurrent = cleanVersion(currentVersion) - - // Check version compatibility using semver library - const isCompatible = satisfiesVersion(cleanCurrent, supportedVersion) - - if (!isCompatible) { - return { - type, - current_value: currentVersion, - required_value: supportedVersion - } - } - - return null - } catch (error) { - console.warn( - `[VersionUtil] Failed to parse version requirement: ${supportedVersion}`, - error - ) - // On error, assume incompatible to be safe - return { - type, - current_value: currentVersion, - required_value: supportedVersion - } - } -} diff --git a/src/workbench/extensions/manager/components/ManagerProgressFooter.vue b/src/workbench/extensions/manager/components/ManagerProgressFooter.vue index 2d71d4a2c..cd51c0ceb 100644 --- a/src/workbench/extensions/manager/components/ManagerProgressFooter.vue +++ b/src/workbench/extensions/manager/components/ManagerProgressFooter.vue @@ -91,7 +91,7 @@ const dialogStore = useDialogStore() const progressDialogContent = useManagerProgressDialogStore() const comfyManagerStore = useComfyManagerStore() const settingStore = useSettingStore() -const { performConflictDetection } = useConflictDetection() +const { runFullConflictAnalysis } = useConflictDetection() // State management for restart process const isRestarting = ref(false) @@ -154,8 +154,8 @@ const handleRestart = async () => { await useWorkflowService().reloadCurrentWorkflow() - // Run conflict detection after restart completion - await performConflictDetection() + // Run conflict detection in background after restart completion + void runFullConflictAnalysis() } finally { await settingStore.set( 'Comfy.Toast.DisableReconnectingToast', diff --git a/src/workbench/extensions/manager/components/manager/NodeConflictDialogContent.vue b/src/workbench/extensions/manager/components/manager/NodeConflictDialogContent.vue index b1fa4884a..5b6c15582 100644 --- a/src/workbench/extensions/manager/components/manager/NodeConflictDialogContent.vue +++ b/src/workbench/extensions/manager/components/manager/NodeConflictDialogContent.vue @@ -168,12 +168,12 @@ import { computed, ref } from 'vue' import { useI18n } from 'vue-i18n' import ContentDivider from '@/components/common/ContentDivider.vue' +import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection' import type { ConflictDetail, ConflictDetectionResult -} from '@/types/conflictDetectionTypes' -import { getConflictMessage } from '@/utils/conflictMessageUtil' -import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection' +} from '@/workbench/extensions/manager/types/conflictDetectionTypes' +import { getConflictMessage } from '@/workbench/extensions/manager/utils/conflictMessageUtil' const { showAfterWhatsNew = false, conflictedPackages } = defineProps<{ showAfterWhatsNew?: boolean diff --git a/src/workbench/extensions/manager/components/manager/PackStatusMessage.vue b/src/workbench/extensions/manager/components/manager/PackStatusMessage.vue index eae2d565b..6f7a1ec3a 100644 --- a/src/workbench/extensions/manager/components/manager/PackStatusMessage.vue +++ b/src/workbench/extensions/manager/components/manager/PackStatusMessage.vue @@ -20,7 +20,7 @@ import Message from 'primevue/message' import { computed, inject } from 'vue' import type { components } from '@/types/comfyRegistryTypes' -import { ImportFailedKey } from '@/types/importFailedTypes' +import { ImportFailedKey } from '@/workbench/extensions/manager/types/importFailedTypes' type PackVersionStatus = components['schemas']['NodeVersionStatus'] type PackStatus = components['schemas']['NodeStatus'] diff --git a/src/workbench/extensions/manager/components/manager/PackVersionBadge.test.ts b/src/workbench/extensions/manager/components/manager/PackVersionBadge.test.ts index 0b34788ad..d57b25f1f 100644 --- a/src/workbench/extensions/manager/components/manager/PackVersionBadge.test.ts +++ b/src/workbench/extensions/manager/components/manager/PackVersionBadge.test.ts @@ -40,7 +40,9 @@ vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({ installedPacks: mockInstalledPacks, isPackInstalled: (id: string) => !!mockInstalledPacks[id as keyof typeof mockInstalledPacks], - isPackEnabled: mockIsPackEnabled + isPackEnabled: mockIsPackEnabled, + getInstalledPackVersion: (id: string) => + mockInstalledPacks[id as keyof typeof mockInstalledPacks]?.ver })) })) diff --git a/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue b/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue index 2737bed88..e5d1876e9 100644 --- a/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue +++ b/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue @@ -93,10 +93,10 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue' import VerifiedIcon from '@/components/icons/VerifiedIcon.vue' import { useComfyRegistryService } from '@/services/comfyRegistryService' import type { components } from '@/types/comfyRegistryTypes' -import { getJoinedConflictMessages } from '@/utils/conflictMessageUtil' import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes' +import { getJoinedConflictMessages } from '@/workbench/extensions/manager/utils/conflictMessageUtil' type ManagerChannel = ManagerComponents['schemas']['ManagerChannel'] type ManagerDatabaseSource = diff --git a/src/workbench/extensions/manager/components/manager/button/PackInstallButton.vue b/src/workbench/extensions/manager/components/manager/button/PackInstallButton.vue index 1d1d75526..7164544df 100644 --- a/src/workbench/extensions/manager/components/manager/button/PackInstallButton.vue +++ b/src/workbench/extensions/manager/components/manager/button/PackInstallButton.vue @@ -31,10 +31,10 @@ import { t } from '@/i18n' import { useDialogService } from '@/services/dialogService' import type { ButtonSize } from '@/types/buttonTypes' import type { components } from '@/types/comfyRegistryTypes' -import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes' -import type { ConflictDetail } from '@/types/conflictDetectionTypes' import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' +import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes' +import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes' import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes' type NodePack = components['schemas']['Node'] diff --git a/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue b/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue index f3e8175a4..8696e837d 100644 --- a/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue +++ b/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue @@ -65,8 +65,6 @@ import { computed, provide, ref } from 'vue' import { useI18n } from 'vue-i18n' import type { components } from '@/types/comfyRegistryTypes' -import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes' -import { ImportFailedKey } from '@/types/importFailedTypes' import PackStatusMessage from '@/workbench/extensions/manager/components/manager/PackStatusMessage.vue' import PackVersionBadge from '@/workbench/extensions/manager/components/manager/PackVersionBadge.vue' import PackEnableToggle from '@/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue' @@ -78,6 +76,8 @@ import { useImportFailedDetection } from '@/workbench/extensions/manager/composa import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore' import { IsInstallingKey } from '@/workbench/extensions/manager/types/comfyManagerTypes' +import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes' +import { ImportFailedKey } from '@/workbench/extensions/manager/types/importFailedTypes' interface InfoItem { key: string diff --git a/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue b/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue index db1d976c3..fda685a50 100644 --- a/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue +++ b/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelHeader.vue @@ -46,13 +46,13 @@ import { computed, inject, ref, watch } from 'vue' import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue' import type { components } from '@/types/comfyRegistryTypes' -import type { ConflictDetail } from '@/types/conflictDetectionTypes' -import { ImportFailedKey } from '@/types/importFailedTypes' import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue' import PackUninstallButton from '@/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue' import PackIcon from '@/workbench/extensions/manager/components/manager/packIcon/PackIcon.vue' import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' +import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes' +import { ImportFailedKey } from '@/workbench/extensions/manager/types/importFailedTypes' const { nodePacks, hasConflict } = defineProps<{ nodePacks: components['schemas']['Node'][] diff --git a/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue b/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue index 97ce03337..8e5b15d78 100644 --- a/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue +++ b/src/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue @@ -59,8 +59,6 @@ import { computed, onUnmounted, provide, toRef } from 'vue' import { useComfyRegistryStore } from '@/stores/comfyRegistryStore' import type { components } from '@/types/comfyRegistryTypes' -import type { ConflictDetail } from '@/types/conflictDetectionTypes' -import { ImportFailedKey } from '@/types/importFailedTypes' import PackStatusMessage from '@/workbench/extensions/manager/components/manager/PackStatusMessage.vue' import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue' import PackUninstallButton from '@/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue' @@ -70,6 +68,8 @@ import PackIconStacked from '@/workbench/extensions/manager/components/manager/p import { usePacksSelection } from '@/workbench/extensions/manager/composables/nodePack/usePacksSelection' import { usePacksStatus } from '@/workbench/extensions/manager/composables/nodePack/usePacksStatus' import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection' +import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes' +import { ImportFailedKey } from '@/workbench/extensions/manager/types/importFailedTypes' const { nodePacks } = defineProps<{ nodePacks: components['schemas']['Node'][] diff --git a/src/workbench/extensions/manager/components/manager/infoPanel/InfoTabs.vue b/src/workbench/extensions/manager/components/manager/infoPanel/InfoTabs.vue index a66ff6eca..d07f5e267 100644 --- a/src/workbench/extensions/manager/components/manager/infoPanel/InfoTabs.vue +++ b/src/workbench/extensions/manager/components/manager/infoPanel/InfoTabs.vue @@ -46,11 +46,11 @@ import Tabs from 'primevue/tabs' import { computed, inject, ref, watchEffect } from 'vue' import type { components } from '@/types/comfyRegistryTypes' -import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes' -import { ImportFailedKey } from '@/types/importFailedTypes' import DescriptionTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/DescriptionTabPanel.vue' import NodesTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/NodesTabPanel.vue' import WarningTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/WarningTabPanel.vue' +import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes' +import { ImportFailedKey } from '@/workbench/extensions/manager/types/importFailedTypes' const { nodePack, hasCompatibilityIssues, conflictResult } = defineProps<{ nodePack: components['schemas']['Node'] diff --git a/src/workbench/extensions/manager/components/manager/infoPanel/tabs/WarningTabPanel.vue b/src/workbench/extensions/manager/components/manager/infoPanel/tabs/WarningTabPanel.vue index 7868855e6..a6fbc2c33 100644 --- a/src/workbench/extensions/manager/components/manager/infoPanel/tabs/WarningTabPanel.vue +++ b/src/workbench/extensions/manager/components/manager/infoPanel/tabs/WarningTabPanel.vue @@ -29,9 +29,9 @@ import { computed } from 'vue' import { t } from '@/i18n' import type { components } from '@/types/comfyRegistryTypes' -import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes' -import { getConflictMessage } from '@/utils/conflictMessageUtil' import { useImportFailedDetection } from '@/workbench/extensions/manager/composables/useImportFailedDetection' +import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes' +import { getConflictMessage } from '@/workbench/extensions/manager/utils/conflictMessageUtil' const { nodePack, conflictResult } = defineProps<{ nodePack: components['schemas']['Node'] diff --git a/src/workbench/extensions/manager/components/manager/packCard/PackCardFooter.vue b/src/workbench/extensions/manager/components/manager/packCard/PackCardFooter.vue index 295ecefd6..57e6ccb2c 100644 --- a/src/workbench/extensions/manager/components/manager/packCard/PackCardFooter.vue +++ b/src/workbench/extensions/manager/components/manager/packCard/PackCardFooter.vue @@ -26,12 +26,12 @@ import { computed, inject } from 'vue' import { useI18n } from 'vue-i18n' import type { components } from '@/types/comfyRegistryTypes' -import type { ConflictDetail } from '@/types/conflictDetectionTypes' import PackEnableToggle from '@/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue' import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue' import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { IsInstallingKey } from '@/workbench/extensions/manager/types/comfyManagerTypes' +import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes' const { nodePack } = defineProps<{ nodePack: components['schemas']['Node'] diff --git a/src/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus.ts b/src/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus.ts index 80369d852..5aec2700d 100644 --- a/src/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus.ts +++ b/src/workbench/extensions/manager/composables/nodePack/usePackUpdateStatus.ts @@ -20,7 +20,12 @@ export const usePackUpdateStatus = ( ) const isUpdateAvailable = computed(() => { - if (!isInstalled.value || isNightlyPack.value || !latestVersion.value) { + if ( + !isInstalled.value || + isNightlyPack.value || + !latestVersion.value || + !installedVersion.value + ) { return false } return compare(latestVersion.value, installedVersion.value) > 0 diff --git a/src/workbench/extensions/manager/composables/nodePack/useUpdateAvailableNodes.ts b/src/workbench/extensions/manager/composables/nodePack/useUpdateAvailableNodes.ts index eb3d40712..c18fcdf61 100644 --- a/src/workbench/extensions/manager/composables/nodePack/useUpdateAvailableNodes.ts +++ b/src/workbench/extensions/manager/composables/nodePack/useUpdateAvailableNodes.ts @@ -27,7 +27,7 @@ export const useUpdateAvailableNodes = () => { const isNightlyPack = !!installedVersion && !valid(installedVersion) - if (isNightlyPack || !latestVersion) { + if (isNightlyPack || !latestVersion || !installedVersion) { return false } diff --git a/src/workbench/extensions/manager/composables/useConflictDetection.ts b/src/workbench/extensions/manager/composables/useConflictDetection.ts index 216d4df95..af8e35e12 100644 --- a/src/workbench/extensions/manager/composables/useConflictDetection.ts +++ b/src/workbench/extensions/manager/composables/useConflictDetection.ts @@ -1,33 +1,41 @@ import { until } from '@vueuse/core' -import { uniqBy } from 'es-toolkit/compat' +import { find } from 'es-toolkit/compat' import { computed, getCurrentInstance, onUnmounted, readonly, ref } from 'vue' -import config from '@/config' import { useComfyRegistryService } from '@/services/comfyRegistryService' import { useSystemStatsStore } from '@/stores/systemStatsStore' -import type { SystemStats } from '@/types' import type { components } from '@/types/comfyRegistryTypes' -import type { - ConflictDetail, - ConflictDetectionResponse, - ConflictDetectionResult, - ConflictDetectionSummary, - ConflictType, - Node, - NodePackRequirements, - SystemEnvironment -} from '@/types/conflictDetectionTypes' -import { normalizePackId } from '@/utils/packUtils' -import { - cleanVersion, - satisfiesVersion, - utilCheckVersionCompatibility -} from '@/utils/versionUtil' import { useInstalledPacks } from '@/workbench/extensions/manager/composables/nodePack/useInstalledPacks' import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment' import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore' +import type { + RegistryAccelerator, + RegistryOS +} from '@/workbench/extensions/manager/types/compatibility.types' +import type { + ConflictDetail, + ConflictDetectionResponse, + ConflictDetectionResult, + Node, + NodeRequirements, + SystemEnvironment +} from '@/workbench/extensions/manager/types/conflictDetectionTypes' +import { + consolidateConflictsByPackage, + createBannedConflict, + createPendingConflict +} from '@/workbench/extensions/manager/utils/conflictUtils' +import { + checkAcceleratorCompatibility, + checkOSCompatibility, + normalizeOSList +} from '@/workbench/extensions/manager/utils/systemCompatibility' +import { + checkVersionCompatibility, + getFrontendVersion +} from '@/workbench/extensions/manager/utils/versionUtil' /** * Composable for conflict detection system. @@ -52,7 +60,6 @@ export function useConflictDetection() { const detectionResults = ref([]) // Store merged conflicts separately for testing const storedMergedConflicts = ref([]) - const detectionSummary = ref(null) // Registry API request cancellation const abortController = ref(null) @@ -76,90 +83,32 @@ export function useConflictDetection() { * Continues with default values even if errors occur. * @returns Promise that resolves to system environment information */ - async function detectSystemEnvironment(): Promise { + async function collectSystemEnvironment(): Promise { try { // Get system stats from store (primary source of system information) - const systemStatsStore = useSystemStatsStore() // Wait for systemStats to be initialized if not already - await until(systemStatsStore.isInitialized) + const { systemStats, isInitialized: systemStatsInitialized } = + useSystemStatsStore() + await until(systemStatsInitialized) - // Fetch version information from backend (with error resilience) - const [frontendVersion] = await Promise.allSettled([ - fetchFrontendVersion() - ]) - - // Extract system information from system stats - const systemStats = systemStatsStore.systemStats - const comfyuiVersion = systemStats?.system?.comfyui_version || 'unknown' - - // Use system stats for OS detection (more accurate than browser detection) - const systemOS = systemStats?.system?.os || 'unknown' - - // Extract architecture from system stats device information - const architecture = extractArchitectureFromSystemStats(systemStats) - - // Detect GPU/accelerator information from system stats - const acceleratorInfo = extractAcceleratorInfo(systemStats) - - // Enhanced OS detection using multiple sources - const detectedOS = detectOSFromSystemStats(systemOS, systemStats) + const frontendVersion = getFrontendVersion() const environment: SystemEnvironment = { - // Version information (use 'unknown' on failure) - comfyui_version: comfyuiVersion, - frontend_version: - frontendVersion.status === 'fulfilled' - ? frontendVersion.value - : 'unknown', - - // Platform information (from system stats) - os: detectedOS, - platform_details: systemOS, - architecture: architecture, - - // GPU/accelerator information - available_accelerators: acceleratorInfo.available, - primary_accelerator: acceleratorInfo.primary, - gpu_memory_mb: acceleratorInfo.memory_mb, - - // Runtime information - node_env: import.meta.env.MODE as 'development' | 'production', - user_agent: navigator.userAgent + comfyui_version: systemStats?.system.comfyui_version ?? '', + frontend_version: frontendVersion, + os: systemStats?.system.os ?? '', + accelerator: systemStats?.devices?.[0]?.type ?? '' } systemEnvironment.value = environment - console.debug( - '[ConflictDetection] System environment detection completed:', - environment - ) return environment } catch (error) { - console.warn( - '[ConflictDetection] Error during system environment detection:', - error - ) - - // Try to get frontend version even in fallback mode - let frontendVersion = 'unknown' - try { - frontendVersion = await fetchFrontendVersion() - } catch { - frontendVersion = 'unknown' - } - - // Provide basic environment information even on error const fallbackEnvironment: SystemEnvironment = { - comfyui_version: 'unknown', - frontend_version: frontendVersion, - os: detectOSFromSystemStats(navigator.platform), - platform_details: navigator.platform, - architecture: getArchitecture(), - available_accelerators: ['CPU'], - primary_accelerator: 'CPU', - node_env: import.meta.env.MODE as 'development' | 'production', - user_agent: navigator.userAgent + comfyui_version: undefined, + frontend_version: undefined, + os: undefined, + accelerator: undefined } - systemEnvironment.value = fallbackEnvironment return fallbackEnvironment } @@ -179,7 +128,7 @@ export function useConflictDetection() { * * @returns Promise that resolves to array of node pack requirements */ - async function fetchPackageRequirements(): Promise { + async function buildNodeRequirements(): Promise { try { // Step 1: Use installed packs composable instead of direct API calls await startFetchInstalled() // Ensure data is loaded @@ -220,7 +169,7 @@ export function useConflictDetection() { abortController.value?.signal ) - if (bulkResponse && bulkResponse.node_versions) { + if (bulkResponse && bulkResponse.node_versions?.length > 0) { // Process bulk response bulkResponse.node_versions.forEach((result) => { if (result.status === 'success' && result.node_version) { @@ -245,35 +194,35 @@ export function useConflictDetection() { } // Step 5: Combine local installation data with Registry version data - const requirements: NodePackRequirements[] = [] + const requirements: NodeRequirements[] = [] // IMPORTANT: Use installedPacksWithVersions to check ALL installed packages // not just the ones that exist in Registry (installedPacks) - for (const installedPack of installedPacksWithVersions.value) { - const packageId = installedPack.id - const versionData = versionDataMap.get(packageId) - const installedVersion = installedPack.version || 'unknown' - - // Check if package is enabled using store method - const isEnabled = managerStore.isPackEnabled(packageId) + for (const installedPackVersion of installedPacksWithVersions.value) { + const versionData = versionDataMap.get(installedPackVersion.id) + const isEnabled = managerStore.isPackEnabled(installedPackVersion.id) // Find the pack info from Registry if available - const packInfo = installedPacks.value.find((p) => p.id === packageId) + const packInfo = find(installedPacks.value, { + id: installedPackVersion.id + }) if (versionData) { // Combine local installation data with version-specific Registry data - const requirement: NodePackRequirements = { + const requirement: NodeRequirements = { // Basic package info - id: packageId, - name: packInfo?.name || packageId, - installed_version: installedVersion, + id: installedPackVersion.id, + name: packInfo?.name || installedPackVersion.id, + installed_version: installedPackVersion.version, is_enabled: isEnabled, // Version-specific compatibility data supported_comfyui_version: versionData.supported_comfyui_version, supported_comfyui_frontend_version: versionData.supported_comfyui_frontend_version, - supported_os: normalizeOSValues(versionData.supported_os), + supported_os: normalizeOSList( + versionData.supported_os + ) as Node['supported_os'], supported_accelerators: versionData.supported_accelerators, // Status information @@ -285,14 +234,14 @@ export function useConflictDetection() { requirements.push(requirement) } else { console.warn( - `[ConflictDetection] No Registry data found for ${packageId}, using fallback` + `[ConflictDetection] No Registry data found for ${installedPackVersion.id}, using fallback` ) // Create fallback requirement without Registry data - const fallbackRequirement: NodePackRequirements = { - id: packageId, - name: packInfo?.name || packageId, - installed_version: installedVersion, + const fallbackRequirement: NodeRequirements = { + id: installedPackVersion.id, + name: packInfo?.name || installedPackVersion.id, + installed_version: installedPackVersion.version, is_enabled: isEnabled, is_banned: false, is_pending: false @@ -319,63 +268,50 @@ export function useConflictDetection() { * @param sysEnv Current system environment * @returns Conflict detection result for the package */ - function detectPackageConflicts( - packageReq: NodePackRequirements, - sysEnv: SystemEnvironment + function analyzePackageConflicts( + packageReq: NodeRequirements, + systemEnvInfo: SystemEnvironment ): ConflictDetectionResult { const conflicts: ConflictDetail[] = [] - // Helper function to check if a value indicates "compatible with all" - const isCompatibleWithAll = (value: any): boolean => { - if (value === null || value === undefined) return true - if (typeof value === 'string' && value.trim() === '') return true - if (Array.isArray(value) && value.length === 0) return true - return false - } - // 1. ComfyUI version conflict check - if (!isCompatibleWithAll(packageReq.supported_comfyui_version)) { - const versionConflict = checkVersionConflict( - 'comfyui_version', - sysEnv.comfyui_version, - packageReq.supported_comfyui_version! - ) - if (versionConflict) conflicts.push(versionConflict) - } + const versionConflict = checkVersionCompatibility( + 'comfyui_version', + systemEnvInfo.comfyui_version, + packageReq.supported_comfyui_version + ) + if (versionConflict) conflicts.push(versionConflict) // 2. Frontend version conflict check - if (!isCompatibleWithAll(packageReq.supported_comfyui_frontend_version)) { - const versionConflict = checkVersionConflict( - 'frontend_version', - sysEnv.frontend_version, - packageReq.supported_comfyui_frontend_version! - ) - if (versionConflict) conflicts.push(versionConflict) - } + const frontendConflict = checkVersionCompatibility( + 'frontend_version', + systemEnvInfo.frontend_version, + packageReq.supported_comfyui_frontend_version + ) + if (frontendConflict) conflicts.push(frontendConflict) // 3. OS compatibility check - if (!isCompatibleWithAll(packageReq.supported_os)) { - const osConflict = checkOSConflict(packageReq.supported_os!, sysEnv.os) - if (osConflict) conflicts.push(osConflict) - } + const osConflict = checkOSCompatibility( + packageReq.supported_os as RegistryOS[] | undefined, + systemEnvInfo.os + ) + if (osConflict) conflicts.push(osConflict) // 4. Accelerator compatibility check - if (!isCompatibleWithAll(packageReq.supported_accelerators)) { - const acceleratorConflict = checkAcceleratorConflict( - packageReq.supported_accelerators!, - sysEnv.available_accelerators - ) - if (acceleratorConflict) conflicts.push(acceleratorConflict) - } + const acceleratorConflict = checkAcceleratorCompatibility( + packageReq.supported_accelerators as RegistryAccelerator[] | undefined, + systemEnvInfo.accelerator + ) + if (acceleratorConflict) conflicts.push(acceleratorConflict) // 5. Banned package check using shared logic - const bannedConflict = checkBannedStatus(packageReq.is_banned) + const bannedConflict = createBannedConflict(packageReq.is_banned) if (bannedConflict) { conflicts.push(bannedConflict) } // 6. Registry data availability check using shared logic - const pendingConflict = checkPendingStatus(packageReq.is_pending) + const pendingConflict = createPendingConflict(packageReq.is_pending) if (pendingConflict) { conflicts.push(pendingConflict) } @@ -499,33 +435,30 @@ export function useConflictDetection() { * Performs complete conflict detection. * @returns Promise that resolves to conflict detection response */ - async function performConflictDetection(): Promise { + async function runFullConflictAnalysis(): Promise { if (isDetecting.value) { - console.debug('[ConflictDetection] Already detecting, skipping') return { success: false, error_message: 'Already detecting conflicts', - summary: detectionSummary.value!, results: detectionResults.value } } isDetecting.value = true detectionError.value = null - const startTime = Date.now() try { // 1. Collect system environment information - const sysEnv = await detectSystemEnvironment() + const systemEnvInfo = await collectSystemEnvironment() - // 2. Collect package requirement information - const packageRequirements = await fetchPackageRequirements() + // 2. Collect installed node requirement information + const installedNodeRequirements = await buildNodeRequirements() // 3. Detect conflicts for each package (parallel processing) - const conflictDetectionTasks = packageRequirements.map( + const conflictDetectionTasks = installedNodeRequirements.map( async (packageReq) => { try { - return detectPackageConflicts(packageReq, sysEnv) + return analyzePackageConflicts(packageReq, systemEnvInfo) } catch (error) { console.warn( `[ConflictDetection] Failed to detect conflicts for package ${packageReq.name}:`, @@ -549,19 +482,10 @@ export function useConflictDetection() { // 5. Combine all results const allResults = [...packageResults, ...importFailResults] - // 6. Generate summary information - const summary = generateSummary(allResults, Date.now() - startTime) - - // 7. Update state + // 6. Update state detectionResults.value = allResults - detectionSummary.value = summary lastDetectionTime.value = new Date().toISOString() - console.debug( - '[ConflictDetection] Conflict detection completed:', - summary - ) - // Store conflict results for later UI display // Dialog will be shown based on specific events, not on app mount if (allResults.some((result) => result.has_conflict)) { @@ -570,7 +494,7 @@ export function useConflictDetection() { ) // Merge conflicts for packages with the same name - const mergedConflicts = mergeConflictsByPackageName(conflictedResults) + const mergedConflicts = consolidateConflictsByPackage(conflictedResults) console.debug( '[ConflictDetection] Conflicts detected (stored for UI):', @@ -581,19 +505,14 @@ export function useConflictDetection() { conflictStore.setConflictedPackages(mergedConflicts) // Also update local state for backward compatibility - detectionResults.value.splice( - 0, - detectionResults.value.length, - ...mergedConflicts - ) + detectionResults.value = [...mergedConflicts] storedMergedConflicts.value = [...mergedConflicts] // Use merged conflicts in response as well const response: ConflictDetectionResponse = { success: true, - summary, results: mergedConflicts, - detected_system_environment: sysEnv + detected_system_environment: systemEnvInfo } return response } else { @@ -604,9 +523,8 @@ export function useConflictDetection() { const response: ConflictDetectionResponse = { success: true, - summary, results: allResults, - detected_system_environment: sysEnv + detected_system_environment: systemEnvInfo } return response @@ -621,7 +539,6 @@ export function useConflictDetection() { return { success: false, error_message: detectionError.value, - summary: detectionSummary.value || getEmptySummary(), results: [] } } finally { @@ -647,15 +564,12 @@ export function useConflictDetection() { const managerState = useManagerState() if (!managerState.isNewManagerUI.value) { - console.debug( - '[ConflictDetection] Manager is not new Manager, skipping conflict detection' - ) return } // Manager is new Manager, perform conflict detection // The useInstalledPacks will handle fetching installed list if needed - await performConflictDetection() + await runFullConflictAnalysis() } catch (error) { console.warn( '[ConflictDetection] Error during initialization (ignored):', @@ -688,16 +602,9 @@ export function useConflictDetection() { * Check if conflicts should trigger modal display after "What's New" dismissal */ async function shouldShowConflictModalAfterUpdate(): Promise { - console.debug( - '[ConflictDetection] Checking if conflict modal should show after update...' - ) - // Ensure conflict detection has run if (detectionResults.value.length === 0) { - console.debug( - '[ConflictDetection] No detection results, running conflict detection...' - ) - await performConflictDetection() + await runFullConflictAnalysis() } // Check if this is a version update scenario @@ -706,12 +613,6 @@ export function useConflictDetection() { const hasActualConflicts = hasConflicts.value const canShowModal = acknowledgment.shouldShowConflictModal.value - console.debug('[ConflictDetection] Modal check:', { - hasConflicts: hasActualConflicts, - canShowModal: canShowModal, - conflictedPackagesCount: conflictedPackages.value.length - }) - return hasActualConflicts && canShowModal } @@ -722,97 +623,49 @@ export function useConflictDetection() { function checkNodeCompatibility( node: Node | components['schemas']['NodeVersion'] ) { - const systemStatsStore = useSystemStatsStore() - const systemStats = systemStatsStore.systemStats - if (!systemStats) return { hasConflict: false, conflicts: [] } - const conflicts: ConflictDetail[] = [] - // Check OS compatibility using centralized function - // 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(supportedOS, currentOS) - if (osConflict) { - conflicts.push(osConflict) - } + // Check OS compatibility + const osConflict = checkOSCompatibility( + normalizeOSList(node.supported_os), + systemEnvironment.value?.os + ) + if (osConflict) { + conflicts.push(osConflict) } - // Check accelerator compatibility using centralized function - // 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'] = [] - - acceleratorInfo.available?.forEach((accel) => { - if (accel === 'CUDA') availableAccelerators.push('CUDA') - if (accel === 'Metal') availableAccelerators.push('Metal') - if (accel === 'CPU') availableAccelerators.push('CPU') - }) - - const acceleratorConflict = checkAcceleratorConflict( - supportedAccelerators, - availableAccelerators - ) - if (acceleratorConflict) { - conflicts.push(acceleratorConflict) - } + // Check Accelerator compatibility + const acceleratorConflict = checkAcceleratorCompatibility( + node.supported_accelerators as RegistryAccelerator[], + systemEnvironment.value?.accelerator + ) + if (acceleratorConflict) { + conflicts.push(acceleratorConflict) } // Check ComfyUI version compatibility - // 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, - comfyuiVersionRequirement - ) - if (versionConflict) { - conflicts.push(versionConflict) - } - } + const comfyUIVersionConflict = checkVersionCompatibility( + 'comfyui_version', + systemEnvironment.value?.comfyui_version, + node.supported_comfyui_version + ) + if (comfyUIVersionConflict) { + conflicts.push(comfyUIVersionConflict) } // Check ComfyUI Frontend version compatibility - // 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, - frontendVersionRequirement - ) - if (versionConflict) { - conflicts.push(versionConflict) - } - } + const currentFrontendVersion = getFrontendVersion() + const frontendVersionConflict = checkVersionCompatibility( + 'frontend_version', + currentFrontendVersion, + node.supported_comfyui_frontend_version + ) + if (frontendVersionConflict) { + conflicts.push(frontendVersionConflict) } // Check banned package status using shared logic - const bannedConflict = checkBannedStatus( + const bannedConflict = createBannedConflict( node.status === 'NodeStatusBanned' || node.status === 'NodeVersionStatusBanned' ) @@ -821,7 +674,7 @@ export function useConflictDetection() { } // Check pending status using shared logic - const pendingConflict = checkPendingStatus( + const pendingConflict = createPendingConflict( node.status === 'NodeVersionStatusPending' ) if (pendingConflict) { @@ -841,7 +694,6 @@ export function useConflictDetection() { detectionError: readonly(detectionError), systemEnvironment: readonly(systemEnvironment), detectionResults: readonly(detectionResults), - detectionSummary: readonly(detectionSummary), // Computed hasConflicts, @@ -850,8 +702,8 @@ export function useConflictDetection() { securityPendingPackages, // Methods - performConflictDetection, - detectSystemEnvironment, + runFullConflictAnalysis, + collectSystemEnvironment, initializeConflictDetection, cancelRequests, shouldShowConflictModalAfterUpdate, @@ -860,515 +712,3 @@ export function useConflictDetection() { checkNodeCompatibility } } - -// Helper Functions Implementation - -/** - * Merges conflict results for packages with the same name. - * Combines all conflicts from different detection sources (registry, python, extension) - * into a single result per package name. - * @param conflicts Array of conflict detection results - * @returns Array of merged conflict detection results - */ -function mergeConflictsByPackageName( - conflicts: ConflictDetectionResult[] -): ConflictDetectionResult[] { - const mergedMap = new Map() - - conflicts.forEach((conflict) => { - // Normalize package name by removing version suffix (@1_0_3) for consistent merging - const normalizedPackageName = normalizePackId(conflict.package_name) - - if (mergedMap.has(normalizedPackageName)) { - // Package already exists, merge conflicts - const existing = mergedMap.get(normalizedPackageName)! - - // Combine all conflicts, avoiding duplicates using es-toolkit uniqBy for O(n) performance - const allConflicts = [...existing.conflicts, ...conflict.conflicts] - const uniqueConflicts = uniqBy( - allConflicts, - (conflict) => - `${conflict.type}|${conflict.current_value}|${conflict.required_value}` - ) - - // Update the existing entry with normalized package name - mergedMap.set(normalizedPackageName, { - ...existing, - package_name: normalizedPackageName, - conflicts: uniqueConflicts, - has_conflict: uniqueConflicts.length > 0, - is_compatible: uniqueConflicts.length === 0 - }) - } else { - // New package, add with normalized package name - mergedMap.set(normalizedPackageName, { - ...conflict, - package_name: normalizedPackageName - }) - } - }) - - return Array.from(mergedMap.values()) -} - -/** - * Fetches frontend version from config. - * @returns Promise that resolves to frontend version string - */ -async function fetchFrontendVersion(): Promise { - try { - // Get frontend version from vite build-time constant or fallback to config - return config.app_version || import.meta.env.VITE_APP_VERSION || 'unknown' - } catch { - return 'unknown' - } -} - -/** - * Detects system architecture from user agent. - * Note: Browser architecture detection has limitations and may not be 100% accurate. - * @returns Architecture string - */ -function getArchitecture(): string { - const ua = navigator.userAgent.toLowerCase() - if (ua.includes('arm64') || ua.includes('aarch64')) return 'arm64' - if (ua.includes('arm')) return 'arm' - if (ua.includes('x86_64') || ua.includes('x64')) return 'x64' - if (ua.includes('x86')) return 'x86' - return 'unknown' -} - -/** - * Normalizes OS values from Registry API to match our SupportedOS type. - * Registry Admin guide specifies: Windows, macOS, Linux - * @param osValues OS values from Registry API - * @returns Normalized OS values - */ -function normalizeOSValues( - osValues: string[] | undefined -): Node['supported_os'] { - if (!osValues || osValues.length === 0) { - return [] - } - - return osValues.map((os) => { - // Map to standard Registry values (case-sensitive) - if (os === 'Windows' || os.toLowerCase().includes('win')) { - return 'Windows' - } - if (os === 'macOS' || os.toLowerCase().includes('mac') || os === 'darwin') { - return 'macOS' - } - if (os === 'Linux' || os.toLowerCase().includes('linux')) { - return 'Linux' - } - if (os.toLowerCase() === 'any') { - return 'any' - } - - // Return as-is if it matches standard format - return os - }) -} - -/** - * Detects operating system from system stats OS string and additional system information. - * @param systemOS OS string from system stats API - * @param systemStats Full system stats object for additional context - * @returns Operating system type - */ -function detectOSFromSystemStats( - systemOS: string, - systemStats?: SystemStats | null -): string { - const os = systemOS.toLowerCase() - - // Handle specific OS strings (return Registry standard format) - if (os.includes('darwin') || os.includes('mac')) return 'macOS' - if (os.includes('linux')) return 'Linux' - if (os.includes('win') || os === 'nt') return 'Windows' - - // Handle Python's os.name values - if (os === 'posix') { - // posix could be macOS or Linux, need additional detection - - // Method 1: Check for MPS device (Metal Performance Shaders = macOS) - if (systemStats?.devices) { - const hasMpsDevice = systemStats.devices.some( - (device) => device.type === 'mps' - ) - if (hasMpsDevice) { - return 'macOS' // Registry standard format - } - } - - // Method 2: Check user agent as fallback - const userAgent = navigator.userAgent.toLowerCase() - if (userAgent.includes('mac')) return 'macOS' - if (userAgent.includes('linux')) return 'Linux' - - // Default to 'any' if we can't determine - return 'any' - } - - return 'any' -} - -/** - * Extracts architecture information from system stats. - * @param systemStats System stats data from API - * @returns Architecture string - */ -function extractArchitectureFromSystemStats( - systemStats: SystemStats | null -): string { - try { - if (systemStats?.devices && systemStats.devices.length > 0) { - // Check if we have MPS device (indicates Apple Silicon) - const hasMpsDevice = systemStats.devices.some( - (device) => device.type === 'mps' - ) - - if (hasMpsDevice) { - // MPS is only available on Apple Silicon Macs - return 'arm64' - } - - // Check device names for architecture hints (fallback) - for (const device of systemStats.devices) { - if (!device?.name || typeof device.name !== 'string') { - continue - } - - const deviceName = device.name.toLowerCase() - - // Apple Silicon detection - if ( - deviceName.includes('apple m1') || - deviceName.includes('apple m2') || - deviceName.includes('apple m3') || - deviceName.includes('apple m4') - ) { - return 'arm64' - } - - // Intel/AMD detection - if ( - deviceName.includes('intel') || - deviceName.includes('amd') || - deviceName.includes('nvidia') || - deviceName.includes('geforce') || - deviceName.includes('radeon') - ) { - return 'x64' - } - } - } - - // Fallback to basic User-Agent detection if system stats don't provide clear info - return getArchitecture() - } catch (error) { - console.warn( - '[ConflictDetection] Failed to extract architecture from system stats:', - error - ) - return getArchitecture() - } -} - -/** - * Extracts accelerator information from system stats. - * @param systemStats System stats data from store - * @returns Accelerator information object - */ -function extractAcceleratorInfo(systemStats: SystemStats | null): { - available: Node['supported_accelerators'] - primary: string - memory_mb?: number -} { - try { - if (systemStats?.devices && systemStats.devices.length > 0) { - const accelerators = new Set() - let primaryDevice: string = 'CPU' - let totalMemory = 0 - let maxDevicePriority = 0 - - // Device type priority (higher = better) - const getDevicePriority = (type: string): number => { - switch (type.toLowerCase()) { - case 'cuda': - return 5 - case 'mps': - return 4 - case 'rocm': - return 3 - case 'xpu': - return 2 // Intel GPU - case 'npu': - return 1 // Neural Processing Unit - case 'mlu': - return 1 // Cambricon MLU - case 'cpu': - return 0 - default: - return 0 - } - } - - // Process all devices - for (const device of systemStats.devices) { - const deviceType = device.type.toLowerCase() - const priority = getDevicePriority(deviceType) - - // Map device type to SupportedAccelerator (Registry standard format) - let acceleratorType: string = 'CPU' - if (deviceType === 'cuda') { - acceleratorType = 'CUDA' - } else if (deviceType === 'mps') { - acceleratorType = 'Metal' // MPS = Metal Performance Shaders - } else if (deviceType === 'rocm') { - acceleratorType = 'ROCm' - } - - accelerators.add(acceleratorType) - - // Update primary device if this one has higher priority - if (priority > maxDevicePriority) { - primaryDevice = acceleratorType - maxDevicePriority = priority - } - - // Accumulate memory from all devices - if (device.vram_total) { - totalMemory += device.vram_total - } - } - - accelerators.add('CPU') // CPU is always available - - return { - available: Array.from(accelerators), - primary: primaryDevice, - memory_mb: - totalMemory > 0 ? Math.round(totalMemory / 1024 / 1024) : undefined - } - } - } catch (error) { - console.warn( - '[ConflictDetection] Failed to extract GPU information:', - error - ) - } - - // Default values - return { - available: ['CPU'], - primary: 'CPU', - memory_mb: undefined - } -} - -/** - * Unified version conflict check using Registry API version strings. - * Uses shared versionUtil functions for consistent version handling. - * @param type Type of version being checked - * @param currentVersion Current version string - * @param supportedVersion Supported version from Registry - * @returns Conflict detail if conflict exists, null otherwise - */ -function checkVersionConflict( - type: ConflictType, - currentVersion: string, - supportedVersion: string -): ConflictDetail | null { - // If current version is unknown, assume compatible (no conflict) - if (currentVersion === 'unknown') { - return null - } - - // If Registry doesn't specify version requirements, assume compatible - if (!supportedVersion || supportedVersion.trim() === '') { - return null - } - - try { - // Clean the current version using shared utility - const cleanCurrent = cleanVersion(currentVersion) - - // Check version compatibility using shared utility - const isCompatible = satisfiesVersion(cleanCurrent, supportedVersion) - - if (!isCompatible) { - return { - type, - current_value: currentVersion, - required_value: supportedVersion - } - } - - return null - } catch (error) { - console.warn( - `[ConflictDetection] Failed to parse version requirement: ${supportedVersion}`, - error - ) - return { - type, - current_value: currentVersion, - required_value: supportedVersion - } - } -} - -/** - * Checks for OS compatibility conflicts. - */ -function checkOSConflict( - supportedOS: Node['supported_os'], - currentOS: string -): ConflictDetail | null { - if (supportedOS?.includes('any') || supportedOS?.includes(currentOS)) { - return null - } - - return { - type: 'os', - current_value: currentOS, - required_value: supportedOS ? supportedOS?.join(', ') : '' - } -} - -/** - * Checks for accelerator compatibility conflicts. - */ -function checkAcceleratorConflict( - supportedAccelerators: Node['supported_accelerators'], - availableAccelerators: Node['supported_accelerators'] -): ConflictDetail | null { - if ( - supportedAccelerators?.includes('any') || - supportedAccelerators?.some((acc) => availableAccelerators?.includes(acc)) - ) { - return null - } - - return { - type: 'accelerator', - current_value: availableAccelerators - ? availableAccelerators.join(', ') - : '', - required_value: supportedAccelerators - ? supportedAccelerators.join(', ') - : '' - } -} - -/** - * Checks for banned package status conflicts. - */ -function checkBannedStatus(isBanned?: boolean): ConflictDetail | null { - if (isBanned === true) { - return { - type: 'banned', - current_value: 'installed', - required_value: 'not_banned' - } - } - return null -} - -/** - * Checks for pending package status conflicts. - */ -function checkPendingStatus(isPending?: boolean): ConflictDetail | null { - if (isPending === true) { - return { - type: 'pending', - current_value: 'installed', - required_value: 'not_pending' - } - } - return null -} - -/** - * Generates summary of conflict detection results. - */ -function generateSummary( - results: ConflictDetectionResult[], - durationMs: number -): ConflictDetectionSummary { - const conflictsByType: Record = { - comfyui_version: 0, - frontend_version: 0, - import_failed: 0, - os: 0, - accelerator: 0, - banned: 0, - pending: 0 - // python_version: 0 - } - - const conflictsByTypeDetails: Record = { - comfyui_version: [], - frontend_version: [], - import_failed: [], - os: [], - accelerator: [], - banned: [], - pending: [] - // python_version: [], - } - - let bannedCount = 0 - let securityPendingCount = 0 - - results.forEach((result) => { - result.conflicts.forEach((conflict) => { - conflictsByType[conflict.type]++ - - if (!conflictsByTypeDetails[conflict.type].includes(result.package_id)) { - conflictsByTypeDetails[conflict.type].push(result.package_id) - } - - if (conflict.type === 'banned') bannedCount++ - if (conflict.type === 'pending') securityPendingCount++ - }) - }) - - return { - total_packages: results.length, - compatible_packages: results.filter((r) => r.is_compatible).length, - conflicted_packages: results.filter((r) => r.has_conflict).length, - banned_packages: bannedCount, - pending_packages: securityPendingCount, - conflicts_by_type_details: conflictsByTypeDetails, - last_check_timestamp: new Date().toISOString(), - check_duration_ms: durationMs - } -} - -/** - * Creates an empty summary for error cases. - */ -function getEmptySummary(): ConflictDetectionSummary { - return { - total_packages: 0, - compatible_packages: 0, - conflicted_packages: 0, - banned_packages: 0, - pending_packages: 0, - conflicts_by_type_details: { - comfyui_version: [], - frontend_version: [], - import_failed: [], - os: [], - accelerator: [], - banned: [], - pending: [] - // python_version: [], - }, - last_check_timestamp: new Date().toISOString(), - check_duration_ms: 0 - } -} diff --git a/src/workbench/extensions/manager/composables/useImportFailedDetection.ts b/src/workbench/extensions/manager/composables/useImportFailedDetection.ts index 3137d4824..9b1fd9b0e 100644 --- a/src/workbench/extensions/manager/composables/useImportFailedDetection.ts +++ b/src/workbench/extensions/manager/composables/useImportFailedDetection.ts @@ -2,9 +2,9 @@ import { type ComputedRef, computed, unref } from 'vue' import { useI18n } from 'vue-i18n' import { useDialogService } from '@/services/dialogService' -import type { ConflictDetail } from '@/types/conflictDetectionTypes' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore' +import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes' /** * Extracting import failed conflicts from conflict list diff --git a/src/workbench/extensions/manager/stores/conflictDetectionStore.ts b/src/workbench/extensions/manager/stores/conflictDetectionStore.ts index 598d17896..95908ac08 100644 --- a/src/workbench/extensions/manager/stores/conflictDetectionStore.ts +++ b/src/workbench/extensions/manager/stores/conflictDetectionStore.ts @@ -1,7 +1,7 @@ import { defineStore } from 'pinia' import { computed, ref } from 'vue' -import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes' +import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes' export const useConflictDetectionStore = defineStore( 'conflictDetection', diff --git a/src/workbench/extensions/manager/types/compatibility.types.ts b/src/workbench/extensions/manager/types/compatibility.types.ts new file mode 100644 index 000000000..10e56ad24 --- /dev/null +++ b/src/workbench/extensions/manager/types/compatibility.types.ts @@ -0,0 +1,10 @@ +/** + * System compatibility type definitions + * Registry supports exactly these values, null/undefined means compatible with all + */ + +// Registry OS +export type RegistryOS = 'Windows' | 'macOS' | 'Linux' + +// Registry Accelerator +export type RegistryAccelerator = 'CUDA' | 'ROCm' | 'Metal' | 'CPU' diff --git a/src/types/conflictDetectionTypes.ts b/src/workbench/extensions/manager/types/conflictDetectionTypes.ts similarity index 68% rename from src/types/conflictDetectionTypes.ts rename to src/workbench/extensions/manager/types/conflictDetectionTypes.ts index a176e0ea0..ab7efe08a 100644 --- a/src/types/conflictDetectionTypes.ts +++ b/src/workbench/extensions/manager/types/conflictDetectionTypes.ts @@ -5,7 +5,7 @@ * This file extends and uses types from comfyRegistryTypes.ts to maintain consistency * with the Registry API schema. */ -import type { components } from './comfyRegistryTypes' +import type { components } from '@/types/comfyRegistryTypes' // Re-export core types from Registry API export type Node = components['schemas']['Node'] @@ -18,7 +18,6 @@ export type ConflictType = | 'comfyui_version' // ComfyUI version mismatch | 'frontend_version' // Frontend version mismatch | 'import_failed' - // | 'python_version' // Python version mismatch | 'os' // Operating system incompatibility | 'accelerator' // GPU/accelerator incompatibility | 'banned' // Banned package @@ -28,7 +27,7 @@ export type ConflictType = * Node Pack requirements from Registry API * Extends Node type with additional installation and compatibility metadata */ -export interface NodePackRequirements extends Node { +export interface NodeRequirements extends Node { installed_version: string is_enabled: boolean is_banned: boolean @@ -42,23 +41,12 @@ export interface NodePackRequirements extends Node { */ export interface SystemEnvironment { // Version information - comfyui_version: string - frontend_version: string - // python_version: string - + comfyui_version?: string + frontend_version?: string // Platform information - os: string - platform_details: string - architecture: string - + os?: string // GPU/accelerator information - available_accelerators: Node['supported_accelerators'] - primary_accelerator: string - gpu_memory_mb?: number - - // Runtime information - node_env: 'development' | 'production' - user_agent: string + accelerator?: string } /** @@ -81,27 +69,12 @@ export interface ConflictDetail { required_value: string } -/** - * Overall conflict detection summary - */ -export interface ConflictDetectionSummary { - total_packages: number - compatible_packages: number - conflicted_packages: number - banned_packages: number - pending_packages: number - conflicts_by_type_details: Record - last_check_timestamp: string - check_duration_ms: number -} - /** * Response payload from conflict detection API */ export interface ConflictDetectionResponse { success: boolean error_message?: string - summary: ConflictDetectionSummary results: ConflictDetectionResult[] detected_system_environment?: Partial } diff --git a/src/types/importFailedTypes.ts b/src/workbench/extensions/manager/types/importFailedTypes.ts similarity index 100% rename from src/types/importFailedTypes.ts rename to src/workbench/extensions/manager/types/importFailedTypes.ts diff --git a/src/utils/conflictMessageUtil.ts b/src/workbench/extensions/manager/utils/conflictMessageUtil.ts similarity index 94% rename from src/utils/conflictMessageUtil.ts rename to src/workbench/extensions/manager/utils/conflictMessageUtil.ts index 2fa913371..c175760b5 100644 --- a/src/utils/conflictMessageUtil.ts +++ b/src/workbench/extensions/manager/utils/conflictMessageUtil.ts @@ -1,4 +1,4 @@ -import type { ConflictDetail } from '@/types/conflictDetectionTypes' +import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes' /** * Generates a localized conflict message for a given conflict detail. diff --git a/src/workbench/extensions/manager/utils/conflictUtils.ts b/src/workbench/extensions/manager/utils/conflictUtils.ts new file mode 100644 index 000000000..1a24ba387 --- /dev/null +++ b/src/workbench/extensions/manager/utils/conflictUtils.ts @@ -0,0 +1,83 @@ +import { groupBy, uniqBy } from 'es-toolkit/compat' + +import { normalizePackId } from '@/utils/packUtils' +import type { + ConflictDetail, + ConflictDetectionResult +} from '@/workbench/extensions/manager/types/conflictDetectionTypes' + +/** + * Checks for banned package status conflicts. + */ +export function createBannedConflict( + isBanned?: boolean +): ConflictDetail | null { + if (isBanned === true) { + return { + type: 'banned', + current_value: 'installed', + required_value: 'not_banned' + } + } + return null +} + +/** + * Checks for pending package status conflicts. + */ +export function createPendingConflict( + isPending?: boolean +): ConflictDetail | null { + if (isPending === true) { + return { + type: 'pending', + current_value: 'installed', + required_value: 'not_pending' + } + } + return null +} + +/** + * Groups and deduplicates conflicts by normalized package name. + * Consolidates multiple conflict sources (registry checks, import failures, disabled packages with version suffix) + * into a single UI entry per package. + * + * Example: + * - Input: [{name: "pack@1_0_3", conflicts: [...]}, {name: "pack", conflicts: [...]}] + * - Output: [{name: "pack", conflicts: [...combined unique conflicts...]}] + * + * @param conflicts Array of conflict detection results (may have duplicate packages with version suffixes) + * @returns Array of deduplicated conflict results grouped by normalized package name + */ +export function consolidateConflictsByPackage( + conflicts: ConflictDetectionResult[] +): ConflictDetectionResult[] { + // Group conflicts by normalized package name using es-toolkit + const grouped = groupBy(conflicts, (conflict) => + normalizePackId(conflict.package_name) + ) + + // Merge conflicts for each group + return Object.entries(grouped).map(([packageName, packageConflicts]) => { + // Flatten all conflicts from the group + const allConflicts = packageConflicts.flatMap((pc) => pc.conflicts) + + // Remove duplicate conflicts using uniqBy + const uniqueConflicts = uniqBy( + allConflicts, + (conflict) => + `${conflict.type}|${conflict.current_value}|${conflict.required_value}` + ) + + // Use the first item as base and update with merged data + const baseItem = packageConflicts[0] + return { + ...baseItem, + package_name: packageName, // Use normalized name + conflicts: uniqueConflicts, + has_conflict: uniqueConflicts.length > 0, + is_compatible: uniqueConflicts.length === 0 + } + }) +} diff --git a/src/workbench/extensions/manager/utils/systemCompatibility.ts b/src/workbench/extensions/manager/utils/systemCompatibility.ts new file mode 100644 index 000000000..cdd1c3103 --- /dev/null +++ b/src/workbench/extensions/manager/utils/systemCompatibility.ts @@ -0,0 +1,125 @@ +import { isEmpty, isNil } from 'es-toolkit/compat' + +import type { + RegistryAccelerator, + RegistryOS +} from '@/workbench/extensions/manager/types/compatibility.types' +import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes' + +/** + * Maps system OS string to Registry OS format + * @param systemOS Raw OS string from system stats ('darwin', 'win32', 'linux', etc) + * @returns Registry OS or undefined if unknown + */ +function getRegistryOS(systemOS?: string): RegistryOS | undefined { + if (!systemOS) return undefined + + const lower = systemOS.toLowerCase() + // Check darwin first to avoid matching 'win' in 'darwin' + if (lower.includes('darwin') || lower.includes('mac')) return 'macOS' + if (lower.includes('win')) return 'Windows' + if (lower.includes('linux')) return 'Linux' + + return undefined +} + +/** + * Maps device type to Registry accelerator format + * @param deviceType Raw device type from system stats ('cuda', 'mps', 'rocm', 'cpu', etc) + * @returns Registry accelerator + */ +function getRegistryAccelerator(deviceType?: string): RegistryAccelerator { + if (!deviceType) return 'CPU' + + const lower = deviceType.toLowerCase() + if (lower === 'cuda') return 'CUDA' + if (lower === 'mps') return 'Metal' + if (lower === 'rocm') return 'ROCm' + + return 'CPU' +} + +/** + * Checks OS compatibility + * @param supported Supported OS list from Registry (null/undefined = all OS supported) + * @param current Current system OS + * @returns ConflictDetail if incompatible, null if compatible + */ +export function checkOSCompatibility( + supported?: RegistryOS[] | null, + current?: string +): ConflictDetail | null { + // null/undefined/empty = all OS supported + if (isNil(supported) || isEmpty(supported)) return null + + const currentOS = getRegistryOS(current) + if (!currentOS) { + return { + type: 'os', + current_value: 'Unknown', + required_value: supported.join(', ') + } + } + + if (!supported.includes(currentOS)) { + return { + type: 'os', + current_value: currentOS, + required_value: supported.join(', ') + } + } + + return null +} + +/** + * Checks accelerator compatibility + * @param supported Supported accelerators from Registry (null/undefined = all accelerators supported) + * @param current Current device type + * @returns ConflictDetail if incompatible, null if compatible + */ +export function checkAcceleratorCompatibility( + supported?: RegistryAccelerator[] | null, + current?: string +): ConflictDetail | null { + // null/undefined/empty = all accelerator supported + if (isNil(supported) || isEmpty(supported)) return null + + const currentAcc = getRegistryAccelerator(current) + + if (!supported.includes(currentAcc)) { + return { + type: 'accelerator', + current_value: currentAcc, + required_value: supported.join(', ') + } + } + + return null +} + +/** + * Normalizes OS values from Registry API + * Handles edge cases like "OS Independent" + * @returns undefined if all OS supported, otherwise filtered valid OS list + */ +export function normalizeOSList( + osValues?: string[] | null +): RegistryOS[] | undefined { + if (isNil(osValues) || isEmpty(osValues)) return undefined + + // "OS Independent" means all OS supported + if (osValues.some((os) => os.toLowerCase() === 'os independent')) { + return undefined + } + + // Filter to valid Registry OS values only + const validOS: RegistryOS[] = [] + osValues.forEach((os) => { + if (os === 'Windows' || os === 'macOS' || os === 'Linux') { + if (!validOS.includes(os)) validOS.push(os) + } + }) + + return validOS.length > 0 ? validOS : undefined +} diff --git a/src/workbench/extensions/manager/utils/versionUtil.ts b/src/workbench/extensions/manager/utils/versionUtil.ts new file mode 100644 index 000000000..a45096304 --- /dev/null +++ b/src/workbench/extensions/manager/utils/versionUtil.ts @@ -0,0 +1,73 @@ +import { isEmpty, isNil } from 'es-toolkit/compat' +import { clean, satisfies } from 'semver' + +import config from '@/config' +import type { + ConflictDetail, + ConflictType +} from '@/workbench/extensions/manager/types/conflictDetectionTypes' + +/** + * Cleans a version string by removing common prefixes and normalizing format + * @param version Raw version string (e.g., "v1.2.3", "1.2.3-alpha") + * @returns Cleaned version string or original if cleaning fails + */ +function cleanVersion(version: string): string { + return clean(version) || version +} + +/** + * Checks version compatibility and returns conflict details. + * Supports all semver ranges including >=, <=, >, <, ~, ^ operators. + * @param type Conflict type (e.g., 'comfyui_version', 'frontend_version') + * @param currentVersion Current version string + * @param supportedVersion Required version range string + * @returns ConflictDetail object if incompatible, null if compatible + */ +export function checkVersionCompatibility( + type: ConflictType, + currentVersion?: string, + supportedVersion?: string +): ConflictDetail | null { + // If current version is unknown, assume compatible (no conflict) + if (isNil(currentVersion) || isEmpty(currentVersion)) { + return null + } + + // If no version requirement specified, assume compatible (no conflict) + if (isNil(supportedVersion) || isEmpty(supportedVersion?.trim())) { + return null + } + + // Clean and check version compatibility + const cleanCurrent = cleanVersion(currentVersion) + + // Check if version satisfies the range + let isCompatible = false + try { + isCompatible = satisfies(cleanCurrent, supportedVersion) + } catch { + // If semver can't parse it, return conflict + return { + type, + current_value: currentVersion, + required_value: supportedVersion + } + } + + if (isCompatible) return null + + return { + type, + current_value: currentVersion, + required_value: supportedVersion + } +} + +/** + * get frontend version from config. + * @returns frontend version string or undefined + */ +export function getFrontendVersion(): string | undefined { + return config.app_version || import.meta.env.VITE_APP_VERSION || undefined +} diff --git a/tests-ui/tests/components/dialog/content/manager/NodeConflictDialogContent.test.ts b/tests-ui/tests/components/dialog/content/manager/NodeConflictDialogContent.test.ts index d79866e2c..bcb4d4954 100644 --- a/tests-ui/tests/components/dialog/content/manager/NodeConflictDialogContent.test.ts +++ b/tests-ui/tests/components/dialog/content/manager/NodeConflictDialogContent.test.ts @@ -4,8 +4,8 @@ import Button from 'primevue/button' import { beforeEach, describe, expect, it, vi } from 'vitest' import { computed, ref } from 'vue' -import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes' import NodeConflictDialogContent from '@/workbench/extensions/manager/components/manager/NodeConflictDialogContent.vue' +import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes' // Mock getConflictMessage utility vi.mock('@/utils/conflictMessageUtil', () => ({ diff --git a/tests-ui/tests/components/dialog/footer/ManagerProgressFooter.test.ts b/tests-ui/tests/components/dialog/footer/ManagerProgressFooter.test.ts index 321325aa0..30253fda7 100644 --- a/tests-ui/tests/components/dialog/footer/ManagerProgressFooter.test.ts +++ b/tests-ui/tests/components/dialog/footer/ManagerProgressFooter.test.ts @@ -27,7 +27,7 @@ vi.mock( () => ({ useConflictDetection: vi.fn(() => ({ conflictedPackages: { value: [] }, - performConflictDetection: vi.fn().mockResolvedValue(undefined) + runFullConflictAnalysis: vi.fn().mockResolvedValue(undefined) })) }) ) diff --git a/tests-ui/tests/composables/nodePack/usePacksStatus.test.ts b/tests-ui/tests/composables/nodePack/usePacksStatus.test.ts index 4ac607958..2fbabb3fb 100644 --- a/tests-ui/tests/composables/nodePack/usePacksStatus.test.ts +++ b/tests-ui/tests/composables/nodePack/usePacksStatus.test.ts @@ -3,9 +3,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ref } from 'vue' import type { components } from '@/types/comfyRegistryTypes' -import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes' import { usePacksStatus } from '@/workbench/extensions/manager/composables/nodePack/usePacksStatus' import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore' +import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes' type NodePack = components['schemas']['Node'] type NodeStatus = components['schemas']['NodeStatus'] diff --git a/tests-ui/tests/composables/useConflictDetection.test.ts b/tests-ui/tests/composables/useConflictDetection.test.ts index be29ebfd8..d3079219e 100644 --- a/tests-ui/tests/composables/useConflictDetection.test.ts +++ b/tests-ui/tests/composables/useConflictDetection.test.ts @@ -1,32 +1,20 @@ import { createPinia, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { nextTick } from 'vue' +import { computed, ref } from 'vue' +import { useComfyRegistryService } from '@/services/comfyRegistryService' +import { useSystemStatsStore } from '@/stores/systemStatsStore' import type { components } from '@/types/comfyRegistryTypes' +import { useInstalledPacks } from '@/workbench/extensions/manager/composables/nodePack/useInstalledPacks' +import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment' import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection' -import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes' - -type InstalledPacksResponse = - ManagerComponents['schemas']['InstalledPacksResponse'] +import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService' +import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' +import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore' +import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes' +import { checkVersionCompatibility } from '@/workbench/extensions/manager/utils/versionUtil' // Mock dependencies -vi.mock('@/scripts/api', () => ({ - api: { - fetchApi: vi.fn(), - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - interrupt: vi.fn(), - init: vi.fn(), - internalURL: vi.fn(), - apiURL: vi.fn(), - fileURL: vi.fn(), - dispatchCustomEvent: vi.fn(), - dispatchEvent: vi.fn(), - getExtensions: vi.fn(), - freeMemory: vi.fn() - } -})) - vi.mock('@/workbench/extensions/manager/services/comfyManagerService', () => ({ useComfyManagerService: vi.fn() })) @@ -39,10 +27,51 @@ vi.mock('@/stores/systemStatsStore', () => ({ useSystemStatsStore: vi.fn() })) -vi.mock('@/config', () => ({ - default: { - app_version: '1.24.0-1' - } +vi.mock('@/workbench/extensions/manager/utils/versionUtil', () => ({ + getFrontendVersion: vi.fn(() => '1.24.0'), + checkVersionCompatibility: vi.fn() +})) + +vi.mock('@/workbench/extensions/manager/utils/systemCompatibility', () => ({ + checkOSCompatibility: vi.fn(), + checkAcceleratorCompatibility: vi.fn(), + normalizeOSList: vi.fn((list) => list) +})) + +vi.mock('@/workbench/extensions/manager/utils/conflictUtils', () => ({ + consolidateConflictsByPackage: vi.fn((results) => results), + createBannedConflict: vi.fn((isBanned) => + isBanned + ? { + type: 'banned', + current_value: 'installed', + required_value: 'not_banned' + } + : null + ), + createPendingConflict: vi.fn((isPending) => + isPending + ? { + type: 'pending', + current_value: 'installed', + required_value: 'not_pending' + } + : null + ), + generateConflictSummary: vi.fn((results, duration) => ({ + total_packages: results.length, + compatible_packages: results.filter( + (r: ConflictDetectionResult) => r.is_compatible + ).length, + conflicted_packages: results.filter( + (r: ConflictDetectionResult) => r.has_conflict + ).length, + banned_packages: 0, + pending_packages: 0, + conflicts_by_type_details: {}, + last_check_timestamp: new Date().toISOString(), + check_duration_ms: duration + })) })) vi.mock( @@ -55,958 +84,386 @@ vi.mock( vi.mock( '@/workbench/extensions/manager/composables/nodePack/useInstalledPacks', () => ({ - useInstalledPacks: vi.fn(() => ({ - installedPacks: { value: [] }, - refreshInstalledPacks: vi.fn(), - startFetchInstalled: vi.fn() - })) + useInstalledPacks: vi.fn() }) ) vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({ - useComfyManagerStore: vi.fn(() => ({ - isPackInstalled: vi.fn(), - installedPacks: { value: [] } - })) + useComfyManagerStore: vi.fn() })) vi.mock('@/workbench/extensions/manager/stores/conflictDetectionStore', () => ({ - useConflictDetectionStore: vi.fn(() => ({ - conflictResults: { value: [] }, - updateConflictResults: vi.fn(), - clearConflicts: vi.fn(), - setConflictResults: vi.fn() + useConflictDetectionStore: vi.fn() +})) + +vi.mock('@/workbench/extensions/manager/composables/useManagerState', () => ({ + useManagerState: vi.fn(() => ({ + isNewManagerUI: { value: true } })) })) -describe.skip('useConflictDetection with Registry Store', () => { +describe('useConflictDetection', () => { let pinia: ReturnType const mockComfyManagerService = { - listInstalledPacks: vi.fn(), - getImportFailInfo: vi.fn() - } + getImportFailInfoBulk: vi.fn(), + isLoading: ref(false), + error: ref(null) + } as unknown as ReturnType const mockRegistryService = { - getPackByVersion: vi.fn() - } + getBulkNodeVersions: vi.fn(), + isLoading: ref(false), + error: ref(null) + } as unknown as ReturnType - const mockAcknowledgment = { - checkComfyUIVersionChange: vi.fn(), - shouldShowConflictModal: { value: true }, - shouldShowRedDot: { value: true }, - acknowledgedPackageIds: { value: [] }, - dismissConflictModal: vi.fn(), - dismissRedDotNotification: vi.fn(), - acknowledgeConflict: vi.fn() - } + // Create a ref that can be modified in tests + const mockInstalledPacksWithVersions = ref<{ id: string; version: string }[]>( + [] + ) + + const mockInstalledPacks = { + startFetchInstalled: vi.fn(), + installedPacks: ref([]), + installedPacksWithVersions: computed( + () => mockInstalledPacksWithVersions.value + ), + isReady: ref(false), + isLoading: ref(false), + error: ref(null) + } as unknown as ReturnType + + const mockManagerStore = { + isPackEnabled: vi.fn() + } as unknown as ReturnType + + // Create refs that can be used to control computed properties + const mockConflictedPackages = ref([]) + + const mockConflictStore = { + hasConflicts: computed(() => + mockConflictedPackages.value.some((p) => p.has_conflict) + ), + conflictedPackages: mockConflictedPackages, + bannedPackages: computed(() => + mockConflictedPackages.value.filter((p) => + p.conflicts?.some((c) => c.type === 'banned') + ) + ), + securityPendingPackages: computed(() => + mockConflictedPackages.value.filter((p) => + p.conflicts?.some((c) => c.type === 'pending') + ) + ), + setConflictedPackages: vi.fn(), + clearConflicts: vi.fn() + } as unknown as ReturnType const mockSystemStatsStore = { - refetchSystemStats: vi.fn(), systemStats: { system: { + os: 'darwin', // sys.platform returns 'darwin' for macOS + ram_total: 17179869184, + ram_free: 8589934592, comfyui_version: '0.3.41', - os: 'Darwin' + required_frontend_version: '1.24.0', + python_version: + '3.11.0 (main, Oct 13 2023, 09:34:16) [Clang 15.0.0 (clang-1500.0.40.1)]', + pytorch_version: '2.1.0', + embedded_python: false, + argv: [] }, devices: [ { name: 'Apple M1 Pro', type: 'mps', - vram_total: 17179869184 + index: 0, + vram_total: 17179869184, + vram_free: 8589934592, + torch_vram_total: 17179869184, + torch_vram_free: 8589934592 } ] - } as any - } + }, + isInitialized: ref(true), + $state: {} as never, + $patch: vi.fn(), + $reset: vi.fn(), + $subscribe: vi.fn(), + $onAction: vi.fn(), + $dispose: vi.fn(), + $id: 'systemStats', + _customProperties: new Set() + } as unknown as ReturnType - beforeEach(async () => { + const mockAcknowledgment = { + checkComfyUIVersionChange: vi.fn(), + acknowledgmentState: computed(() => ({})), + shouldShowConflictModal: computed(() => false), + shouldShowRedDot: computed(() => false), + shouldShowManagerBanner: computed(() => false), + dismissRedDotNotification: vi.fn(), + dismissWarningBanner: vi.fn(), + markConflictsAsSeen: vi.fn() + } as unknown as ReturnType + + beforeEach(() => { vi.clearAllMocks() pinia = createPinia() setActivePinia(pinia) - // Reset mock system stats to default state - mockSystemStatsStore.systemStats = { - system: { - comfyui_version: '0.3.41', - os: 'Darwin' - }, - devices: [ - { - name: 'Apple M1 Pro', - type: 'mps', - vram_total: 17179869184 - } - ] - } as any + // Setup mocks + vi.mocked(useComfyManagerService).mockReturnValue(mockComfyManagerService) + vi.mocked(useComfyRegistryService).mockReturnValue(mockRegistryService) + vi.mocked(useSystemStatsStore).mockReturnValue(mockSystemStatsStore) + vi.mocked(useConflictAcknowledgment).mockReturnValue(mockAcknowledgment) + vi.mocked(useInstalledPacks).mockReturnValue(mockInstalledPacks) + vi.mocked(useComfyManagerStore).mockReturnValue(mockManagerStore) + vi.mocked(useConflictDetectionStore).mockReturnValue(mockConflictStore) - // Reset mock functions - mockSystemStatsStore.refetchSystemStats.mockResolvedValue(undefined) - mockComfyManagerService.listInstalledPacks.mockReset() - mockComfyManagerService.getImportFailInfo.mockReset() - mockRegistryService.getPackByVersion.mockReset() - - // Mock useComfyManagerService - const { useComfyManagerService } = await import( - '@/workbench/extensions/manager/services/comfyManagerService' + // Reset mock implementations + vi.mocked(mockInstalledPacks.startFetchInstalled).mockResolvedValue( + undefined ) - vi.mocked(useComfyManagerService).mockReturnValue( - mockComfyManagerService as any + vi.mocked(mockManagerStore.isPackEnabled).mockReturnValue(true) + vi.mocked(mockRegistryService.getBulkNodeVersions).mockResolvedValue({ + node_versions: [] + }) + vi.mocked(mockComfyManagerService.getImportFailInfoBulk).mockResolvedValue( + {} ) - // Mock useComfyRegistryService - const { useComfyRegistryService } = await import( - '@/services/comfyRegistryService' - ) - vi.mocked(useComfyRegistryService).mockReturnValue( - mockRegistryService as any - ) - - // Mock useSystemStatsStore - const { useSystemStatsStore } = await import('@/stores/systemStatsStore') - vi.mocked(useSystemStatsStore).mockReturnValue(mockSystemStatsStore as any) - - // Mock useConflictAcknowledgment - const { useConflictAcknowledgment } = await import( - '@/workbench/extensions/manager/composables/useConflictAcknowledgment' - ) - vi.mocked(useConflictAcknowledgment).mockReturnValue( - mockAcknowledgment as any - ) + // Reset the installedPacksWithVersions data + mockInstalledPacksWithVersions.value = [] + // Reset conflicted packages + mockConflictedPackages.value = [] }) afterEach(() => { vi.restoreAllMocks() }) - describe('system environment detection', () => { - it('should collect system environment information successfully', async () => { - const { detectSystemEnvironment } = useConflictDetection() - const environment = await detectSystemEnvironment() + describe('system environment collection', () => { + it('should collect system environment correctly', async () => { + const { collectSystemEnvironment } = useConflictDetection() + const environment = await collectSystemEnvironment() - expect(environment.comfyui_version).toBe('0.3.41') - expect(environment.frontend_version).toBe('1.24.0-1') - expect(environment.available_accelerators).toContain('Metal') - expect(environment.available_accelerators).toContain('CPU') - expect(environment.primary_accelerator).toBe('Metal') + expect(environment).toEqual({ + comfyui_version: '0.3.41', + frontend_version: '1.24.0', + os: 'darwin', + accelerator: 'mps' + }) }) - it('should return fallback environment information when systemStatsStore fails', async () => { - // Mock systemStatsStore failure - mockSystemStatsStore.refetchSystemStats.mockRejectedValue( - new Error('Store failure') - ) - mockSystemStatsStore.systemStats = null + it('should handle missing system stats gracefully', async () => { + mockSystemStatsStore.systemStats = null as never - const { detectSystemEnvironment } = useConflictDetection() - const environment = await detectSystemEnvironment() + const { collectSystemEnvironment } = useConflictDetection() + const environment = await collectSystemEnvironment() - expect(environment.comfyui_version).toBe('unknown') - expect(environment.frontend_version).toBe('1.24.0-1') - expect(environment.available_accelerators).toEqual(['CPU']) + // When systemStats is null, empty strings are used as fallback + expect(environment).toEqual({ + comfyui_version: '', + frontend_version: '1.24.0', + os: '', + accelerator: '' + }) }) }) - describe('package requirements detection with Registry Store', () => { - it('should fetch and combine local + Registry data successfully', async () => { - // Mock installed packages - const mockInstalledPacks: InstalledPacksResponse = { - 'ComfyUI-Manager': { - ver: 'cb0fa5829d5378e5dddb8e8515b30a3ff20e1471', - cnr_id: '', - aux_id: 'viva-jinyi/ComfyUI-Manager', - enabled: true - }, - 'ComfyUI-TestNode': { - ver: '1.0.0', - cnr_id: 'test-node', - aux_id: null, - enabled: false - } - } - - // Mock Registry data - const mockRegistryPacks: components['schemas']['Node'][] = [ + describe('conflict detection', () => { + it('should detect version conflicts', async () => { + // Setup installed packages + mockInstalledPacks.isReady.value = true + mockInstalledPacks.installedPacks.value = [ { - id: 'ComfyUI-Manager', - name: 'ComfyUI Manager', - supported_os: ['Windows', 'Linux', 'macOS'], - supported_accelerators: ['CUDA', 'Metal', 'CPU'], - supported_comfyui_version: '>=0.3.0', - status: 'NodeStatusActive' - } as components['schemas']['Node'], - { - id: 'ComfyUI-TestNode', - name: 'Test Node', - supported_os: ['Windows', 'Linux'], - supported_accelerators: ['CUDA'], - supported_comfyui_version: '>=0.2.0', - status: 'NodeStatusBanned' + id: 'test-pack', + name: 'Test Pack', + latest_version: { version: '1.0.0' } } as components['schemas']['Node'] ] - mockComfyManagerService.listInstalledPacks.mockResolvedValue( - mockInstalledPacks - ) + mockInstalledPacksWithVersions.value = [ + { + id: 'test-pack', + version: '1.0.0' + } + ] - // Mock Registry Service individual calls - mockRegistryService.getPackByVersion.mockImplementation( - (packageName: string) => { - const packageData = mockRegistryPacks.find( - (p) => p.id === packageName - ) - if (packageData) { - return Promise.resolve({ - ...packageData, - supported_comfyui_version: packageData.supported_comfyui_version, - supported_os: packageData.supported_os, - supported_accelerators: packageData.supported_accelerators, - status: packageData.status - }) + // Mock registry response with version requirements + vi.mocked(mockRegistryService.getBulkNodeVersions).mockResolvedValue({ + node_versions: [ + { + status: 'success' as const, + identifier: { node_id: 'test-pack', version: '1.0.0' }, + node_version: { + supported_comfyui_version: '>=0.4.0', + supported_comfyui_frontend_version: '>=2.0.0', + supported_os: ['Windows', 'Linux', 'macOS'], + supported_accelerators: ['CUDA', 'Metal', 'CPU'], + status: 'NodeVersionStatusActive' as const, + version: '1.0.0', + publisher_id: 'test-publisher', + node_id: 'test-pack', + created_at: '2024-01-01T00:00:00Z' + } as components['schemas']['NodeVersion'] } - return Promise.resolve(null) + ] + }) + + // Mock version checks to return conflicts + vi.mocked(checkVersionCompatibility).mockImplementation( + (type, current, required) => { + if (type === 'comfyui_version' && required === '>=0.4.0') { + return { + type: 'comfyui_version', + current_value: current || '0.3.41', + required_value: '>=0.4.0' + } + } + return null } ) - const { performConflictDetection } = useConflictDetection() - const result = await performConflictDetection() + const { runFullConflictAnalysis } = useConflictDetection() + const result = await runFullConflictAnalysis() expect(result.success).toBe(true) - expect(result.summary.total_packages).toBeGreaterThanOrEqual(1) - expect(result.results.length).toBeGreaterThanOrEqual(1) - - // Verify individual calls were made - expect(mockRegistryService.getPackByVersion).toHaveBeenCalledWith( - 'ComfyUI-Manager', - 'cb0fa5829d5378e5dddb8e8515b30a3ff20e1471', - expect.anything() - ) - expect(mockRegistryService.getPackByVersion).toHaveBeenCalledWith( - 'ComfyUI-TestNode', - '1.0.0', - expect.anything() - ) - - // Check that at least one package was processed - expect(result.results.length).toBeGreaterThan(0) - - // If we have results, check their structure - if (result.results.length > 0) { - const firstResult = result.results[0] - expect(firstResult).toHaveProperty('package_id') - expect(firstResult).toHaveProperty('conflicts') - expect(firstResult).toHaveProperty('is_compatible') - } - }) - - it('should handle Registry Store failures gracefully', async () => { - // Mock installed packages - const mockInstalledPacks: InstalledPacksResponse = { - 'Unknown-Package': { - ver: '1.0.0', - cnr_id: 'unknown', - aux_id: null, - enabled: true - } - } - - mockComfyManagerService.listInstalledPacks.mockResolvedValue( - mockInstalledPacks - ) - - // Mock Registry Service returning null (no packages found) - mockRegistryService.getPackByVersion.mockResolvedValue(null) - - const { performConflictDetection } = useConflictDetection() - const result = await performConflictDetection() - - expect(result.success).toBe(true) - expect(result.summary.total_packages).toBe(1) expect(result.results).toHaveLength(1) - - // Should have warning about missing Registry data - const unknownPackage = result.results[0] - expect(unknownPackage.conflicts).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: 'pending', - current_value: 'no_registry_data', - required_value: 'registry_data_available' - }) - ]) - ) + expect(result.results[0].has_conflict).toBe(true) + expect(result.results[0].conflicts).toContainEqual({ + type: 'comfyui_version', + current_value: '0.3.41', + required_value: '>=0.4.0' + }) }) - it('should return empty array when local package information cannot be retrieved', async () => { - mockComfyManagerService.listInstalledPacks.mockResolvedValue(null) - - const { performConflictDetection } = useConflictDetection() - const result = await performConflictDetection() - - expect(result.success).toBe(true) - expect(result.summary.total_packages).toBe(0) - expect(result.results).toHaveLength(0) - }) - }) - - describe('conflict detection logic with Registry Store', () => { - it('should detect no conflicts for fully compatible packages', async () => { - // Mock compatible package - const mockInstalledPacks: InstalledPacksResponse = { - CompatibleNode: { - ver: '1.0.0', - cnr_id: 'compatible-node', - aux_id: null, - enabled: true - } - } - - const mockCompatibleRegistryPacks: components['schemas']['Node'][] = [ + it('should detect banned packages', async () => { + mockInstalledPacks.isReady.value = true + mockInstalledPacks.installedPacks.value = [ { - id: 'CompatibleNode', - name: 'Compatible Node', - supported_os: ['Windows', 'Linux', 'macOS'], - supported_accelerators: ['Metal', 'CUDA', 'CPU'], - supported_comfyui_version: '>=0.3.0', - status: 'NodeStatusActive' + id: 'banned-pack', + name: 'Banned Pack' } as components['schemas']['Node'] ] - mockComfyManagerService.listInstalledPacks.mockResolvedValue( - mockInstalledPacks - ) - // Mock Registry Service for compatible package - mockRegistryService.getPackByVersion.mockImplementation( - (packageName: string) => { - const packageData = mockCompatibleRegistryPacks.find( - (p) => p.id === packageName - ) - return Promise.resolve(packageData || null) - } - ) - - const { performConflictDetection } = useConflictDetection() - const result = await performConflictDetection() - - expect(result.success).toBe(true) - expect(result.summary.conflicted_packages).toBe(0) - expect(result.summary.compatible_packages).toBe(1) - expect(result.results[0].conflicts).toHaveLength(0) - }) - - it('should detect OS incompatibility conflicts', async () => { - // Mock OS-incompatible package - const mockInstalledPacks: InstalledPacksResponse = { - WindowsOnlyNode: { - ver: '1.0.0', - cnr_id: 'windows-only', - aux_id: null, - enabled: true - } - } - - const mockWindowsOnlyRegistryPacks: components['schemas']['Node'][] = [ + mockInstalledPacksWithVersions.value = [ { - id: 'WindowsOnlyNode', - name: 'Windows Only Node', - supported_os: ['Windows'], - supported_accelerators: ['Metal', 'CUDA', 'CPU'], - supported_comfyui_version: '>=0.3.0', - status: 'NodeStatusActive' - } as components['schemas']['Node'] + id: 'banned-pack', + version: '1.0.0' + } ] - mockComfyManagerService.listInstalledPacks.mockResolvedValue( - mockInstalledPacks - ) - mockRegistryService.getPackByVersion.mockImplementation( - (packageName: string) => { - const packageData = mockWindowsOnlyRegistryPacks.find( - (p: any) => p.id === packageName - ) - return Promise.resolve(packageData || null) - } - ) - - const { performConflictDetection } = useConflictDetection() - const result = await performConflictDetection() - - expect(result.success).toBe(true) - expect(result.summary.conflicted_packages).toBe(1) - - const windowsNode = result.results[0] - expect(windowsNode.conflicts).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: 'os', - current_value: 'macOS', - required_value: expect.stringContaining('Windows') - }) - ]) - ) - }) - - it('should detect accelerator incompatibility conflicts', async () => { - // Mock CUDA-only package - const mockInstalledPacks: InstalledPacksResponse = { - CudaOnlyNode: { - ver: '1.0.0', - cnr_id: 'cuda-only', - aux_id: null, - enabled: true - } - } - - const mockCudaOnlyRegistryPacks: components['schemas']['Node'][] = [ - { - id: 'CudaOnlyNode', - name: 'CUDA Only Node', - supported_os: ['windows', 'linux', 'macos'], - supported_accelerators: ['CUDA'], - supported_comfyui_version: '>=0.3.0', - status: 'NodeStatusActive' - } as components['schemas']['Node'] - ] - - mockComfyManagerService.listInstalledPacks.mockResolvedValue( - mockInstalledPacks - ) - mockRegistryService.getPackByVersion.mockImplementation( - (packageName: string) => { - const packageData = mockCudaOnlyRegistryPacks.find( - (p: any) => p.id === packageName - ) - return Promise.resolve(packageData || null) - } - ) - - const { performConflictDetection } = useConflictDetection() - const result = await performConflictDetection() - - expect(result.success).toBe(true) - expect(result.summary.conflicted_packages).toBe(1) - - const cudaNode = result.results[0] - expect(cudaNode.conflicts).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: 'accelerator', - current_value: expect.any(String), - required_value: expect.stringContaining('CUDA') - }) - ]) - ) - }) - - it('should treat Registry-banned packages as conflicts', async () => { - // Mock Registry-banned package - const mockInstalledPacks: InstalledPacksResponse = { - BannedNode: { - ver: '1.0.0', - cnr_id: 'banned-node', - aux_id: null, - enabled: true - } - } - - const mockBannedRegistryPacks: components['schemas']['NodeVersion'][] = [ - { - id: 'BannedNode', - supported_os: ['windows', 'linux', 'macos'], - supported_accelerators: ['Metal', 'CUDA', 'CPU'], - supported_comfyui_version: '>=0.3.0', - status: 'NodeVersionStatusBanned' - } as components['schemas']['NodeVersion'] - ] - - mockComfyManagerService.listInstalledPacks.mockResolvedValue( - mockInstalledPacks - ) - mockRegistryService.getPackByVersion.mockImplementation( - (packageName: string) => { - const packageData = mockBannedRegistryPacks.find( - (p: any) => p.id === packageName - ) - return Promise.resolve(packageData || null) - } - ) - - const { performConflictDetection } = useConflictDetection() - const result = await performConflictDetection() - - expect(result.success).toBe(true) - expect(result.summary.banned_packages).toBe(1) - - const bannedNode = result.results[0] - expect(bannedNode.conflicts).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: 'banned', - current_value: 'installed', - required_value: 'not_banned' - }) - ]) - ) - // Banned nodes should have 'banned' conflict type - expect(bannedNode.conflicts.some((c) => c.type === 'banned')).toBe(true) - }) - - it('should treat locally disabled packages as banned', async () => { - // Mock locally disabled package - const mockInstalledPacks: InstalledPacksResponse = { - DisabledNode: { - ver: '1.0.0', - cnr_id: 'disabled-node', - aux_id: null, - enabled: false - } - } - - const mockActiveRegistryPacks: components['schemas']['Node'][] = [ - { - id: 'DisabledNode', - name: 'Disabled Node', - supported_os: ['windows', 'linux', 'macos'], - supported_accelerators: ['Metal', 'CUDA', 'CPU'], - supported_comfyui_version: '>=0.3.0', - status: 'NodeStatusActive' - } as components['schemas']['Node'] - ] - - mockComfyManagerService.listInstalledPacks.mockResolvedValue( - mockInstalledPacks - ) - mockRegistryService.getPackByVersion.mockImplementation( - (packageName: string) => { - const packageData = mockActiveRegistryPacks.find( - (p: any) => p.id === packageName - ) - return Promise.resolve(packageData || null) - } - ) - - const { performConflictDetection } = useConflictDetection() - const result = await performConflictDetection() - - expect(result.success).toBe(true) - expect(result.summary.banned_packages).toBe(1) - - const disabledNode = result.results[0] - expect(disabledNode.conflicts).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: 'banned', - current_value: 'installed', - required_value: 'not_banned' - }) - ]) - ) - // Disabled nodes should have 'banned' conflict type - expect(disabledNode.conflicts.some((c) => c.type === 'banned')).toBe(true) - }) - }) - - describe('computed properties with Registry Store', () => { - it('should return true for hasConflicts when Registry conflicts exist', async () => { - // Mock package with OS incompatibility - const mockInstalledPacks: InstalledPacksResponse = { - ConflictedNode: { - ver: '1.0.0', - cnr_id: 'conflicted-node', - aux_id: null, - enabled: true - } - } - - const mockConflictedRegistryPacks: components['schemas']['Node'][] = [ - { - id: 'ConflictedNode', - name: 'Conflicted Node', - supported_os: ['Windows'], - supported_accelerators: ['Metal', 'CUDA', 'CPU'], - supported_comfyui_version: '>=0.3.0', - status: 'NodeStatusActive' - } as components['schemas']['Node'] - ] - - mockComfyManagerService.listInstalledPacks.mockResolvedValue( - mockInstalledPacks - ) - mockRegistryService.getPackByVersion.mockImplementation( - (packageName: string) => { - const packageData = mockConflictedRegistryPacks.find( - (p: any) => p.id === packageName - ) - return Promise.resolve(packageData || null) - } - ) - - const { hasConflicts, performConflictDetection } = useConflictDetection() - - // Initial value should be false - expect(hasConflicts.value).toBe(false) - - // Execute conflict detection - await performConflictDetection() - await nextTick() - - // Should be true when conflicts are detected - expect(hasConflicts.value).toBe(true) - }) - - it('should return packages with conflicts', async () => { - // Mock package with conflicts - const mockInstalledPacks: InstalledPacksResponse = { - ErrorNode: { - ver: '1.0.0', - cnr_id: 'error-node', - aux_id: null, - enabled: true - } - } - - const mockErrorRegistryPacks: components['schemas']['Node'][] = [ - { - id: 'ErrorNode', - name: 'Error Node', - supported_os: ['Windows'], - supported_accelerators: ['CUDA'], - supported_comfyui_version: '>=0.3.0', - status: 'NodeStatusActive' - } as components['schemas']['Node'] - ] - - mockComfyManagerService.listInstalledPacks.mockResolvedValue( - mockInstalledPacks - ) - mockRegistryService.getPackByVersion.mockImplementation( - (packageName: string) => { - const packageData = mockErrorRegistryPacks.find( - (p: any) => p.id === packageName - ) - return Promise.resolve(packageData || null) - } - ) - - const { conflictedPackages, performConflictDetection } = - useConflictDetection() - - await performConflictDetection() - await nextTick() - - expect(conflictedPackages.value.length).toBeGreaterThan(0) - expect( - conflictedPackages.value.every((result) => result.has_conflict === true) - ).toBe(true) - }) - - it('should return only banned packages for bannedPackages', async () => { - // Mock one banned and one normal package - const mockInstalledPacks: InstalledPacksResponse = { - BannedNode: { - ver: '1.0.0', - cnr_id: 'banned-node', - aux_id: null, - enabled: false - }, - NormalNode: { - ver: '1.0.0', - cnr_id: 'normal-node', - aux_id: null, - enabled: true - } - } - - const mockRegistryPacks: components['schemas']['Node'][] = [ - { - id: 'BannedNode', - name: 'Banned Node', - supported_os: ['windows', 'linux', 'macos'], - supported_accelerators: ['Metal', 'CUDA', 'CPU'], - supported_comfyui_version: '>=0.3.0', - status: 'NodeStatusActive' - } as components['schemas']['Node'], - { - id: 'NormalNode', - name: 'Normal Node', - supported_os: ['windows', 'linux', 'macos'], - supported_accelerators: ['Metal', 'CUDA', 'CPU'], - supported_comfyui_version: '>=0.3.0', - status: 'NodeStatusActive' - } as components['schemas']['Node'] - ] - - mockComfyManagerService.listInstalledPacks.mockResolvedValue( - mockInstalledPacks - ) - mockRegistryService.getPackByVersion.mockImplementation( - (packageName: string) => { - const packageData = mockRegistryPacks.find( - (p: any) => p.id === packageName - ) - return Promise.resolve(packageData || null) - } - ) - - const { bannedPackages, performConflictDetection } = - useConflictDetection() - - await performConflictDetection() - await nextTick() - - expect(bannedPackages.value).toHaveLength(1) - expect(bannedPackages.value[0].package_id).toBe('BannedNode') - }) - }) - - describe('error resilience with Registry Store', () => { - it('should continue execution even when system environment detection fails', async () => { - // Mock system stats store failure - mockSystemStatsStore.refetchSystemStats.mockRejectedValue( - new Error('Store error') - ) - mockSystemStatsStore.systemStats = null - mockComfyManagerService.listInstalledPacks.mockResolvedValue({}) - mockRegistryService.getPackByVersion.mockImplementation( - (packageName: string) => { - const packageData = [].find((p: any) => p.id === packageName) - return Promise.resolve(packageData || null) - } - ) - - const { performConflictDetection } = useConflictDetection() - const result = await performConflictDetection() - - expect(result.success).toBe(true) - expect(result.detected_system_environment?.comfyui_version).toBe( - 'unknown' - ) - }) - - it('should continue system operation even when local package information fails', async () => { - // Mock local package service failure - mockComfyManagerService.listInstalledPacks.mockRejectedValue( - new Error('Service error') - ) - - const { performConflictDetection } = useConflictDetection() - const result = await performConflictDetection() - - expect(result.success).toBe(true) - expect(result.summary.total_packages).toBe(0) - }) - - it('should handle Registry Store partial data gracefully', async () => { - // Mock successful local data but partial Registry data - const mockInstalledPacks: InstalledPacksResponse = { - 'Package-A': { - ver: '1.0.0', - cnr_id: 'a', - aux_id: null, - enabled: true - }, - 'Package-B': { - ver: '2.0.0', - cnr_id: 'b', - aux_id: null, - enabled: true - } - } - - mockComfyManagerService.listInstalledPacks.mockResolvedValue( - mockInstalledPacks - ) - - // Only first package found in Registry / Registry에서 첫 번째 패키지만 찾음 - const mockPartialRegistryPacks: components['schemas']['Node'][] = [ - { - id: 'Package-A', - name: 'Package A', - supported_os: ['windows', 'linux', 'macos'], - supported_accelerators: ['Metal', 'CUDA', 'CPU'], - status: 'NodeStatusActive' - } as components['schemas']['Node'] - // Package-B is missing from Registry results - ] - - mockRegistryService.getPackByVersion.mockImplementation( - (packageName: string) => { - const packageData = mockPartialRegistryPacks.find( - (p: any) => p.id === packageName - ) - return Promise.resolve(packageData || null) - } - ) - - const { performConflictDetection } = useConflictDetection() - const result = await performConflictDetection() - - expect(result.success).toBe(true) - expect(result.summary.total_packages).toBeGreaterThanOrEqual(1) - - // Check that packages were processed - expect(result.results.length).toBeGreaterThan(0) - - // If packages exist, verify they have proper structure - if (result.results.length > 0) { - for (const pkg of result.results) { - expect(pkg).toHaveProperty('package_id') - expect(pkg).toHaveProperty('conflicts') - expect(Array.isArray(pkg.conflicts)).toBe(true) - } - } - }) - - it('should handle complete system failure gracefully', async () => { - // Mock all stores/services failing - mockSystemStatsStore.refetchSystemStats.mockRejectedValue( - new Error('Critical error') - ) - mockSystemStatsStore.systemStats = null - mockComfyManagerService.listInstalledPacks.mockRejectedValue( - new Error('Critical error') - ) - mockRegistryService.getPackByVersion.mockRejectedValue( - new Error('Critical error') - ) - - const { performConflictDetection } = useConflictDetection() - const result = await performConflictDetection() - - expect(result.success).toBe(true) // Error resilience maintains success - expect(result.summary.total_packages).toBe(0) - }) - }) - - describe('acknowledgment integration', () => { - it('should check ComfyUI version change during conflict detection', async () => { - mockComfyManagerService.listInstalledPacks.mockResolvedValue({ - TestNode: { - ver: '1.0.0', - cnr_id: 'test-node', - aux_id: null, - enabled: true - } + vi.mocked(mockRegistryService.getBulkNodeVersions).mockResolvedValue({ + node_versions: [ + { + status: 'success' as const, + identifier: { node_id: 'banned-pack', version: '1.0.0' }, + node_version: { + status: 'NodeVersionStatusBanned' as const, + version: '1.0.0', + publisher_id: 'test-publisher', + node_id: 'banned-pack', + created_at: '2024-01-01T00:00:00Z', + supported_comfyui_version: undefined, + supported_comfyui_frontend_version: undefined, + supported_os: undefined, + supported_accelerators: undefined + } as components['schemas']['NodeVersion'] + } + ] }) - mockRegistryService.getPackByVersion.mockResolvedValue({ - id: 'TestNode', - supported_os: ['Windows'], - supported_accelerators: ['CUDA'], - supported_comfyui_version: '>=0.3.0', - status: 'NodeVersionStatusActive' + const { runFullConflictAnalysis } = useConflictDetection() + const result = await runFullConflictAnalysis() + + expect(result.results[0].conflicts).toContainEqual({ + type: 'banned', + current_value: 'installed', + required_value: 'not_banned' }) - - const { performConflictDetection } = useConflictDetection() - await performConflictDetection() - - expect(mockAcknowledgment.checkComfyUIVersionChange).toHaveBeenCalledWith( - '0.3.41' - ) }) - it('should expose conflict modal display method', () => { - const { shouldShowConflictModalAfterUpdate } = useConflictDetection() - - expect(shouldShowConflictModalAfterUpdate).toBeDefined() - }) - - it('should determine conflict modal display after update correctly', async () => { - const { shouldShowConflictModalAfterUpdate } = useConflictDetection() - - // With no conflicts initially, should return false - const result = await shouldShowConflictModalAfterUpdate() - expect(result).toBe(false) // No conflicts initially - }) - - it('should show conflict modal after update when conflicts exist', async () => { - // Mock package with conflicts - const mockInstalledPacks: InstalledPacksResponse = { - ConflictedNode: { - ver: '1.0.0', - cnr_id: 'conflicted-node', - aux_id: null, - enabled: true - } - } - - const mockConflictedRegistryPacks: components['schemas']['Node'][] = [ + it('should detect import failures', async () => { + mockInstalledPacks.isReady.value = true + mockInstalledPacksWithVersions.value = [ { - id: 'ConflictedNode', - name: 'Conflicted Node', - supported_os: ['Windows'], // Will conflict with macOS - supported_accelerators: ['Metal', 'CUDA', 'CPU'], - supported_comfyui_version: '>=0.3.0', - status: 'NodeStatusActive' - } as components['schemas']['Node'] + id: 'fail-pack', + version: '1.0.0' + } ] - mockComfyManagerService.listInstalledPacks.mockResolvedValue( - mockInstalledPacks - ) - mockRegistryService.getPackByVersion.mockImplementation( - (packageName: string) => { - const packageData = mockConflictedRegistryPacks.find( - (p: any) => p.id === packageName - ) - return Promise.resolve(packageData || null) - } - ) + vi.mocked( + mockComfyManagerService.getImportFailInfoBulk + ).mockResolvedValue({ + 'fail-pack': { + msg: 'Import error', + name: 'fail-pack', + path: '/path/to/pack' + } as any // The actual API returns different structure than types + }) - const { shouldShowConflictModalAfterUpdate, performConflictDetection } = - useConflictDetection() + // Mock registry response for the package + vi.mocked(mockRegistryService.getBulkNodeVersions).mockResolvedValue({ + node_versions: [] + }) - // First run conflict detection to populate conflicts - await performConflictDetection() - await nextTick() + const { runFullConflictAnalysis } = useConflictDetection() + const result = await runFullConflictAnalysis() - // Now check if modal should show after update - const result = await shouldShowConflictModalAfterUpdate() - expect(result).toBe(true) // Should show modal when conflicts exist and not dismissed + expect(result.results).toHaveLength(1) + // Import failure should match the actual implementation + expect(result.results[0].conflicts).toContainEqual({ + type: 'import_failed', + current_value: 'installed', + required_value: 'Import error' + }) }) + }) - it('should detect system environment correctly', async () => { - // Mock system environment - mockSystemStatsStore.systemStats = { - system: { - comfyui_version: '0.3.41', - os: 'Darwin' - }, - devices: [] - } + describe('computed properties', () => { + it('should expose conflict status from store', () => { + mockConflictedPackages.value = [ + { + package_id: 'test', + package_name: 'Test', + has_conflict: true, + is_compatible: false, + conflicts: [] + } + ] - const { detectSystemEnvironment } = useConflictDetection() + useConflictDetection() - // Detect system environment - const environment = await detectSystemEnvironment() - - expect(environment.comfyui_version).toBe('0.3.41') + // The hasConflicts computed should be true since we have a conflict + expect(mockConflictedPackages.value).toHaveLength(1) + expect(mockConflictedPackages.value[0].has_conflict).toBe(true) }) }) describe('initialization', () => { - it('should execute initializeConflictDetection without errors', async () => { - mockComfyManagerService.listInstalledPacks.mockResolvedValue({}) + it('should initialize without errors', async () => { + // Mock that installed packs are ready + mockInstalledPacks.isReady.value = true + mockInstalledPacksWithVersions.value = [] + + // Ensure startFetchInstalled resolves + vi.mocked(mockInstalledPacks.startFetchInstalled).mockResolvedValue( + undefined + ) const { initializeConflictDetection } = useConflictDetection() - await expect(initializeConflictDetection()).resolves.not.toThrow() - }) - - it('should set initial state values correctly', () => { - const { - isDetecting, - lastDetectionTime, - detectionError, - systemEnvironment, - detectionResults, - detectionSummary - } = useConflictDetection() - - expect(isDetecting.value).toBe(false) - expect(lastDetectionTime.value).toBeNull() - expect(detectionError.value).toBeNull() - expect(systemEnvironment.value).toBeNull() - expect(detectionResults.value).toEqual([]) - expect(detectionSummary.value).toBeNull() + // Set a timeout to prevent hanging + await expect( + Promise.race([ + initializeConflictDetection(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), 1000) + ) + ]) + ).resolves.not.toThrow() }) }) }) diff --git a/tests-ui/tests/composables/useImportFailedDetection.test.ts b/tests-ui/tests/composables/useImportFailedDetection.test.ts index 3e60072e1..5a6614ca0 100644 --- a/tests-ui/tests/composables/useImportFailedDetection.test.ts +++ b/tests-ui/tests/composables/useImportFailedDetection.test.ts @@ -22,22 +22,30 @@ vi.mock('vue-i18n', async (importOriginal) => { }) describe('useImportFailedDetection', () => { - let mockComfyManagerStore: any - let mockConflictDetectionStore: any - let mockDialogService: any + let mockComfyManagerStore: ReturnType< + typeof comfyManagerStore.useComfyManagerStore + > + let mockConflictDetectionStore: ReturnType< + typeof conflictDetectionStore.useConflictDetectionStore + > + let mockDialogService: ReturnType beforeEach(() => { setActivePinia(createPinia()) mockComfyManagerStore = { isPackInstalled: vi.fn() - } + } as unknown as ReturnType + mockConflictDetectionStore = { getConflictsForPackageByID: vi.fn() - } + } as unknown as ReturnType< + typeof conflictDetectionStore.useConflictDetectionStore + > + mockDialogService = { showErrorDialog: vi.fn() - } + } as unknown as ReturnType vi.mocked(comfyManagerStore.useComfyManagerStore).mockReturnValue( mockComfyManagerStore @@ -49,7 +57,7 @@ describe('useImportFailedDetection', () => { }) it('should return false for importFailed when package is not installed', () => { - mockComfyManagerStore.isPackInstalled.mockReturnValue(false) + vi.mocked(mockComfyManagerStore.isPackInstalled).mockReturnValue(false) const { importFailed } = useImportFailedDetection('test-package') @@ -57,8 +65,10 @@ describe('useImportFailedDetection', () => { }) it('should return false for importFailed when no conflicts exist', () => { - mockComfyManagerStore.isPackInstalled.mockReturnValue(true) - mockConflictDetectionStore.getConflictsForPackageByID.mockReturnValue(null) + vi.mocked(mockComfyManagerStore.isPackInstalled).mockReturnValue(true) + vi.mocked( + mockConflictDetectionStore.getConflictsForPackageByID + ).mockReturnValue(undefined) const { importFailed } = useImportFailedDetection('test-package') @@ -66,12 +76,25 @@ describe('useImportFailedDetection', () => { }) it('should return false for importFailed when conflicts exist but no import_failed type', () => { - mockComfyManagerStore.isPackInstalled.mockReturnValue(true) - mockConflictDetectionStore.getConflictsForPackageByID.mockReturnValue({ + vi.mocked(mockComfyManagerStore.isPackInstalled).mockReturnValue(true) + vi.mocked( + mockConflictDetectionStore.getConflictsForPackageByID + ).mockReturnValue({ package_id: 'test-package', + package_name: 'Test Package', + has_conflict: true, + is_compatible: false, conflicts: [ - { type: 'dependency', message: 'Dependency conflict' }, - { type: 'version', message: 'Version conflict' } + { + type: 'comfyui_version', + current_value: 'current', + required_value: 'required' + }, + { + type: 'frontend_version', + current_value: 'current', + required_value: 'required' + } ] }) @@ -81,16 +104,25 @@ describe('useImportFailedDetection', () => { }) it('should return true for importFailed when import_failed conflicts exist', () => { - mockComfyManagerStore.isPackInstalled.mockReturnValue(true) - mockConflictDetectionStore.getConflictsForPackageByID.mockReturnValue({ + vi.mocked(mockComfyManagerStore.isPackInstalled).mockReturnValue(true) + vi.mocked( + mockConflictDetectionStore.getConflictsForPackageByID + ).mockReturnValue({ package_id: 'test-package', + package_name: 'Test Package', + has_conflict: true, + is_compatible: false, conflicts: [ { type: 'import_failed', - message: 'Import failed', + current_value: 'current', required_value: 'Error details' }, - { type: 'dependency', message: 'Dependency conflict' } + { + type: 'comfyui_version', + current_value: 'current', + required_value: 'required' + } ] }) @@ -101,13 +133,18 @@ describe('useImportFailedDetection', () => { it('should work with computed ref packageId', () => { const packageId = ref('test-package') - mockComfyManagerStore.isPackInstalled.mockReturnValue(true) - mockConflictDetectionStore.getConflictsForPackageByID.mockReturnValue({ + vi.mocked(mockComfyManagerStore.isPackInstalled).mockReturnValue(true) + vi.mocked( + mockConflictDetectionStore.getConflictsForPackageByID + ).mockReturnValue({ package_id: 'test-package', + package_name: 'Test Package', + has_conflict: true, + is_compatible: false, conflicts: [ { type: 'import_failed', - message: 'Import failed', + current_value: 'current', required_value: 'Error details' } ] @@ -121,7 +158,9 @@ describe('useImportFailedDetection', () => { // Change packageId packageId.value = 'another-package' - mockConflictDetectionStore.getConflictsForPackageByID.mockReturnValue(null) + vi.mocked( + mockConflictDetectionStore.getConflictsForPackageByID + ).mockReturnValue(undefined) expect(importFailed.value).toBe(false) }) @@ -129,23 +168,32 @@ describe('useImportFailedDetection', () => { it('should return correct importFailedInfo', () => { const importFailedConflicts = [ { - type: 'import_failed', - message: 'Import failed 1', + type: 'import_failed' as const, + current_value: 'current', required_value: 'Error 1' }, { - type: 'import_failed', - message: 'Import failed 2', + type: 'import_failed' as const, + current_value: 'current', required_value: 'Error 2' } ] - mockComfyManagerStore.isPackInstalled.mockReturnValue(true) - mockConflictDetectionStore.getConflictsForPackageByID.mockReturnValue({ + vi.mocked(mockComfyManagerStore.isPackInstalled).mockReturnValue(true) + vi.mocked( + mockConflictDetectionStore.getConflictsForPackageByID + ).mockReturnValue({ package_id: 'test-package', + package_name: 'Test Package', + has_conflict: true, + is_compatible: false, conflicts: [ ...importFailedConflicts, - { type: 'dependency', message: 'Dependency conflict' } + { + type: 'comfyui_version', + current_value: 'current', + required_value: 'required' + } ] }) @@ -157,15 +205,20 @@ describe('useImportFailedDetection', () => { it('should show error dialog when showImportFailedDialog is called', () => { const importFailedConflicts = [ { - type: 'import_failed', - message: 'Import failed', + type: 'import_failed' as const, + current_value: 'current', required_value: 'Error details' } ] - mockComfyManagerStore.isPackInstalled.mockReturnValue(true) - mockConflictDetectionStore.getConflictsForPackageByID.mockReturnValue({ + vi.mocked(mockComfyManagerStore.isPackInstalled).mockReturnValue(true) + vi.mocked( + mockConflictDetectionStore.getConflictsForPackageByID + ).mockReturnValue({ package_id: 'test-package', + package_name: 'Test Package', + has_conflict: true, + is_compatible: false, conflicts: importFailedConflicts }) diff --git a/tests-ui/tests/composables/useUpdateAvailableNodes.test.ts b/tests-ui/tests/composables/useUpdateAvailableNodes.test.ts index 4a78ad99e..c15b96ef6 100644 --- a/tests-ui/tests/composables/useUpdateAvailableNodes.test.ts +++ b/tests-ui/tests/composables/useUpdateAvailableNodes.test.ts @@ -2,6 +2,7 @@ import { compare, valid } from 'semver' import { beforeEach, describe, expect, it, vi } from 'vitest' import { nextTick, ref } from 'vue' +import type { components } from '@/types/comfyRegistryTypes' import { useInstalledPacks } from '@/workbench/extensions/manager/composables/nodePack/useInstalledPacks' import { useUpdateAvailableNodes } from '@/workbench/extensions/manager/composables/nodePack/useUpdateAvailableNodes' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' @@ -44,22 +45,22 @@ describe('useUpdateAvailableNodes', () => { id: 'pack-1', name: 'Outdated Pack', latest_version: { version: '2.0.0' } - }, + } as components['schemas']['Node'], { id: 'pack-2', name: 'Up to Date Pack', latest_version: { version: '1.0.0' } - }, + } as components['schemas']['Node'], { id: 'pack-3', name: 'Nightly Pack', latest_version: { version: '1.5.0' } - }, + } as components['schemas']['Node'], { id: 'pack-4', name: 'No Latest Version', - latest_version: null - } + latest_version: undefined + } as components['schemas']['Node'] ] const mockStartFetchInstalled = vi.fn() @@ -106,14 +107,17 @@ describe('useUpdateAvailableNodes', () => { isPackInstalled: mockIsPackInstalled, getInstalledPackVersion: mockGetInstalledPackVersion, isPackEnabled: mockIsPackEnabled - } as any) + } as unknown as ReturnType) mockUseInstalledPacks.mockReturnValue({ installedPacks: ref([]), isLoading: ref(false), + isReady: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) }) describe('core filtering logic', () => { @@ -121,9 +125,12 @@ describe('useUpdateAvailableNodes', () => { mockUseInstalledPacks.mockReturnValue({ installedPacks: ref(mockInstalledPacks), isLoading: ref(false), + isReady: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { updateAvailableNodePacks } = useUpdateAvailableNodes() @@ -136,9 +143,12 @@ describe('useUpdateAvailableNodes', () => { mockUseInstalledPacks.mockReturnValue({ installedPacks: ref([mockInstalledPacks[1]]), // pack-2: up to date isLoading: ref(false), + isReady: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { updateAvailableNodePacks } = useUpdateAvailableNodes() @@ -149,9 +159,12 @@ describe('useUpdateAvailableNodes', () => { mockUseInstalledPacks.mockReturnValue({ installedPacks: ref([mockInstalledPacks[2]]), // pack-3: nightly isLoading: ref(false), + isReady: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { updateAvailableNodePacks } = useUpdateAvailableNodes() @@ -162,9 +175,12 @@ describe('useUpdateAvailableNodes', () => { mockUseInstalledPacks.mockReturnValue({ installedPacks: ref([mockInstalledPacks[3]]), // pack-4: no latest version isLoading: ref(false), + isReady: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { updateAvailableNodePacks } = useUpdateAvailableNodes() @@ -176,9 +192,12 @@ describe('useUpdateAvailableNodes', () => { mockUseInstalledPacks.mockReturnValue({ installedPacks: ref(mockInstalledPacks), isLoading: ref(false), + isReady: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { updateAvailableNodePacks } = useUpdateAvailableNodes() @@ -198,8 +217,11 @@ describe('useUpdateAvailableNodes', () => { installedPacks: ref([mockInstalledPacks[0]]), // pack-1: outdated isLoading: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + isReady: ref(false), + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { hasUpdateAvailable } = useUpdateAvailableNodes() @@ -210,9 +232,12 @@ describe('useUpdateAvailableNodes', () => { mockUseInstalledPacks.mockReturnValue({ installedPacks: ref([mockInstalledPacks[1]]), // pack-2: up to date isLoading: ref(false), + isReady: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { hasUpdateAvailable } = useUpdateAvailableNodes() @@ -231,9 +256,12 @@ describe('useUpdateAvailableNodes', () => { mockUseInstalledPacks.mockReturnValue({ installedPacks: ref(mockInstalledPacks), isLoading: ref(false), + isReady: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) useUpdateAvailableNodes() @@ -245,8 +273,11 @@ describe('useUpdateAvailableNodes', () => { installedPacks: ref([]), isLoading: ref(true), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + isReady: ref(false), + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) useUpdateAvailableNodes() @@ -260,8 +291,11 @@ describe('useUpdateAvailableNodes', () => { installedPacks: ref([]), isLoading: ref(true), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + isReady: ref(false), + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { isLoading } = useUpdateAvailableNodes() @@ -274,8 +308,11 @@ describe('useUpdateAvailableNodes', () => { installedPacks: ref([]), isLoading: ref(false), error: ref(testError), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + isReady: ref(false), + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { error } = useUpdateAvailableNodes() @@ -285,13 +322,16 @@ describe('useUpdateAvailableNodes', () => { describe('reactivity', () => { it('updates when installed packs change', async () => { - const installedPacksRef = ref([]) + const installedPacksRef = ref([]) mockUseInstalledPacks.mockReturnValue({ installedPacks: installedPacksRef, isLoading: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + isReady: ref(false), + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { updateAvailableNodePacks, hasUpdateAvailable } = useUpdateAvailableNodes() @@ -301,7 +341,7 @@ describe('useUpdateAvailableNodes', () => { expect(hasUpdateAvailable.value).toBe(false) // Update installed packs - installedPacksRef.value = [mockInstalledPacks[0]] as any // pack-1: outdated + installedPacksRef.value = [mockInstalledPacks[0]] await nextTick() // Should update available updates @@ -316,8 +356,11 @@ describe('useUpdateAvailableNodes', () => { installedPacks: ref([mockInstalledPacks[0]]), // pack-1 isLoading: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + isReady: ref(false), + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { updateAvailableNodePacks } = useUpdateAvailableNodes() @@ -331,9 +374,12 @@ describe('useUpdateAvailableNodes', () => { mockUseInstalledPacks.mockReturnValue({ installedPacks: ref([mockInstalledPacks[2]]), // pack-3: nightly isLoading: ref(false), + isReady: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { updateAvailableNodePacks } = useUpdateAvailableNodes() @@ -347,9 +393,12 @@ describe('useUpdateAvailableNodes', () => { mockUseInstalledPacks.mockReturnValue({ installedPacks: ref(mockInstalledPacks), isLoading: ref(false), + isReady: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { updateAvailableNodePacks } = useUpdateAvailableNodes() @@ -374,8 +423,11 @@ describe('useUpdateAvailableNodes', () => { installedPacks: ref([mockInstalledPacks[0], mockInstalledPacks[1]]), isLoading: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + isReady: ref(false), + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { updateAvailableNodePacks, enabledUpdateAvailableNodePacks } = useUpdateAvailableNodes() @@ -393,8 +445,11 @@ describe('useUpdateAvailableNodes', () => { installedPacks: ref([mockInstalledPacks[0]]), // pack-1: outdated isLoading: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + isReady: ref(false), + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { updateAvailableNodePacks, enabledUpdateAvailableNodePacks } = useUpdateAvailableNodes() @@ -416,8 +471,11 @@ describe('useUpdateAvailableNodes', () => { installedPacks: ref([mockInstalledPacks[0]]), // pack-1: outdated isLoading: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + isReady: ref(false), + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { hasDisabledUpdatePacks } = useUpdateAvailableNodes() @@ -429,8 +487,11 @@ describe('useUpdateAvailableNodes', () => { installedPacks: ref([mockInstalledPacks[0]]), // pack-1: outdated isLoading: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + isReady: ref(false), + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { hasDisabledUpdatePacks } = useUpdateAvailableNodes() @@ -441,9 +502,12 @@ describe('useUpdateAvailableNodes', () => { mockUseInstalledPacks.mockReturnValue({ installedPacks: ref([mockInstalledPacks[1]]), // pack-2: up to date isLoading: ref(false), + isReady: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { hasDisabledUpdatePacks } = useUpdateAvailableNodes() @@ -459,8 +523,11 @@ describe('useUpdateAvailableNodes', () => { installedPacks: ref([mockInstalledPacks[0]]), // pack-1: outdated isLoading: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + isReady: ref(false), + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { hasUpdateAvailable } = useUpdateAvailableNodes() @@ -477,8 +544,11 @@ describe('useUpdateAvailableNodes', () => { installedPacks: ref([mockInstalledPacks[0]]), // pack-1: outdated isLoading: ref(false), error: ref(null), - startFetchInstalled: mockStartFetchInstalled - } as any) + startFetchInstalled: mockStartFetchInstalled, + isReady: ref(false), + installedPacksWithVersions: ref([]), + filterInstalledPack: vi.fn() + } as unknown as ReturnType) const { hasUpdateAvailable } = useUpdateAvailableNodes() diff --git a/tests-ui/tests/stores/conflictDetectionStore.test.ts b/tests-ui/tests/stores/conflictDetectionStore.test.ts index 877e6d48c..7c74ea6c1 100644 --- a/tests-ui/tests/stores/conflictDetectionStore.test.ts +++ b/tests-ui/tests/stores/conflictDetectionStore.test.ts @@ -1,8 +1,8 @@ import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, it } from 'vitest' -import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes' import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore' +import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes' describe('useConflictDetectionStore', () => { beforeEach(() => { diff --git a/tests-ui/tests/utils/conflictUtils.test.ts b/tests-ui/tests/utils/conflictUtils.test.ts new file mode 100644 index 000000000..876684378 --- /dev/null +++ b/tests-ui/tests/utils/conflictUtils.test.ts @@ -0,0 +1,207 @@ +import { describe, expect, it } from 'vitest' + +import type { + ConflictDetail, + ConflictDetectionResult +} from '@/workbench/extensions/manager/types/conflictDetectionTypes' +import { + consolidateConflictsByPackage, + createBannedConflict, + createPendingConflict +} from '@/workbench/extensions/manager/utils/conflictUtils' + +describe('conflictUtils', () => { + describe('createBannedConflict', () => { + it('should return banned conflict when isBanned is true', () => { + const result = createBannedConflict(true) + expect(result).toEqual({ + type: 'banned', + current_value: 'installed', + required_value: 'not_banned' + }) + }) + + it('should return null when isBanned is false', () => { + const result = createBannedConflict(false) + expect(result).toBeNull() + }) + + it('should return null when isBanned is undefined', () => { + const result = createBannedConflict(undefined) + expect(result).toBeNull() + }) + }) + + describe('createPendingConflict', () => { + it('should return pending conflict when isPending is true', () => { + const result = createPendingConflict(true) + expect(result).toEqual({ + type: 'pending', + current_value: 'installed', + required_value: 'not_pending' + }) + }) + + it('should return null when isPending is false', () => { + const result = createPendingConflict(false) + expect(result).toBeNull() + }) + + it('should return null when isPending is undefined', () => { + const result = createPendingConflict(undefined) + expect(result).toBeNull() + }) + }) + + describe('consolidateConflictsByPackage', () => { + it('should group conflicts by normalized package name', () => { + const conflicts: ConflictDetectionResult[] = [ + { + package_name: 'mypack@1_0_3', + package_id: 'mypack@1_0_3', + conflicts: [ + { type: 'os', current_value: 'Windows', required_value: 'Linux' } + ], + has_conflict: true, + is_compatible: false + }, + { + package_name: 'mypack', + package_id: 'mypack', + conflicts: [ + { + type: 'comfyui_version', + current_value: '1.0.0', + required_value: '>=2.0.0' + } + ], + has_conflict: true, + is_compatible: false + } + ] + + const result = consolidateConflictsByPackage(conflicts) + + expect(result).toHaveLength(1) + expect(result[0].package_name).toBe('mypack') + expect(result[0].conflicts).toHaveLength(2) + expect(result[0].has_conflict).toBe(true) + expect(result[0].is_compatible).toBe(false) + }) + + it('should deduplicate identical conflicts', () => { + const duplicateConflict: ConflictDetail = { + type: 'os', + current_value: 'Windows', + required_value: 'Linux' + } + + const conflicts: ConflictDetectionResult[] = [ + { + package_name: 'pack', + package_id: 'pack', + conflicts: [duplicateConflict], + has_conflict: true, + is_compatible: false + }, + { + package_name: 'pack@version', + package_id: 'pack@version', + conflicts: [duplicateConflict], + has_conflict: true, + is_compatible: false + } + ] + + const result = consolidateConflictsByPackage(conflicts) + + expect(result).toHaveLength(1) + expect(result[0].conflicts).toHaveLength(1) + }) + + it('should handle packages without conflicts', () => { + const conflicts: ConflictDetectionResult[] = [ + { + package_name: 'compatible-pack', + package_id: 'compatible-pack', + conflicts: [], + has_conflict: false, + is_compatible: true + } + ] + + const result = consolidateConflictsByPackage(conflicts) + + expect(result).toHaveLength(1) + expect(result[0].conflicts).toHaveLength(0) + expect(result[0].has_conflict).toBe(false) + expect(result[0].is_compatible).toBe(true) + }) + + it('should handle empty input', () => { + const result = consolidateConflictsByPackage([]) + expect(result).toEqual([]) + }) + + it('should merge conflicts from multiple versions of same package', () => { + const conflicts: ConflictDetectionResult[] = [ + { + package_name: 'mynode@1_0_0', + package_id: 'mynode@1_0_0', + conflicts: [ + { type: 'os', current_value: 'Windows', required_value: 'Linux' } + ], + has_conflict: true, + is_compatible: false + }, + { + package_name: 'mynode@2_0_0', + package_id: 'mynode@2_0_0', + conflicts: [ + { + type: 'accelerator', + current_value: 'CPU', + required_value: 'CUDA' + } + ], + has_conflict: true, + is_compatible: false + }, + { + package_name: 'mynode', + package_id: 'mynode', + conflicts: [ + { + type: 'comfyui_version', + current_value: '1.0.0', + required_value: '>=2.0.0' + } + ], + has_conflict: true, + is_compatible: false + } + ] + + const result = consolidateConflictsByPackage(conflicts) + + expect(result).toHaveLength(1) + expect(result[0].package_name).toBe('mynode') + expect(result[0].conflicts).toHaveLength(3) + expect(result[0].conflicts).toContainEqual({ + type: 'os', + current_value: 'Windows', + required_value: 'Linux' + }) + expect(result[0].conflicts).toContainEqual({ + type: 'accelerator', + current_value: 'CPU', + required_value: 'CUDA' + }) + expect(result[0].conflicts).toContainEqual({ + type: 'comfyui_version', + current_value: '1.0.0', + required_value: '>=2.0.0' + }) + }) + }) +}) diff --git a/tests-ui/tests/utils/systemCompatibility.test.ts b/tests-ui/tests/utils/systemCompatibility.test.ts new file mode 100644 index 000000000..0e34cc7eb --- /dev/null +++ b/tests-ui/tests/utils/systemCompatibility.test.ts @@ -0,0 +1,270 @@ +import { describe, expect, it } from 'vitest' + +import type { + RegistryAccelerator, + RegistryOS +} from '@/workbench/extensions/manager/types/compatibility.types' +import { + checkAcceleratorCompatibility, + checkOSCompatibility, + normalizeOSList +} from '@/workbench/extensions/manager/utils/systemCompatibility' + +describe('systemCompatibility', () => { + describe('checkOSCompatibility', () => { + it('should return null when supported OS list is null', () => { + const result = checkOSCompatibility(null, 'darwin') + expect(result).toBeNull() + }) + + it('should return null when supported OS list is undefined', () => { + const result = checkOSCompatibility(undefined, 'darwin') + expect(result).toBeNull() + }) + + it('should return null when supported OS list is empty', () => { + const result = checkOSCompatibility([], 'darwin') + expect(result).toBeNull() + }) + + it('should return null when OS is compatible (macOS)', () => { + const supported: RegistryOS[] = ['macOS', 'Windows'] + const result = checkOSCompatibility(supported, 'darwin') + expect(result).toBeNull() + }) + + it('should return null when OS is compatible (Windows)', () => { + const supported: RegistryOS[] = ['Windows', 'Linux'] + const result = checkOSCompatibility(supported, 'win32') + expect(result).toBeNull() + }) + + it('should return null when OS is compatible (Linux)', () => { + const supported: RegistryOS[] = ['Linux', 'macOS'] + const result = checkOSCompatibility(supported, 'linux') + expect(result).toBeNull() + }) + + it('should return conflict when OS is incompatible', () => { + const supported: RegistryOS[] = ['Windows'] + const result = checkOSCompatibility(supported, 'darwin') + expect(result).toEqual({ + type: 'os', + current_value: 'macOS', + required_value: 'Windows' + }) + }) + + it('should return conflict with Unknown OS when current OS is unrecognized', () => { + const supported: RegistryOS[] = ['Windows', 'Linux'] + const result = checkOSCompatibility(supported, 'freebsd') + expect(result).toEqual({ + type: 'os', + current_value: 'Unknown', + required_value: 'Windows, Linux' + }) + }) + + it('should handle various OS string formats', () => { + const supported: RegistryOS[] = ['Windows'] + + // Test Windows variations + expect(checkOSCompatibility(supported, 'win32')).toBeNull() + expect(checkOSCompatibility(supported, 'windows')).toBeNull() + expect(checkOSCompatibility(supported, 'Windows_NT')).toBeNull() + + // Test macOS variations + const macSupported: RegistryOS[] = ['macOS'] + expect(checkOSCompatibility(macSupported, 'darwin')).toBeNull() + expect(checkOSCompatibility(macSupported, 'Darwin')).toBeNull() + expect(checkOSCompatibility(macSupported, 'macos')).toBeNull() + expect(checkOSCompatibility(macSupported, 'mac')).toBeNull() + }) + + it('should handle undefined current OS', () => { + const supported: RegistryOS[] = ['Windows'] + const result = checkOSCompatibility(supported, undefined) + expect(result).toEqual({ + type: 'os', + current_value: 'Unknown', + required_value: 'Windows' + }) + }) + }) + + describe('checkAcceleratorCompatibility', () => { + it('should return null when supported accelerator list is null', () => { + const result = checkAcceleratorCompatibility(null, 'cuda') + expect(result).toBeNull() + }) + + it('should return null when supported accelerator list is undefined', () => { + const result = checkAcceleratorCompatibility(undefined, 'cuda') + expect(result).toBeNull() + }) + + it('should return null when supported accelerator list is empty', () => { + const result = checkAcceleratorCompatibility([], 'cuda') + expect(result).toBeNull() + }) + + it('should return null when accelerator is compatible (CUDA)', () => { + const supported: RegistryAccelerator[] = ['CUDA', 'CPU'] + const result = checkAcceleratorCompatibility(supported, 'cuda') + expect(result).toBeNull() + }) + + it('should return null when accelerator is compatible (Metal)', () => { + const supported: RegistryAccelerator[] = ['Metal', 'CPU'] + const result = checkAcceleratorCompatibility(supported, 'mps') + expect(result).toBeNull() + }) + + it('should return null when accelerator is compatible (ROCm)', () => { + const supported: RegistryAccelerator[] = ['ROCm', 'CPU'] + const result = checkAcceleratorCompatibility(supported, 'rocm') + expect(result).toBeNull() + }) + + it('should return null when accelerator is compatible (CPU)', () => { + const supported: RegistryAccelerator[] = ['CPU'] + const result = checkAcceleratorCompatibility(supported, 'cpu') + expect(result).toBeNull() + }) + + it('should return conflict when accelerator is incompatible', () => { + const supported: RegistryAccelerator[] = ['CUDA'] + const result = checkAcceleratorCompatibility(supported, 'mps') + expect(result).toEqual({ + type: 'accelerator', + current_value: 'Metal', + required_value: 'CUDA' + }) + }) + + it('should default to CPU for unknown device types', () => { + const supported: RegistryAccelerator[] = ['CUDA'] + const result = checkAcceleratorCompatibility(supported, 'unknown') + expect(result).toEqual({ + type: 'accelerator', + current_value: 'CPU', + required_value: 'CUDA' + }) + }) + + it('should default to CPU when device type is undefined', () => { + const supported: RegistryAccelerator[] = ['CUDA'] + const result = checkAcceleratorCompatibility(supported, undefined) + expect(result).toEqual({ + type: 'accelerator', + current_value: 'CPU', + required_value: 'CUDA' + }) + }) + + it('should handle case-insensitive device types', () => { + const supported: RegistryAccelerator[] = ['CUDA'] + + // CUDA variations + expect(checkAcceleratorCompatibility(supported, 'cuda')).toBeNull() + expect(checkAcceleratorCompatibility(supported, 'CUDA')).toBeNull() + expect(checkAcceleratorCompatibility(supported, 'Cuda')).toBeNull() + + // Metal variations + const metalSupported: RegistryAccelerator[] = ['Metal'] + expect(checkAcceleratorCompatibility(metalSupported, 'mps')).toBeNull() + expect(checkAcceleratorCompatibility(metalSupported, 'MPS')).toBeNull() + + // ROCm variations + const rocmSupported: RegistryAccelerator[] = ['ROCm'] + expect(checkAcceleratorCompatibility(rocmSupported, 'rocm')).toBeNull() + expect(checkAcceleratorCompatibility(rocmSupported, 'ROCM')).toBeNull() + }) + + it('should handle multiple required accelerators', () => { + const supported: RegistryAccelerator[] = ['CUDA', 'ROCm'] + const result = checkAcceleratorCompatibility(supported, 'mps') + expect(result).toEqual({ + type: 'accelerator', + current_value: 'Metal', + required_value: 'CUDA, ROCm' + }) + }) + }) + + describe('normalizeOSList', () => { + it('should return undefined for null input', () => { + const result = normalizeOSList(null) + expect(result).toBeUndefined() + }) + + it('should return undefined for undefined input', () => { + const result = normalizeOSList(undefined) + expect(result).toBeUndefined() + }) + + it('should return undefined for empty array', () => { + const result = normalizeOSList([]) + expect(result).toBeUndefined() + }) + + it('should return undefined when OS Independent is present', () => { + const result = normalizeOSList(['OS Independent', 'Windows']) + expect(result).toBeUndefined() + }) + + it('should return undefined for case-insensitive OS Independent', () => { + const result = normalizeOSList(['os independent']) + expect(result).toBeUndefined() + }) + + it('should filter and return valid OS values', () => { + const result = normalizeOSList(['Windows', 'Linux', 'macOS']) + expect(result).toEqual(['Windows', 'Linux', 'macOS']) + }) + + it('should filter out invalid OS values', () => { + const result = normalizeOSList(['Windows', 'FreeBSD', 'Linux', 'Android']) + expect(result).toEqual(['Windows', 'Linux']) + }) + + it('should deduplicate OS values', () => { + const result = normalizeOSList([ + 'Windows', + 'Linux', + 'Windows', + 'macOS', + 'Linux' + ]) + expect(result).toEqual(['Windows', 'Linux', 'macOS']) + }) + + it('should return undefined when no valid OS values remain', () => { + const result = normalizeOSList(['FreeBSD', 'Android', 'iOS']) + expect(result).toBeUndefined() + }) + + it('should handle mixed valid and invalid values', () => { + const result = normalizeOSList([ + 'windows', + 'Windows', + 'linux', + 'Linux', + 'macos' + ]) + // Only exact matches are valid + expect(result).toEqual(['Windows', 'Linux']) + }) + + it('should preserve order of first occurrence when deduplicating', () => { + const result = normalizeOSList([ + 'Linux', + 'Windows', + 'macOS', + 'Linux', + 'Windows' + ]) + expect(result).toEqual(['Linux', 'Windows', 'macOS']) + }) + }) +}) diff --git a/tests-ui/tests/utils/versionUtil.test.ts b/tests-ui/tests/utils/versionUtil.test.ts new file mode 100644 index 000000000..87e343496 --- /dev/null +++ b/tests-ui/tests/utils/versionUtil.test.ts @@ -0,0 +1,346 @@ +import { describe, expect, it, vi } from 'vitest' + +import { + checkVersionCompatibility, + getFrontendVersion +} from '@/workbench/extensions/manager/utils/versionUtil' + +// Mock config module +vi.mock('@/config', () => ({ + default: { + app_version: '1.24.0-1' + } +})) + +describe('versionUtil', () => { + describe('checkVersionCompatibility', () => { + it('should return null when current version is undefined', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + undefined, + '>=1.0.0' + ) + expect(result).toBeNull() + }) + + it('should return null when current version is null', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + null as any, + '>=1.0.0' + ) + expect(result).toBeNull() + }) + + it('should return null when current version is empty string', () => { + const result = checkVersionCompatibility('comfyui_version', '', '>=1.0.0') + expect(result).toBeNull() + }) + + it('should return null when supported version is undefined', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + '1.0.0', + undefined + ) + expect(result).toBeNull() + }) + + it('should return null when supported version is null', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + '1.0.0', + null as any + ) + expect(result).toBeNull() + }) + + it('should return null when supported version is empty string', () => { + const result = checkVersionCompatibility('comfyui_version', '1.0.0', '') + expect(result).toBeNull() + }) + + it('should return null when supported version is whitespace only', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + '1.0.0', + ' ' + ) + expect(result).toBeNull() + }) + + describe('version compatibility checks', () => { + it('should return null when version satisfies >= requirement', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + '2.0.0', + '>=1.0.0' + ) + expect(result).toBeNull() + }) + + it('should return null when version exactly matches requirement', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + '1.0.0', + '1.0.0' + ) + expect(result).toBeNull() + }) + + it('should return null when version satisfies ^ requirement', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + '1.2.3', + '^1.0.0' + ) + expect(result).toBeNull() + }) + + it('should return null when version satisfies ~ requirement', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + '1.0.5', + '~1.0.0' + ) + expect(result).toBeNull() + }) + + it('should return null when version satisfies range requirement', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + '1.5.0', + '1.0.0 - 2.0.0' + ) + expect(result).toBeNull() + }) + + it('should return conflict when version does not satisfy >= requirement', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + '0.9.0', + '>=1.0.0' + ) + expect(result).toEqual({ + type: 'comfyui_version', + current_value: '0.9.0', + required_value: '>=1.0.0' + }) + }) + + it('should return conflict when version does not satisfy ^ requirement', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + '2.0.0', + '^1.0.0' + ) + expect(result).toEqual({ + type: 'comfyui_version', + current_value: '2.0.0', + required_value: '^1.0.0' + }) + }) + + it('should return conflict when version is outside range', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + '3.0.0', + '1.0.0 - 2.0.0' + ) + expect(result).toEqual({ + type: 'comfyui_version', + current_value: '3.0.0', + required_value: '1.0.0 - 2.0.0' + }) + }) + }) + + describe('version cleaning', () => { + it('should handle versions with v prefix', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + 'v1.0.0', + '>=1.0.0' + ) + expect(result).toBeNull() + }) + + it('should handle versions with pre-release tags', () => { + // Pre-release versions have specific semver rules + // 1.0.0-alpha satisfies >=1.0.0-alpha but not >=1.0.0 + const result = checkVersionCompatibility( + 'comfyui_version', + '1.0.0-alpha', + '>=1.0.0-alpha' + ) + expect(result).toBeNull() + + // This should fail because pre-release < stable + const result2 = checkVersionCompatibility( + 'comfyui_version', + '1.0.0-alpha', + '>=1.0.0' + ) + expect(result2).toEqual({ + type: 'comfyui_version', + current_value: '1.0.0-alpha', + required_value: '>=1.0.0' + }) + }) + + it('should handle versions with build metadata', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + '1.0.0+build123', + '>=1.0.0' + ) + expect(result).toBeNull() + }) + + it('should handle malformed versions gracefully', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + 'not-a-version', + '>=1.0.0' + ) + expect(result).toEqual({ + type: 'comfyui_version', + current_value: 'not-a-version', + required_value: '>=1.0.0' + }) + }) + }) + + describe('different conflict types', () => { + it('should handle comfyui_version type', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + '0.5.0', + '>=1.0.0' + ) + expect(result?.type).toBe('comfyui_version') + }) + + it('should handle frontend_version type', () => { + const result = checkVersionCompatibility( + 'frontend_version', + '0.5.0', + '>=1.0.0' + ) + expect(result?.type).toBe('frontend_version') + }) + }) + + describe('complex version ranges', () => { + it('should handle OR conditions with ||', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + '1.5.0', + '>=1.0.0 <2.0.0 || >=3.0.0' + ) + expect(result).toBeNull() + }) + + it('should handle multiple constraints', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + '1.5.0', + '>=1.0.0 <2.0.0' + ) + expect(result).toBeNull() + }) + + it('should return conflict when no constraints are met', () => { + const result = checkVersionCompatibility( + 'comfyui_version', + '2.5.0', + '>=1.0.0 <2.0.0 || >=3.0.0 <4.0.0' + ) + expect(result).toEqual({ + type: 'comfyui_version', + current_value: '2.5.0', + required_value: '>=1.0.0 <2.0.0 || >=3.0.0 <4.0.0' + }) + }) + }) + }) + + describe('getFrontendVersion', () => { + it('should return app_version from config when available', () => { + const version = getFrontendVersion() + expect(version).toBe('1.24.0-1') + }) + + it('should fallback to VITE_APP_VERSION when app_version is not available', async () => { + // Save original environment + const originalEnv = import.meta.env.VITE_APP_VERSION + + // Mock config without app_version + vi.doMock('@/config', () => ({ + default: {} + })) + + // Set VITE_APP_VERSION + import.meta.env.VITE_APP_VERSION = '2.0.0' + + // Clear module cache to force re-import + vi.resetModules() + + // Import fresh module + const versionUtil = await import( + '@/workbench/extensions/manager/utils/versionUtil' + ) + + const version = versionUtil.getFrontendVersion() + expect(version).toBe('2.0.0') + + // Restore original env + import.meta.env.VITE_APP_VERSION = originalEnv + + // Reset mocks for next test + vi.resetModules() + vi.doMock('@/config', () => ({ + default: { + app_version: '1.24.0-1' + } + })) + }) + + it('should return undefined when no version is available', async () => { + // Save original environment + const originalEnv = import.meta.env.VITE_APP_VERSION + + // Mock config without app_version + vi.doMock('@/config', () => ({ + default: {} + })) + + // Clear VITE_APP_VERSION + delete import.meta.env.VITE_APP_VERSION + + // Clear module cache to force re-import + vi.resetModules() + + // Import fresh module + const versionUtil = await import( + '@/workbench/extensions/manager/utils/versionUtil' + ) + + const version = versionUtil.getFrontendVersion() + expect(version).toBeUndefined() + + // Restore original env + if (originalEnv !== undefined) { + import.meta.env.VITE_APP_VERSION = originalEnv + } + + // Reset mocks for next test + vi.resetModules() + vi.doMock('@/config', () => ({ + default: { + app_version: '1.24.0-1' + } + })) + }) + }) +})