feat: Complete manager migration with conflict detection integration

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

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

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

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

View File

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

View File

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

View File

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

View File

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