[Manager] Compatibility Detection Logic (#4348)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jin Yi
2025-07-08 06:44:53 +09:00
parent 5055092cc5
commit 0a2f2d8368
9 changed files with 2726 additions and 121 deletions

167
package-lock.json generated
View File

@@ -40,6 +40,7 @@
"pinia": "^2.1.7",
"primeicons": "^7.0.0",
"primevue": "^4.2.5",
"semver": "^7.7.2",
"three": "^0.170.0",
"tiptap-markdown": "^0.8.10",
"vue": "^3.5.13",
@@ -62,6 +63,7 @@
"@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.17.6",
"@types/node": "^20.14.8",
"@types/semver": "^7.7.0",
"@types/three": "^0.169.0",
"@vitejs/plugin-vue": "^5.1.4",
"@vue/test-utils": "^2.4.6",
@@ -557,6 +559,16 @@
"url": "https://opencollective.com/babel"
}
},
"node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/generator": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz",
@@ -601,6 +613,16 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz",
@@ -622,6 +644,16 @@
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/helper-member-expression-to-functions": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
@@ -4522,6 +4554,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/semver": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz",
"integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/stats.js": {
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz",
@@ -4753,19 +4792,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.0.tgz",
@@ -6536,19 +6562,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/conf/node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/confbox": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz",
@@ -7448,19 +7461,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/editorconfig/node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -7851,19 +7851,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-plugin-vue/node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/eslint-plugin-vue/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -10725,19 +10712,6 @@
"node": ">=14"
}
},
"node_modules/langsmith/node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/latest-version": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-9.0.0.tgz",
@@ -12687,19 +12661,6 @@
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
"dev": true
},
"node_modules/package-json/node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/package-manager-detector": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.0.tgz",
@@ -14342,12 +14303,15 @@
"dev": true
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/send": {
@@ -16206,19 +16170,6 @@
"url": "https://github.com/yeoman/update-notifier?sponsor=1"
}
},
"node_modules/update-notifier/node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -17038,19 +16989,6 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/vue-eslint-parser/node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/vue-i18n": {
"version": "9.14.3",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.3.tgz",
@@ -17103,19 +17041,6 @@
"typescript": ">=5.0.0"
}
},
"node_modules/vue-tsc/node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/vuefire": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/vuefire/-/vuefire-3.2.1.tgz",

View File

@@ -40,6 +40,7 @@
"@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.17.6",
"@types/node": "^20.14.8",
"@types/semver": "^7.7.0",
"@types/three": "^0.169.0",
"@vitejs/plugin-vue": "^5.1.4",
"@vue/test-utils": "^2.4.6",
@@ -105,6 +106,7 @@
"pinia": "^2.1.7",
"primeicons": "^7.0.0",
"primevue": "^4.2.5",
"semver": "^7.7.2",
"three": "^0.170.0",
"tiptap-markdown": "^0.8.10",
"vue": "^3.5.13",

View File

