Compare commits

...

1 Commits

Author SHA1 Message Date
Matt Miller
c46eefcc43 refactor: extract shared evaluateCompatibility conflict pipeline
Both analyzePackageConflicts and checkNodeCompatibility built a
ConflictDetail[] from the same six leaf checks via a duplicated
push-if-truthy sequence. Extract that orchestration into a pure
evaluateCompatibility(input, env) helper in conflictUtils; each call site
keeps its own banned/pending derivation (the two enums differ) and passes
normalized booleans in.

checkNodeCompatibility consumers now see warnings in the canonical order
(comfyui_version, frontend_version, os, accelerator, banned, pending);
every consumer filters by conflict type, so this is cosmetic only.
2026-07-02 13:20:46 -07:00
4 changed files with 212 additions and 148 deletions

View File

@@ -14,6 +14,7 @@ import { useComfyManagerService } from '@/workbench/extensions/manager/services/
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 type * as ConflictUtils from '@/workbench/extensions/manager/utils/conflictUtils'
import { checkVersionCompatibility } from '@/workbench/extensions/manager/utils/versionUtil'
// Mock @vueuse/core until function
@@ -51,41 +52,15 @@ vi.mock('@/workbench/extensions/manager/utils/systemCompatibility', () => ({
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('@/workbench/extensions/manager/utils/conflictUtils', async () => {
const actual = await vi.importActual<typeof ConflictUtils>(
'@/workbench/extensions/manager/utils/conflictUtils'
)
return {
...actual,
consolidateConflictsByPackage: vi.fn((results) => results)
}
})
vi.mock(
'@/workbench/extensions/manager/composables/useConflictAcknowledgment',
@@ -448,6 +423,37 @@ describe('useConflictDetection', () => {
})
})
describe('checkNodeCompatibility status derivation', () => {
it('flags a banned conflict for a Node with NodeStatusBanned', () => {
const { checkNodeCompatibility } = useConflictDetection()
const { conflicts } = checkNodeCompatibility({
status: 'NodeStatusBanned'
} as components['schemas']['Node'])
expect(conflicts.map((c) => c.type)).toContain('banned')
})
it('flags a banned conflict for a NodeVersion with NodeVersionStatusBanned', () => {
const { checkNodeCompatibility } = useConflictDetection()
const { conflicts } = checkNodeCompatibility({
status: 'NodeVersionStatusBanned'
} as components['schemas']['NodeVersion'])
expect(conflicts.map((c) => c.type)).toContain('banned')
})
it('flags a pending conflict for a NodeVersion with NodeVersionStatusPending', () => {
const { checkNodeCompatibility } = useConflictDetection()
const { conflicts } = checkNodeCompatibility({
status: 'NodeVersionStatusPending'
} as components['schemas']['NodeVersion'])
const types = conflicts.map((c) => c.type)
expect(types).toContain('pending')
expect(types).not.toContain('banned')
})
})
describe('computed properties', () => {
it('should expose conflict status from store', () => {
mockConflictedPackages = [

View File

@@ -16,7 +16,6 @@ import type {
RegistryOS
} from '@/workbench/extensions/manager/types/compatibility.types'
import type {
ConflictDetail,
ConflictDetectionResponse,
ConflictDetectionResult,
ImportFailureMap,
@@ -26,18 +25,10 @@ import type {
} from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import {
consolidateConflictsByPackage,
createBannedConflict,
createPendingConflict
evaluateCompatibility
} 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'
import { normalizeOSList } from '@/workbench/extensions/manager/utils/systemCompatibility'
import { getFrontendVersion } from '@/workbench/extensions/manager/utils/versionUtil'
/**
* Composable for conflict detection system.
@@ -276,51 +267,21 @@ export function useConflictDetection() {
packageReq: NodeRequirements,
systemEnvInfo: SystemEnvironment
): ConflictDetectionResult {
const conflicts: ConflictDetail[] = []
// 1. ComfyUI version conflict check
const versionConflict = checkVersionCompatibility(
'comfyui_version',
systemEnvInfo.comfyui_version,
packageReq.supported_comfyui_version
const conflicts = evaluateCompatibility(
{
supported_os: packageReq.supported_os as RegistryOS[] | undefined,
supported_accelerators: packageReq.supported_accelerators as
| RegistryAccelerator[]
| undefined,
supported_comfyui_version: packageReq.supported_comfyui_version,
supported_comfyui_frontend_version:
packageReq.supported_comfyui_frontend_version,
isBanned: packageReq.is_banned,
isPending: packageReq.is_pending
},
systemEnvInfo
)
if (versionConflict) conflicts.push(versionConflict)
// 2. Frontend version conflict check
const frontendConflict = checkVersionCompatibility(
'frontend_version',
systemEnvInfo.frontend_version,
packageReq.supported_comfyui_frontend_version
)
if (frontendConflict) conflicts.push(frontendConflict)
// 3. OS compatibility check
const osConflict = checkOSCompatibility(
packageReq.supported_os as RegistryOS[] | undefined,
systemEnvInfo.os
)
if (osConflict) conflicts.push(osConflict)
// 4. Accelerator compatibility check
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 = createBannedConflict(packageReq.is_banned)
if (bannedConflict) {
conflicts.push(bannedConflict)
}
// 6. Registry data availability check using shared logic
const pendingConflict = createPendingConflict(packageReq.is_pending)
if (pendingConflict) {
conflicts.push(pendingConflict)
}
// Generate result
const hasConflict = conflicts.length > 0
return {
@@ -615,63 +576,26 @@ export function useConflictDetection() {
function checkNodeCompatibility(
node: Node | components['schemas']['NodeVersion']
) {
const conflicts: ConflictDetail[] = []
// Check OS compatibility
const osConflict = checkOSCompatibility(
normalizeOSList(node.supported_os),
systemEnvironment.value?.os
const conflicts = evaluateCompatibility(
{
supported_os: normalizeOSList(node.supported_os),
supported_accelerators:
node.supported_accelerators as RegistryAccelerator[],
supported_comfyui_version: node.supported_comfyui_version,
supported_comfyui_frontend_version:
node.supported_comfyui_frontend_version,
isBanned:
node.status === 'NodeStatusBanned' ||
node.status === 'NodeVersionStatusBanned',
isPending: node.status === 'NodeVersionStatusPending'
},
{
comfyui_version: systemEnvironment.value?.comfyui_version,
frontend_version: getFrontendVersion(),
os: systemEnvironment.value?.os,
accelerator: systemEnvironment.value?.accelerator
}
)
if (osConflict) {
conflicts.push(osConflict)
}
// Check Accelerator compatibility
const acceleratorConflict = checkAcceleratorCompatibility(
node.supported_accelerators as RegistryAccelerator[],
systemEnvironment.value?.accelerator
)
if (acceleratorConflict) {
conflicts.push(acceleratorConflict)
}
// Check ComfyUI version compatibility
const comfyUIVersionConflict = checkVersionCompatibility(
'comfyui_version',
systemEnvironment.value?.comfyui_version,
node.supported_comfyui_version
)
if (comfyUIVersionConflict) {
conflicts.push(comfyUIVersionConflict)
}
// Check ComfyUI Frontend version compatibility
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 = createBannedConflict(
node.status === 'NodeStatusBanned' ||
node.status === 'NodeVersionStatusBanned'
)
if (bannedConflict) {
conflicts.push(bannedConflict)
}
// Check pending status using shared logic
const pendingConflict = createPendingConflict(
node.status === 'NodeVersionStatusPending'
)
if (pendingConflict) {
conflicts.push(pendingConflict)
}
return {
hasConflict: conflicts.length > 0,

View File

@@ -7,10 +7,71 @@ import type {
import {
consolidateConflictsByPackage,
createBannedConflict,
createPendingConflict
createPendingConflict,
evaluateCompatibility
} from '@/workbench/extensions/manager/utils/conflictUtils'
describe('conflictUtils', () => {
describe('evaluateCompatibility', () => {
const incompatibleEnv = {
comfyui_version: '0.3.0',
frontend_version: '1.0.0',
os: 'darwin',
accelerator: 'mps'
}
it('emits conflicts in canonical order when all six checks fail', () => {
const conflicts = evaluateCompatibility(
{
supported_comfyui_version: '>=1.0.0',
supported_comfyui_frontend_version: '>=2.0.0',
supported_os: ['Linux'],
supported_accelerators: ['CUDA'],
isBanned: true,
isPending: true
},
incompatibleEnv
)
expect(conflicts.map((conflict) => conflict.type)).toEqual([
'comfyui_version',
'frontend_version',
'os',
'accelerator',
'banned',
'pending'
])
})
it('adds a banned conflict only when isBanned is true', () => {
const compatibleInput = {
supported_comfyui_version: undefined,
supported_comfyui_frontend_version: undefined,
supported_os: undefined,
supported_accelerators: undefined,
isPending: false
}
const withoutBan = evaluateCompatibility(
{ ...compatibleInput, isBanned: false },
incompatibleEnv
)
expect(withoutBan).toEqual([])
const withBan = evaluateCompatibility(
{ ...compatibleInput, isBanned: true },
incompatibleEnv
)
expect(withBan).toEqual([
{
type: 'banned',
current_value: 'installed',
required_value: 'not_banned'
}
])
})
})
describe('createBannedConflict', () => {
it('should return banned conflict when isBanned is true', () => {
const result = createBannedConflict(true)

View File

@@ -1,10 +1,20 @@
import { groupBy, uniqBy } from 'es-toolkit/compat'
import { normalizePackId } from '@/utils/packUtils'
import type {
RegistryAccelerator,
RegistryOS
} from '@/workbench/extensions/manager/types/compatibility.types'
import type {
ConflictDetail,
ConflictDetectionResult
ConflictDetectionResult,
SystemEnvironment
} from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import {
checkAcceleratorCompatibility,
checkOSCompatibility
} from '@/workbench/extensions/manager/utils/systemCompatibility'
import { checkVersionCompatibility } from '@/workbench/extensions/manager/utils/versionUtil'
/**
* Checks for banned package status conflicts.
@@ -38,6 +48,69 @@ export function createPendingConflict(
return null
}
/**
* Normalized compatibility inputs for a single package, produced by each call
* site from its own source shape. Banned/pending are pre-derived booleans
* because the two callers read different enums (see {@link evaluateCompatibility}).
*/
export interface CompatibilityInput {
supported_os?: RegistryOS[]
supported_accelerators?: RegistryAccelerator[]
supported_comfyui_version?: string
supported_comfyui_frontend_version?: string
isBanned: boolean
isPending: boolean
}
/**
* Runs the six compatibility leaf checks and collects the conflicts that fire.
*
* Canonical order = comfyui_version → frontend_version → OS → accelerator →
* banned → pending. This is the order surfaced by the primary conflict dialog
* (analyzePackageConflicts → runFullConflictAnalysis → WarningTabPanel); every
* consumer filters conflicts by `.type`, so the order is cosmetic only.
*/
export function evaluateCompatibility(
input: CompatibilityInput,
env: Pick<
SystemEnvironment,
'comfyui_version' | 'frontend_version' | 'os' | 'accelerator'
>
): ConflictDetail[] {
const conflicts: ConflictDetail[] = []
const versionConflict = checkVersionCompatibility(
'comfyui_version',
env.comfyui_version,
input.supported_comfyui_version
)
if (versionConflict) conflicts.push(versionConflict)
const frontendConflict = checkVersionCompatibility(
'frontend_version',
env.frontend_version,
input.supported_comfyui_frontend_version
)
if (frontendConflict) conflicts.push(frontendConflict)
const osConflict = checkOSCompatibility(input.supported_os, env.os)
if (osConflict) conflicts.push(osConflict)
const acceleratorConflict = checkAcceleratorCompatibility(
input.supported_accelerators,
env.accelerator
)
if (acceleratorConflict) conflicts.push(acceleratorConflict)
const bannedConflict = createBannedConflict(input.isBanned)
if (bannedConflict) conflicts.push(bannedConflict)
const pendingConflict = createPendingConflict(input.isPending)
if (pendingConflict) conflicts.push(pendingConflict)
return conflicts
}
/**
* Groups and deduplicates conflicts by normalized package name.
* Consolidates multiple conflict sources (registry checks, import failures, disabled packages with version suffix)