merge main into rh-test

This commit is contained in:
bymyself
2025-09-28 15:33:29 -07:00
parent 1c0f151d02
commit ff0c15b119
1317 changed files with 85439 additions and 18373 deletions

View File

@@ -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'
})
})
})
})

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { ISerialisedGraph } from '@/lib/litegraph/src/types/serialisation'
import type { ISerialisedGraph } from '@/lib/litegraph/src/types/serialisation'
import type { IWidget } from '@/lib/litegraph/src/types/widgets'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import {

View File

@@ -0,0 +1,97 @@
import { describe, expect, it } from 'vitest'
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
import { computeUnionBounds, gcd, lcm } from '@/utils/mathUtil'
describe('mathUtil', () => {
describe('gcd', () => {
it('should compute greatest common divisor correctly', () => {
expect(gcd(48, 18)).toBe(6)
expect(gcd(100, 25)).toBe(25)
expect(gcd(17, 13)).toBe(1)
expect(gcd(0, 5)).toBe(5)
expect(gcd(5, 0)).toBe(5)
})
})
describe('lcm', () => {
it('should compute least common multiple correctly', () => {
expect(lcm(4, 6)).toBe(12)
expect(lcm(15, 20)).toBe(60)
expect(lcm(7, 11)).toBe(77)
})
})
describe('computeUnionBounds', () => {
it('should return null for empty input', () => {
expect(computeUnionBounds([])).toBe(null)
})
// Tests for tuple format (ReadOnlyRect)
it('should work with ReadOnlyRect tuple format', () => {
const tuples: ReadOnlyRect[] = [
[10, 20, 30, 40] as const, // bounds: 10,20 to 40,60
[50, 10, 20, 30] as const // bounds: 50,10 to 70,40
]
const result = computeUnionBounds(tuples)
expect(result).toEqual({
x: 10, // min(10, 50)
y: 10, // min(20, 10)
width: 60, // max(40, 70) - min(10, 50) = 70 - 10
height: 50 // max(60, 40) - min(20, 10) = 60 - 10
})
})
it('should handle single ReadOnlyRect tuple', () => {
const tuple: ReadOnlyRect = [10, 20, 30, 40] as const
const result = computeUnionBounds([tuple])
expect(result).toEqual({
x: 10,
y: 20,
width: 30,
height: 40
})
})
it('should handle tuple format with negative dimensions', () => {
const tuples: ReadOnlyRect[] = [
[100, 50, -20, -10] as const, // x+width=80, y+height=40
[90, 45, 15, 20] as const // x+width=105, y+height=65
]
const result = computeUnionBounds(tuples)
expect(result).toEqual({
x: 90, // min(100, 90)
y: 45, // min(50, 45)
width: 15, // max(80, 105) - min(100, 90) = 105 - 90
height: 20 // max(40, 65) - min(50, 45) = 65 - 45
})
})
it('should maintain optimal performance with SoA tuples', () => {
// Test that array access is as expected for typical selection sizes
const tuples: ReadOnlyRect[] = Array.from(
{ length: 10 },
(_, i) =>
[
i * 20, // x
i * 15, // y
100 + i * 5, // width
80 + i * 3 // height
] as const
)
const result = computeUnionBounds(tuples)
expect(result).toBeTruthy()
expect(result!.x).toBe(0)
expect(result!.y).toBe(0)
expect(result!.width).toBe(325)
expect(result!.height).toBe(242)
})
})
})

View File

@@ -2,7 +2,7 @@ import fs from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import type { WorkflowJSON04 } from '@/schemas/comfyWorkflowSchema'
import type { WorkflowJSON04 } from '@/platform/workflow/validation/schemas/workflowSchema'
import { migrateLegacyRerouteNodes } from '@/utils/migration/migrateReroute'
describe('migrateReroute', () => {
@@ -19,7 +19,7 @@ describe('migrateReroute', () => {
'single_connected.json',
'floating.json',
'floating_branch.json'
])('should correctly migrate %s', (fileName) => {
])('should correctly migrate %s', async (fileName) => {
// Load the legacy workflow
const legacyWorkflow = loadWorkflow(
`workflows/reroute/legacy/${fileName}`
@@ -29,9 +29,9 @@ describe('migrateReroute', () => {
const migratedWorkflow = migrateLegacyRerouteNodes(legacyWorkflow)
// Compare with snapshot
expect(JSON.stringify(migratedWorkflow, null, 2)).toMatchFileSnapshot(
`workflows/reroute/native/${fileName}`
)
await expect(
JSON.stringify(migratedWorkflow, null, 2)
).toMatchFileSnapshot(`workflows/reroute/native/${fileName}`)
})
})
})

View File

@@ -0,0 +1,254 @@
import { describe, expect, it } from 'vitest'
import { normalizePackId, normalizePackKeys } from '@/utils/packUtils'
describe('packUtils', () => {
describe('normalizePackId', () => {
it('should return pack ID unchanged when no version suffix exists', () => {
expect(normalizePackId('ComfyUI-GGUF')).toBe('ComfyUI-GGUF')
expect(normalizePackId('ComfyUI-Manager')).toBe('ComfyUI-Manager')
expect(normalizePackId('simple-pack')).toBe('simple-pack')
})
it('should remove version suffix with underscores', () => {
expect(normalizePackId('ComfyUI-GGUF@1_1_4')).toBe('ComfyUI-GGUF')
expect(normalizePackId('ComfyUI-Manager@2_0_0')).toBe('ComfyUI-Manager')
expect(normalizePackId('pack@1_0_0_beta')).toBe('pack')
})
it('should remove version suffix with dots', () => {
expect(normalizePackId('ComfyUI-GGUF@1.1.4')).toBe('ComfyUI-GGUF')
expect(normalizePackId('pack@2.0.0')).toBe('pack')
})
it('should handle multiple @ symbols by only removing after first @', () => {
expect(normalizePackId('pack@1_0_0@extra')).toBe('pack')
expect(normalizePackId('my@pack@1_0_0')).toBe('my')
})
it('should handle empty string', () => {
expect(normalizePackId('')).toBe('')
})
it('should handle pack ID with @ but no version', () => {
expect(normalizePackId('pack@')).toBe('pack')
})
it('should handle special characters in pack name', () => {
expect(normalizePackId('my-pack_v2@1_0_0')).toBe('my-pack_v2')
expect(normalizePackId('pack.with.dots@2_0_0')).toBe('pack.with.dots')
expect(normalizePackId('UPPERCASE-Pack@1_0_0')).toBe('UPPERCASE-Pack')
})
it('should handle edge cases', () => {
// Only @ symbol
expect(normalizePackId('@')).toBe('')
expect(normalizePackId('@1_0_0')).toBe('')
// Whitespace
expect(normalizePackId(' pack @1_0_0')).toBe(' pack ')
expect(normalizePackId('pack @1_0_0')).toBe('pack ')
})
})
describe('normalizePackKeys', () => {
it('should normalize all keys with version suffixes', () => {
const input = {
'ComfyUI-GGUF': { ver: '1.1.4', enabled: true },
'ComfyUI-Manager@2_0_0': { ver: '2.0.0', enabled: false },
'another-pack@1_0_0': { ver: '1.0.0', enabled: true }
}
const expected = {
'ComfyUI-GGUF': { ver: '1.1.4', enabled: true },
'ComfyUI-Manager': { ver: '2.0.0', enabled: false },
'another-pack': { ver: '1.0.0', enabled: true }
}
expect(normalizePackKeys(input)).toEqual(expected)
})
it('should handle empty object', () => {
expect(normalizePackKeys({})).toEqual({})
})
it('should handle keys without version suffixes', () => {
const input = {
pack1: { data: 'value1' },
pack2: { data: 'value2' }
}
expect(normalizePackKeys(input)).toEqual(input)
})
it('should handle mixed keys (with and without versions)', () => {
const input = {
'normal-pack': { ver: '1.0.0' },
'versioned-pack@2_0_0': { ver: '2.0.0' },
'another-normal': { ver: '3.0.0' },
'another-versioned@4_0_0': { ver: '4.0.0' }
}
const expected = {
'normal-pack': { ver: '1.0.0' },
'versioned-pack': { ver: '2.0.0' },
'another-normal': { ver: '3.0.0' },
'another-versioned': { ver: '4.0.0' }
}
expect(normalizePackKeys(input)).toEqual(expected)
})
it('should handle duplicate keys after normalization (last one wins)', () => {
const input = {
'pack@1_0_0': { ver: '1.0.0', data: 'first' },
'pack@2_0_0': { ver: '2.0.0', data: 'second' },
pack: { ver: '3.0.0', data: 'third' }
}
const result = normalizePackKeys(input)
// The exact behavior depends on object iteration order,
// but there should only be one 'pack' key in the result
expect(Object.keys(result)).toEqual(['pack'])
expect(result.pack).toBeDefined()
expect(result.pack.ver).toBeDefined()
})
it('should preserve value references', () => {
const value1 = { ver: '1.0.0', complex: { nested: 'data' } }
const value2 = { ver: '2.0.0', complex: { nested: 'data2' } }
const input = {
'pack1@1_0_0': value1,
'pack2@2_0_0': value2
}
const result = normalizePackKeys(input)
// Values should be the same references, not cloned
expect(result.pack1).toBe(value1)
expect(result.pack2).toBe(value2)
})
it('should handle special characters in keys', () => {
const input = {
'@1_0_0': { ver: '1.0.0' },
'my-pack.v2@2_0_0': { ver: '2.0.0' },
'UPPERCASE@3_0_0': { ver: '3.0.0' }
}
const expected = {
'': { ver: '1.0.0' },
'my-pack.v2': { ver: '2.0.0' },
UPPERCASE: { ver: '3.0.0' }
}
expect(normalizePackKeys(input)).toEqual(expected)
})
it('should work with different value types', () => {
const input = {
'pack1@1_0_0': 'string value',
'pack2@2_0_0': 123,
'pack3@3_0_0': null,
'pack4@4_0_0': undefined,
'pack5@5_0_0': true,
pack6: []
}
const expected = {
pack1: 'string value',
pack2: 123,
pack3: null,
pack4: undefined,
pack5: true,
pack6: []
}
expect(normalizePackKeys(input)).toEqual(expected)
})
})
describe('Integration scenarios from JSDoc examples', () => {
it('should handle the examples from normalizePackId JSDoc', () => {
expect(normalizePackId('ComfyUI-GGUF')).toBe('ComfyUI-GGUF')
expect(normalizePackId('ComfyUI-GGUF@1_1_4')).toBe('ComfyUI-GGUF')
})
it('should handle the examples from normalizePackKeys JSDoc', () => {
const input = {
'ComfyUI-GGUF': { ver: '1.1.4', enabled: true },
'ComfyUI-Manager@2_0_0': { ver: '2.0.0', enabled: false }
}
const expected = {
'ComfyUI-GGUF': { ver: '1.1.4', enabled: true },
'ComfyUI-Manager': { ver: '2.0.0', enabled: false }
}
expect(normalizePackKeys(input)).toEqual(expected)
})
})
describe('Real-world scenarios', () => {
it('should handle typical ComfyUI-Manager response with mixed enabled/disabled packs', () => {
// Simulating actual server response pattern
const serverResponse = {
// Enabled packs come without version suffix
'ComfyUI-Essential': { ver: '1.2.3', enabled: true, aux_id: undefined },
'ComfyUI-Impact': { ver: '2.0.0', enabled: true, aux_id: undefined },
// Disabled packs come with version suffix
'ComfyUI-GGUF@1_1_4': {
ver: '1.1.4',
enabled: false,
aux_id: undefined
},
'ComfyUI-Manager@2_5_0': {
ver: '2.5.0',
enabled: false,
aux_id: undefined
}
}
const normalized = normalizePackKeys(serverResponse)
// All keys should be normalized (no version suffixes)
expect(Object.keys(normalized)).toEqual([
'ComfyUI-Essential',
'ComfyUI-Impact',
'ComfyUI-GGUF',
'ComfyUI-Manager'
])
// Values should be preserved
expect(normalized['ComfyUI-GGUF']).toEqual({
ver: '1.1.4',
enabled: false,
aux_id: undefined
})
})
it('should allow consistent access by pack ID regardless of enabled state', () => {
const packsBeforeToggle = {
'my-pack': { ver: '1.0.0', enabled: true }
}
const packsAfterToggle = {
'my-pack@1_0_0': { ver: '1.0.0', enabled: false }
}
const normalizedBefore = normalizePackKeys(packsBeforeToggle)
const normalizedAfter = normalizePackKeys(packsAfterToggle)
// Both should have the same key after normalization
expect(normalizedBefore['my-pack']).toBeDefined()
expect(normalizedAfter['my-pack']).toBeDefined()
// Can access by the same key regardless of the original format
expect(Object.keys(normalizedBefore)).toEqual(
Object.keys(normalizedAfter)
)
})
})
})

View File

@@ -0,0 +1,269 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { type Bounds, QuadTree } from '@/renderer/core/spatial/QuadTree'
describe('QuadTree', () => {
let quadTree: QuadTree<string>
const worldBounds: Bounds = { x: 0, y: 0, width: 1000, height: 1000 }
beforeEach(() => {
quadTree = new QuadTree<string>(worldBounds, {
maxDepth: 4,
maxItemsPerNode: 4
})
})
describe('insertion', () => {
it('should insert items within bounds', () => {
const success = quadTree.insert(
'node1',
{ x: 100, y: 100, width: 50, height: 50 },
'node1'
)
expect(success).toBe(true)
expect(quadTree.size).toBe(1)
})
it('should reject items outside bounds', () => {
const success = quadTree.insert(
'node1',
{ x: -100, y: -100, width: 50, height: 50 },
'node1'
)
expect(success).toBe(false)
expect(quadTree.size).toBe(0)
})
it('should handle duplicate IDs by replacing', () => {
quadTree.insert(
'node1',
{ x: 100, y: 100, width: 50, height: 50 },
'data1'
)
quadTree.insert(
'node1',
{ x: 200, y: 200, width: 50, height: 50 },
'data2'
)
expect(quadTree.size).toBe(1)
const results = quadTree.query({
x: 150,
y: 150,
width: 100,
height: 100
})
expect(results).toContain('data2')
expect(results).not.toContain('data1')
})
})
describe('querying', () => {
beforeEach(() => {
// Insert test nodes in a grid pattern
for (let x = 0; x < 10; x++) {
for (let y = 0; y < 10; y++) {
const id = `node_${x}_${y}`
quadTree.insert(
id,
{
x: x * 100,
y: y * 100,
width: 50,
height: 50
},
id
)
}
}
})
it('should find nodes within query bounds', () => {
const results = quadTree.query({ x: 0, y: 0, width: 250, height: 250 })
expect(results.length).toBe(9) // 3x3 grid
})
it('should return empty array for out-of-bounds query', () => {
const results = quadTree.query({
x: 2000,
y: 2000,
width: 100,
height: 100
})
expect(results.length).toBe(0)
})
it('should handle partial overlaps', () => {
const results = quadTree.query({ x: 25, y: 25, width: 100, height: 100 })
expect(results.length).toBe(4) // 2x2 grid due to overlap
})
it('should handle large query areas efficiently', () => {
const startTime = performance.now()
const results = quadTree.query({ x: 0, y: 0, width: 1000, height: 1000 })
const queryTime = performance.now() - startTime
expect(results.length).toBe(100) // All nodes
expect(queryTime).toBeLessThan(5) // Should be fast
})
})
describe('removal', () => {
it('should remove existing items', () => {
quadTree.insert(
'node1',
{ x: 100, y: 100, width: 50, height: 50 },
'node1'
)
expect(quadTree.size).toBe(1)
const success = quadTree.remove('node1')
expect(success).toBe(true)
expect(quadTree.size).toBe(0)
})
it('should handle removal of non-existent items', () => {
const success = quadTree.remove('nonexistent')
expect(success).toBe(false)
})
})
describe('updating', () => {
it('should update item position', () => {
quadTree.insert(
'node1',
{ x: 100, y: 100, width: 50, height: 50 },
'node1'
)
const success = quadTree.update('node1', {
x: 200,
y: 200,
width: 50,
height: 50
})
expect(success).toBe(true)
// Should not find at old position
const oldResults = quadTree.query({
x: 75,
y: 75,
width: 100,
height: 100
})
expect(oldResults).not.toContain('node1')
// Should find at new position
const newResults = quadTree.query({
x: 175,
y: 175,
width: 100,
height: 100
})
expect(newResults).toContain('node1')
})
})
describe('subdivision', () => {
it('should subdivide when exceeding max items', () => {
// Insert 5 items (max is 4) to trigger subdivision
for (let i = 0; i < 5; i++) {
quadTree.insert(
`node${i}`,
{
x: i * 10,
y: i * 10,
width: 5,
height: 5
},
`node${i}`
)
}
expect(quadTree.size).toBe(5)
// Verify all items can still be found
const allResults = quadTree.query(worldBounds)
expect(allResults.length).toBe(5)
})
})
describe('performance', () => {
it('should handle 1000 nodes efficiently', () => {
const insertStart = performance.now()
// Insert 1000 nodes
for (let i = 0; i < 1000; i++) {
const x = Math.random() * 900
const y = Math.random() * 900
quadTree.insert(
`node${i}`,
{
x,
y,
width: 50,
height: 50
},
`node${i}`
)
}
const insertTime = performance.now() - insertStart
expect(insertTime).toBeLessThan(50) // Should be fast
// Query performance
const queryStart = performance.now()
const results = quadTree.query({
x: 400,
y: 400,
width: 200,
height: 200
})
const queryTime = performance.now() - queryStart
expect(queryTime).toBeLessThan(2) // Queries should be very fast
expect(results.length).toBeGreaterThan(0)
expect(results.length).toBeLessThan(1000) // Should cull most nodes
})
})
describe('edge cases', () => {
it('should handle zero-sized bounds', () => {
const success = quadTree.insert(
'point',
{ x: 100, y: 100, width: 0, height: 0 },
'point'
)
expect(success).toBe(true)
const results = quadTree.query({ x: 99, y: 99, width: 2, height: 2 })
expect(results).toContain('point')
})
it('should handle items spanning multiple quadrants', () => {
const success = quadTree.insert(
'large',
{
x: 400,
y: 400,
width: 200,
height: 200
},
'large'
)
expect(success).toBe(true)
// Should be found when querying any overlapping quadrant
const topLeft = quadTree.query({ x: 0, y: 0, width: 500, height: 500 })
const bottomRight = quadTree.query({
x: 500,
y: 500,
width: 500,
height: 500
})
expect(topLeft).toContain('large')
expect(bottomRight).toContain('large')
})
})
})

View File

@@ -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'])
})
})
})

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { TreeNode } from '@/types/treeExplorerTypes'
import type { TreeNode } from '@/types/treeExplorerTypes'
import { buildTree, sortedTree } from '@/utils/treeUtil'
describe('buildTree', () => {

View File

@@ -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'
}
}))
})
})
})