@@ -16,11 +16,13 @@ import { computed, onMounted } from 'vue'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import config from '@/config'
import { useConflictDetection } from '@/composables/useConflictDetection'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { electronAPI, isElectron } from './utils/envUtil'
const workspaceStore = useWorkspaceStore()
const conflictDetection = useConflictDetection()
const isLoading = computed<boolean>(() => workspaceStore.spinner)
const handleKey = (e: KeyboardEvent) => {
workspaceStore.shiftDown = e.shiftKey
@@ -47,5 +49,9 @@ onMounted(() => {
if (isElectron()) {
document.addEventListener('contextmenu', showContextMenu)
}
// Initialize conflict detection in background
// This runs async and doesn't block UI setup
void conflictDetection.initializeConflictDetection()
})
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,239 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import type {
ConflictType,
SystemEnvironment
} from '@/types/conflictDetectionTypes'
interface IncompatibleNodeInfo {
nodeId: string
nodeName: string
disableReason: ConflictType
conflictDetails: string
detectedAt: string
}
/**
* Store for managing node compatibility checking functionality.
* Follows error-resilient patterns from useConflictDetection composable.
*/
export const useNodeCompatibilityStore = defineStore(
'nodeCompatibility',
() => {
// Core state
const isChecking = ref(false)
const lastCheckTime = ref<string | null>(null)
const checkError = ref<string | null>(null)
const systemEnvironment = ref<SystemEnvironment | null>(null)
// Node tracking maps
const incompatibleNodes = ref<Map<string, IncompatibleNodeInfo>>(new Map())
const failedImportNodes = ref<Set<string>>(new Set())
const bannedNodes = ref<Set<string>>(new Set())
const securityPendingNodes = ref<Set<string>>(new Set())
// User interaction state
const hasShownNotificationModal = ref(false)
const pendingNotificationNodes = ref<IncompatibleNodeInfo[]>([])
// Computed properties
const hasIncompatibleNodes = computed(
() => incompatibleNodes.value.size > 0
)
const totalIncompatibleCount = computed(
() =>
incompatibleNodes.value.size +
failedImportNodes.value.size +
bannedNodes.value.size
)
const incompatibleNodesList = computed(() =>
Array.from(incompatibleNodes.value.values())
)
const shouldShowNotification = computed(() => {
// Show notification if there are incompatible nodes and we haven't shown notification yet
return hasIncompatibleNodes.value && !hasShownNotificationModal.value
})
/**
* Checks if a node has compatibility issues.
*/
function hasNodeCompatibilityIssues(nodeId: string): boolean {
return (
incompatibleNodes.value.has(nodeId) ||
failedImportNodes.value.has(nodeId) ||
bannedNodes.value.has(nodeId)
)
}
/**
* Gets the compatibility info for a node.
*/
function getNodeCompatibilityInfo(
nodeId: string
): IncompatibleNodeInfo | null {
return incompatibleNodes.value.get(nodeId) || null
}
/**
* Adds a node to the incompatible list.
*/
function addIncompatibleNode(
nodeId: string,
nodeName: string,
reason: ConflictType,
details: string
): void {
const info: IncompatibleNodeInfo = {
nodeId,
nodeName,
disableReason: reason,
conflictDetails: details,
detectedAt: new Date().toISOString()
}
incompatibleNodes.value.set(nodeId, info)
// Add to pending list (for notification purposes)
if (!hasShownNotificationModal.value) {
pendingNotificationNodes.value.push(info)
}
}
/**
* Removes a node from the incompatible list.
*/
function removeIncompatibleNode(nodeId: string): void {
incompatibleNodes.value.delete(nodeId)
failedImportNodes.value.delete(nodeId)
bannedNodes.value.delete(nodeId)
securityPendingNodes.value.delete(nodeId)
// Remove from pending list
pendingNotificationNodes.value = pendingNotificationNodes.value.filter(
(node) => node.nodeId !== nodeId
)
}
/**
* Clears all compatibility check results.
*/
function clearResults(): void {
incompatibleNodes.value.clear()
failedImportNodes.value.clear()
bannedNodes.value.clear()
securityPendingNodes.value.clear()
pendingNotificationNodes.value = []
checkError.value = null
}
/**
* Marks that the notification modal has been shown.
*/
function markNotificationModalShown(): void {
hasShownNotificationModal.value = true
pendingNotificationNodes.value = []
}
/**
* Resets the notification modal state (for testing or re-initialization).
*/
function resetNotificationModalState(): void {
hasShownNotificationModal.value = false
pendingNotificationNodes.value = Array.from(
incompatibleNodes.value.values()
)
}
/**
* Updates the system environment information.
*/
function setSystemEnvironment(env: SystemEnvironment): void {
systemEnvironment.value = env
}
/**
* Sets the checking state.
*/
function setCheckingState(checking: boolean): void {
isChecking.value = checking
if (checking) {
checkError.value = null
}
}
/**
* Records a successful check completion.
*/
function recordCheckCompletion(): void {
lastCheckTime.value = new Date().toISOString()
isChecking.value = false
}
/**
* Records a check error.
*/
function recordCheckError(error: string): void {
checkError.value = error
isChecking.value = false
}
/**
* Gets a summary of the current compatibility state.
*/
function getCompatibilitySummary() {
return {
totalChecked: lastCheckTime.value ? 'completed' : 'pending',
incompatibleCount: incompatibleNodes.value.size,
failedImportCount: failedImportNodes.value.size,
bannedCount: bannedNodes.value.size,
securityPendingCount: securityPendingNodes.value.size,
totalIssues: totalIncompatibleCount.value,
lastCheckTime: lastCheckTime.value,
hasError: !!checkError.value
}
}
return {
// State
isChecking: computed(() => isChecking.value),
lastCheckTime: computed(() => lastCheckTime.value),
checkError: computed(() => checkError.value),
systemEnvironment: computed(() => systemEnvironment.value),
// Node tracking
incompatibleNodes: computed(() => incompatibleNodes.value),
incompatibleNodesList,
failedImportNodes: computed(() => failedImportNodes.value),
bannedNodes: computed(() => bannedNodes.value),
securityPendingNodes: computed(() => securityPendingNodes.value),
// User interaction
hasShownNotificationModal: computed(
() => hasShownNotificationModal.value
),
pendingNotificationNodes: computed(() => pendingNotificationNodes.value),
shouldShowNotification,
// Computed
hasIncompatibleNodes,
totalIncompatibleCount,
// Methods
hasNodeCompatibilityIssues,
getNodeCompatibilityInfo,
addIncompatibleNode,
removeIncompatibleNode,
clearResults,
markNotificationModalShown,
resetNotificationModalState,
setSystemEnvironment,
setCheckingState,
recordCheckCompletion,
recordCheckError,
getCompatibilitySummary
}
}
)

View File

@@ -0,0 +1,264 @@
/**
* Type definitions for the conflict detection system.
* These types are used to detect compatibility issues between Node Packs and the system environment.
*/
/**
* Conflict types that can be detected in the system
* @enum {string}
*/
export type ConflictType =
| 'comfyui_version' // ComfyUI version mismatch
| 'frontend_version' // Frontend version mismatch
| 'python_version' // Python version mismatch
| 'os' // Operating system incompatibility
| 'accelerator' // GPU/accelerator incompatibility
| 'banned' // Banned package
| 'security_pending' // Security verification pending
/**
* Security scan status for packages
* @enum {string}
*/
export type SecurityScanStatus = 'pending' | 'passed' | 'failed' | 'unknown'
/**
* Supported operating systems (as per Registry Admin guide)
* @enum {string}
*/
export type SupportedOS = 'Windows' | 'macOS' | 'Linux' | 'any'
/**
* Supported accelerators for GPU computation (as per Registry Admin guide)
* @enum {string}
*/
export type SupportedAccelerator = 'CUDA' | 'ROCm' | 'Metal' | 'CPU' | 'any'
/**
* Version comparison operators
* @enum {string}
*/
export type VersionOperator = '>=' | '>' | '<=' | '<' | '==' | '!='
/**
* Version requirement specification
*/
export interface VersionRequirement {
/** @description Comparison operator for version checking */
operator: VersionOperator
/** @description Target version string */
version: string
}
/**
* Node Pack requirements from Registry API
*/
export interface NodePackRequirements {
/** @description Unique package identifier */
package_id: string
/** @description Human-readable package name */
package_name: string
/** @description Currently installed version */
installed_version: string
/** @description Whether the package is enabled locally */
is_enabled: boolean
/** @description Supported ComfyUI version from Registry */
supported_comfyui_version?: string
/** @description Supported frontend version from Registry */
supported_comfyui_frontend_version?: string
/** @description List of supported operating systems from Registry */
supported_os?: SupportedOS[]
/** @description List of supported accelerators from Registry */
supported_accelerators?: SupportedAccelerator[]
/** @description Package dependencies from Registry */
dependencies?: string[]
/** @description Node status from Registry (Active/Banned/Deleted) */
registry_status?:
| 'NodeStatusActive'
| 'NodeStatusBanned'
| 'NodeStatusDeleted'
/** @description Node version status from Registry */
version_status?:
| 'NodeVersionStatusActive'
| 'NodeVersionStatusBanned'
| 'NodeVersionStatusDeleted'
| 'NodeVersionStatusPending'
| 'NodeVersionStatusFlagged'
/** @description Whether package is banned (derived from status) */
is_banned: boolean
/** @description Reason for ban if applicable */
ban_reason?: string
// Metadata
/** @description Registry data fetch timestamp */
registry_fetch_time: string
/** @description Whether Registry data was successfully fetched */
has_registry_data: boolean
}
/**
* Current system environment information
*/
export interface SystemEnvironment {
// Version information
/** @description Current ComfyUI version */
comfyui_version: string
/** @description Current frontend version */
frontend_version: string
/** @description Current Python version */
python_version: string
// Platform information
/** @description Operating system type */
os: SupportedOS
/** @description Detailed platform information (e.g., 'Darwin 24.5.0', 'Windows 10') */
platform_details: string
/** @description System architecture (e.g., 'x64', 'arm64') */
architecture: string
// GPU/accelerator information
/** @description List of available accelerators */
available_accelerators: SupportedAccelerator[]
/** @description Primary accelerator in use */
primary_accelerator: SupportedAccelerator
/** @description GPU memory in megabytes, if available */
gpu_memory_mb?: number
// Runtime information
/** @description Node.js environment mode */
node_env: 'development' | 'production'
/** @description Browser user agent string */
user_agent: string
}
/**
* Individual conflict detection result for a package
*/
export interface ConflictDetectionResult {
/** @description Package identifier */
package_id: string
/** @description Package name */
package_name: string
/** @description Whether any conflicts were detected */
has_conflict: boolean
/** @description List of detected conflicts */
conflicts: ConflictDetail[]
/** @description Overall compatibility status */
is_compatible: boolean
/** @description Whether conflicts can be automatically resolved */
can_auto_resolve: boolean
/** @description Recommended action to resolve conflicts */
recommended_action: RecommendedAction
}
/**
* Detailed information about a specific conflict
*/
export interface ConflictDetail {
/** @description Type of conflict detected */
type: ConflictType
/** @description Severity level of the conflict */
severity: 'error' | 'warning' | 'info'
/** @description Human-readable description of the conflict */
description: string
/** @description Current system value */
current_value: string
/** @description Required value for compatibility */
required_value: string
/** @description Optional steps to resolve the conflict */
resolution_steps?: string[]
}
/**
* Recommended action to resolve conflicts
*/
export interface RecommendedAction {
/** @description Type of action to take */
action_type: 'disable' | 'update' | 'ignore' | 'manual_review'
/** @description Reason for the recommended action */
reason: string
/** @description Step-by-step instructions */
steps: string[]
/** @description Estimated difficulty of implementing the action */
estimated_difficulty: 'easy' | 'medium' | 'hard'
}
/**
* Overall conflict detection summary
*/
export interface ConflictDetectionSummary {
/** @description Total number of packages checked */
total_packages: number
/** @description Number of compatible packages */
compatible_packages: number
/** @description Number of packages with conflicts */
conflicted_packages: number
/** @description Number of banned packages */
banned_packages: number
/** @description Number of packages pending security verification */
security_pending_packages: number
/** @description Node IDs grouped by conflict type */
conflicts_by_type_details: Record<ConflictType, string[]>
/** @description Timestamp of the last conflict check */
last_check_timestamp: string
/** @description Duration of the conflict check in milliseconds */
check_duration_ms: number
}
/**
* API request/response interfaces
*/
/**
* Request payload for conflict detection API
*/
export interface ConflictDetectionRequest {
/** @description Current system environment information */
system_environment: SystemEnvironment
/** @description Optional list of specific package IDs to check */
package_ids?: string[]
/** @description Whether to include banned packages in the check */
include_banned?: boolean
/** @description Whether to include security-pending packages in the check */
include_security_pending?: boolean
}
/**
* Response payload from conflict detection API
*/
export interface ConflictDetectionResponse {
/** @description Whether the API request was successful */
success: boolean
/** @description Error message if the request failed */
error_message?: string
/** @description Summary of the conflict detection results */
summary: ConflictDetectionSummary
/** @description Detailed results for each package */
results: ConflictDetectionResult[]
/** @description System environment information detected by the server (for comparison) */
detected_system_environment?: Partial<SystemEnvironment>
}
/**
* Real-time conflict detection event
*/
export interface ConflictDetectionEvent {
/** @description Type of event */
event_type:
| 'conflict_detected'
| 'conflict_resolved'
| 'scan_started'
| 'scan_completed'
/** @description Event timestamp */
timestamp: string
/** @description Package ID associated with the event, if applicable */
package_id?: string
/** @description Type of conflict, if applicable */
conflict_type?: ConflictType
/** @description Additional event details */
details?: string
}

80
src/utils/versionUtil.ts Normal file
View File

@@ -0,0 +1,80 @@
import * as semver from 'semver'
/**
* 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 semver.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 semver.satisfies(cleanedVersion, range)
} catch {
return false
}
}
/**
* Compares two versions and returns the difference type
* @param version1 First version
* @param version2 Second version
* @returns Difference type or null if comparison fails
*/
export function getVersionDifference(
version1: string,
version2: string
): semver.ReleaseType | null {
try {
const clean1 = cleanVersion(version1)
const clean2 = cleanVersion(version2)
return semver.diff(clean1, clean2)
} catch {
return null
}
}
/**
* Checks if a version is valid according to semver
* @param version Version string to validate
* @returns true if version is valid
*/
export function isValidVersion(version: string): boolean {
return semver.valid(version) !== null
}
/**
* Gets a human-readable description of a version range
* @param range Version range string
* @returns Description of what the range means
*/
export function describeVersionRange(range: string): string {
if (range.startsWith('>=')) {
return `version ${range.substring(2)} or higher`
} else if (range.startsWith('>')) {
return `version higher than ${range.substring(1)}`
} else if (range.startsWith('<=')) {
return `version ${range.substring(2)} or lower`
} else if (range.startsWith('<')) {
return `version lower than ${range.substring(1)}`
} else if (range.startsWith('^')) {
return `compatible with version ${range.substring(1)}`
} else if (range.startsWith('~')) {
return `approximately version ${range.substring(1)}`
} else if (range.includes(' - ')) {
const [min, max] = range.split(' - ')
return `version between ${min} and ${max}`
} else if (range.includes('||')) {
return `one of multiple version ranges: ${range}`
}
return `version ${range}`
}

View File

@@ -0,0 +1,864 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { useConflictDetection } from '@/composables/useConflictDetection'
import type { components } from '@/types/comfyRegistryTypes'
import type { components as ManagerComponents } from '@/types/generatedManagerTypes'
// Mock dependencies
vi.mock('@/scripts/api', () => ({
api: {
fetchApi: vi.fn()
}
}))
vi.mock('@/services/comfyManagerService', () => ({
useComfyManagerService: vi.fn()
}))
vi.mock('@/services/comfyRegistryService', () => ({
useComfyRegistryService: vi.fn()
}))
vi.mock('@/stores/systemStatsStore', () => ({
useSystemStatsStore: vi.fn()
}))
vi.mock('@/config', () => ({
default: {
app_version: '1.24.0-1'
}
}))
describe('useConflictDetection with Registry Store', () => {
const mockComfyManagerService = {
listInstalledPacks: vi.fn()
}
const mockRegistryService = {
getPackByVersion: vi.fn()
}
const mockSystemStatsStore = {
fetchSystemStats: vi.fn(),
systemStats: {
system: {
comfyui_version: '0.3.41',
python_version: '3.12.11',
os: 'Darwin'
},
devices: [
{
name: 'Apple M1 Pro',
type: 'mps',
vram_total: 17179869184
}
]
} as any
}
beforeEach(async () => {
vi.clearAllMocks()
// Reset mock system stats to default state
mockSystemStatsStore.systemStats = {
system: {
comfyui_version: '0.3.41',
python_version: '3.12.11',
os: 'Darwin'
},
devices: [
{
name: 'Apple M1 Pro',
type: 'mps',
vram_total: 17179869184
}
]
} as any
// Reset mock functions
mockSystemStatsStore.fetchSystemStats.mockResolvedValue(undefined)
mockComfyManagerService.listInstalledPacks.mockReset()
mockRegistryService.getPackByVersion.mockReset()
// Mock useComfyManagerService
const { useComfyManagerService } = await import(
'@/services/comfyManagerService'
)
vi.mocked(useComfyManagerService).mockReturnValue(
mockComfyManagerService as any
)
// 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)
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('system environment detection', () => {
it('should collect system environment information successfully', async () => {
const { detectSystemEnvironment } = useConflictDetection()
const environment = await detectSystemEnvironment()
expect(environment.comfyui_version).toBe('0.3.41')
expect(environment.frontend_version).toBe('1.24.0-1')
expect(environment.python_version).toBe('3.12.11')
expect(environment.available_accelerators).toContain('Metal')
expect(environment.available_accelerators).toContain('CPU')
expect(environment.primary_accelerator).toBe('Metal')
})
it('should return fallback environment information when systemStatsStore fails', async () => {
// Mock systemStatsStore failure
mockSystemStatsStore.fetchSystemStats.mockRejectedValue(
new Error('Store failure')
)
mockSystemStatsStore.systemStats = null
const { detectSystemEnvironment } = useConflictDetection()
const environment = await detectSystemEnvironment()
expect(environment.comfyui_version).toBe('unknown')
expect(environment.frontend_version).toBe('1.24.0-1')
expect(environment.python_version).toBe('unknown')
expect(environment.available_accelerators).toEqual(['CPU'])
})
})
describe('package requirements detection with Registry Store', () => {
it('should fetch and combine local + Registry data successfully', async () => {
// Mock installed packages
const mockInstalledPacks: ManagerComponents['schemas']['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'][] = [
{
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'
} as components['schemas']['Node']
]
mockComfyManagerService.listInstalledPacks.mockResolvedValue(
mockInstalledPacks
)
// 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
})
}
return Promise.resolve(null)
}
)
const { performConflictDetection } = useConflictDetection()
const result = await performConflictDetection()
expect(result.success).toBe(true)
expect(result.summary.total_packages).toBe(2)
expect(result.results).toHaveLength(2)
// 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 Registry data was properly integrated
const managerNode = result.results.find(
(r) => r.package_id === 'ComfyUI-Manager'
)
expect(managerNode?.is_compatible).toBe(true) // Should be compatible
// Disabled + banned node should have conflicts
const testNode = result.results.find(
(r) => r.package_id === 'ComfyUI-TestNode'
)
expect(testNode?.conflicts).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'banned',
severity: 'error'
})
])
)
})
it('should handle Registry Store failures gracefully', async () => {
// Mock installed packages
const mockInstalledPacks: ManagerComponents['schemas']['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: 'security_pending',
severity: 'warning',
description: expect.stringContaining('Registry data not available')
})
])
)
})
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: ManagerComponents['schemas']['InstalledPacksResponse'] =
{
CompatibleNode: {
ver: '1.0.0',
cnr_id: 'compatible-node',
aux_id: null,
enabled: true
}
}
const mockCompatibleRegistryPacks: components['schemas']['Node'][] = [
{
id: 'CompatibleNode',
name: 'Compatible 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
)
// 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: ManagerComponents['schemas']['InstalledPacksResponse'] =
{
WindowsOnlyNode: {
ver: '1.0.0',
cnr_id: 'windows-only',
aux_id: null,
enabled: true
}
}
const mockWindowsOnlyRegistryPacks: components['schemas']['Node'][] = [
{
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']
]
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',
severity: 'error',
description: expect.stringContaining('Unsupported operating system')
})
])
)
})
it('should detect accelerator incompatibility conflicts', async () => {
// Mock CUDA-only package
const mockInstalledPacks: ManagerComponents['schemas']['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',
severity: 'error',
description: expect.stringContaining(
'Required GPU/accelerator not available'
)
})
])
)
})
it('should treat Registry-banned packages as conflicts', async () => {
// Mock Registry-banned package
const mockInstalledPacks: ManagerComponents['schemas']['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',
severity: 'error',
description: expect.stringContaining('Package is banned')
})
])
)
expect(bannedNode.recommended_action.action_type).toBe('disable')
})
it('should treat locally disabled packages as banned', async () => {
// Mock locally disabled package
const mockInstalledPacks: ManagerComponents['schemas']['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',
severity: 'error',
description: expect.stringContaining('Package is disabled locally')
})
])
)
expect(disabledNode.recommended_action.action_type).toBe('disable')
})
})
describe('computed properties with Registry Store', () => {
it('should return true for hasConflicts when Registry conflicts exist', async () => {
// Mock package with OS incompatibility
const mockInstalledPacks: ManagerComponents['schemas']['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 only error-level conflicts for criticalConflicts', async () => {
// Mock package with error-level conflict
const mockInstalledPacks: ManagerComponents['schemas']['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 { criticalConflicts, performConflictDetection } =
useConflictDetection()
await performConflictDetection()
await nextTick()
expect(criticalConflicts.value.length).toBeGreaterThan(0)
expect(
criticalConflicts.value.every(
(conflict) => conflict.severity === 'error'
)
).toBe(true)
})
it('should return only banned packages for bannedPackages', async () => {
// Mock one banned and one normal package
const mockInstalledPacks: ManagerComponents['schemas']['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.fetchSystemStats.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: ManagerComponents['schemas']['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).toBe(2)
// Package A should have Registry data
const packageA = result.results.find((r) => r.package_id === 'Package-A')
expect(packageA?.conflicts).toHaveLength(0) // No conflicts
// Package B should have warning about missing Registry data
const packageB = result.results.find((r) => r.package_id === 'Package-B')
expect(packageB?.conflicts).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'security_pending',
severity: 'warning',
description: expect.stringContaining('Registry data not available')
})
])
)
})
it('should handle complete system failure gracefully', async () => {
// Mock all stores/services failing
mockSystemStatsStore.fetchSystemStats.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('initialization', () => {
it('should execute initializeConflictDetection without errors', async () => {
mockComfyManagerService.listInstalledPacks.mockResolvedValue({})
const { initializeConflictDetection } = useConflictDetection()
expect(() => {
void initializeConflictDetection()
}).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()
})
})
})

View File

@@ -0,0 +1,129 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { useNodeCompatibilityStore } from '@/stores/nodeCompatibilityStore'
describe('useNodeCompatibilityStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should initialize with empty state', () => {
const store = useNodeCompatibilityStore()
expect(store.isChecking).toBe(false)
expect(store.lastCheckTime).toBeNull()
expect(store.checkError).toBeNull()
expect(store.hasIncompatibleNodes).toBe(false)
expect(store.totalIncompatibleCount).toBe(0)
expect(store.shouldShowNotification).toBe(false)
})
it('should add incompatible nodes correctly', () => {
const store = useNodeCompatibilityStore()
store.addIncompatibleNode(
'test-node',
'Test Node',
'banned',
'Node is banned for testing'
)
expect(store.hasIncompatibleNodes).toBe(true)
expect(store.totalIncompatibleCount).toBe(1)
expect(store.hasNodeCompatibilityIssues('test-node')).toBe(true)
const compatibilityInfo = store.getNodeCompatibilityInfo('test-node')
expect(compatibilityInfo).toBeDefined()
expect(compatibilityInfo?.disableReason).toBe('banned')
})
it('should remove incompatible nodes correctly', () => {
const store = useNodeCompatibilityStore()
store.addIncompatibleNode(
'test-node',
'Test Node',
'banned',
'Node is banned for testing'
)
expect(store.hasIncompatibleNodes).toBe(true)
store.removeIncompatibleNode('test-node')
expect(store.hasIncompatibleNodes).toBe(false)
expect(store.hasNodeCompatibilityIssues('test-node')).toBe(false)
})
it('should handle notification modal state correctly', () => {
const store = useNodeCompatibilityStore()
// Add an incompatible node
store.addIncompatibleNode(
'test-node',
'Test Node',
'banned',
'Node is banned for testing'
)
expect(store.shouldShowNotification).toBe(true)
expect(store.pendingNotificationNodes).toHaveLength(1)
store.markNotificationModalShown()
expect(store.shouldShowNotification).toBe(false)
expect(store.pendingNotificationNodes).toHaveLength(0)
})
it('should clear all results correctly', () => {
const store = useNodeCompatibilityStore()
store.addIncompatibleNode(
'test-node',
'Test Node',
'banned',
'Node is banned for testing'
)
store.recordCheckError('Test error')
expect(store.hasIncompatibleNodes).toBe(true)
expect(store.checkError).toBe('Test error')
store.clearResults()
expect(store.hasIncompatibleNodes).toBe(false)
expect(store.checkError).toBeNull()
})
it('should track checking state correctly', () => {
const store = useNodeCompatibilityStore()
expect(store.isChecking).toBe(false)
store.setCheckingState(true)
expect(store.isChecking).toBe(true)
store.recordCheckCompletion()
expect(store.isChecking).toBe(false)
expect(store.lastCheckTime).toBeDefined()
})
it('should provide compatibility summary', () => {
const store = useNodeCompatibilityStore()
store.addIncompatibleNode(
'banned-node',
'Banned Node',
'banned',
'Node is banned'
)
const summary = store.getCompatibilitySummary()
expect(summary.incompatibleCount).toBe(1)
expect(summary.bannedCount).toBe(0) // bannedNodes is separate from incompatibleNodes
expect(summary.totalIssues).toBe(1)
expect(summary.hasError).toBe(false)
})
})