Files
ComfyUI-Lora-Manager/vue-widgets/tests/utils/BatchQueueSimulator.ts
Will Miao e17d6c8ebf feat(testing): enhance test configuration and add Vue component tests
- Update package.json test script to run both JS and Vue tests
- Simplify LoraCyclerLM output by removing redundant lora name fallback
- Extend Vitest config to include TypeScript test files
- Add Vue testing dependencies and setup for component testing
- Implement comprehensive test suite for BatchQueueSimulator component
- Add test setup file with global mocks for ComfyUI modules
2026-02-01 00:59:50 +08:00

231 lines
6.6 KiB
TypeScript

/**
* BatchQueueSimulator - Simulates ComfyUI's two execution modes
*
* ComfyUI has two distinct execution patterns:
* 1. Batch Queue Mode: ALL beforeQueued calls happen BEFORE any onExecuted calls
* 2. Sequential Mode: beforeQueued and onExecuted interleave for each prompt
*
* This simulator helps test how the widget behaves in both modes.
*/
import type { CyclerConfig } from '@/composables/types'
export interface ExecutionHooks {
/** Called when a prompt is queued (before execution) */
beforeQueued: () => void
/** Called when execution completes with output */
onExecuted: (output: unknown) => void
}
export interface SimulatorOptions {
/** Total number of LoRAs in the pool */
totalCount: number
/** Function to generate output for each execution */
generateOutput?: (executionIndex: number, config: CyclerConfig) => unknown
}
/**
* Creates execution output based on the current state
*/
function defaultGenerateOutput(executionIndex: number, config: CyclerConfig) {
// Calculate what the next index would be after this execution
let nextIdx = (config.execution_index ?? config.current_index) + 1
if (nextIdx > config.total_count) {
nextIdx = 1
}
return {
next_index: [nextIdx],
total_count: [config.total_count],
next_lora_name: [`lora${nextIdx}.safetensors`],
next_lora_filename: [`lora${nextIdx}.safetensors`],
current_lora_name: [`lora${config.execution_index ?? config.current_index}.safetensors`],
current_lora_filename: [`lora${config.execution_index ?? config.current_index}.safetensors`]
}
}
export class BatchQueueSimulator {
private executionCount = 0
private options: Required<SimulatorOptions>
constructor(options: SimulatorOptions) {
this.options = {
totalCount: options.totalCount,
generateOutput: options.generateOutput ?? defaultGenerateOutput
}
}
/**
* Reset the simulator state
*/
reset() {
this.executionCount = 0
}
/**
* Simulates Batch Queue Mode execution
*
* In this mode, ComfyUI queues multiple prompts at once:
* - ALL beforeQueued() calls happen first (for all prompts in the batch)
* - THEN all onExecuted() calls happen (as each prompt completes)
*
* This is the mode used when queueing multiple prompts from the UI.
*
* @param count Number of prompts to simulate
* @param hooks The widget's execution hooks
* @param getConfig Function to get current widget config state
*/
async runBatchQueue(
count: number,
hooks: ExecutionHooks,
getConfig: () => CyclerConfig
): Promise<void> {
// Phase 1: All beforeQueued calls (snapshot configs)
const snapshotConfigs: CyclerConfig[] = []
for (let i = 0; i < count; i++) {
hooks.beforeQueued()
// Snapshot the config after beforeQueued updates it
snapshotConfigs.push({ ...getConfig() })
}
// Phase 2: All onExecuted calls (in order)
for (let i = 0; i < count; i++) {
const config = snapshotConfigs[i]
const output = this.options.generateOutput(this.executionCount, config)
hooks.onExecuted(output)
this.executionCount++
}
}
/**
* Simulates Sequential Mode execution
*
* In this mode, execution is one-at-a-time:
* - beforeQueued() is called
* - onExecuted() is called
* - Then the next prompt's beforeQueued() is called
* - And so on...
*
* This is the mode used in API-driven execution or single prompt queuing.
*
* @param count Number of prompts to simulate
* @param hooks The widget's execution hooks
* @param getConfig Function to get current widget config state
*/
async runSequential(
count: number,
hooks: ExecutionHooks,
getConfig: () => CyclerConfig
): Promise<void> {
for (let i = 0; i < count; i++) {
// Queue the prompt
hooks.beforeQueued()
const config = { ...getConfig() }
// Execute it immediately
const output = this.options.generateOutput(this.executionCount, config)
hooks.onExecuted(output)
this.executionCount++
}
}
/**
* Simulates a single execution (queue + execute)
*/
async runSingle(
hooks: ExecutionHooks,
getConfig: () => CyclerConfig
): Promise<void> {
return this.runSequential(1, hooks, getConfig)
}
/**
* Simulates interrupted execution (some beforeQueued calls without matching onExecuted)
*
* This can happen if the user cancels execution mid-batch.
*
* @param queuedCount Number of prompts queued (beforeQueued called)
* @param executedCount Number of prompts that actually executed
* @param hooks The widget's execution hooks
* @param getConfig Function to get current widget config state
*/
async runInterrupted(
queuedCount: number,
executedCount: number,
hooks: ExecutionHooks,
getConfig: () => CyclerConfig
): Promise<void> {
if (executedCount > queuedCount) {
throw new Error('executedCount cannot be greater than queuedCount')
}
// Phase 1: All beforeQueued calls
const snapshotConfigs: CyclerConfig[] = []
for (let i = 0; i < queuedCount; i++) {
hooks.beforeQueued()
snapshotConfigs.push({ ...getConfig() })
}
// Phase 2: Only some onExecuted calls
for (let i = 0; i < executedCount; i++) {
const config = snapshotConfigs[i]
const output = this.options.generateOutput(this.executionCount, config)
hooks.onExecuted(output)
this.executionCount++
}
}
}
/**
* Helper to create execution hooks from a widget-like object
*/
export function createHooksFromWidget(widget: {
beforeQueued?: () => void
}, node: {
onExecuted?: (output: unknown) => void
}): ExecutionHooks {
return {
beforeQueued: () => widget.beforeQueued?.(),
onExecuted: (output) => node.onExecuted?.(output)
}
}
/**
* Tracks index history during simulation for assertions
*/
export class IndexTracker {
public indexHistory: number[] = []
public repeatHistory: number[] = []
public pauseHistory: boolean[] = []
reset() {
this.indexHistory = []
this.repeatHistory = []
this.pauseHistory = []
}
record(config: CyclerConfig) {
this.indexHistory.push(config.current_index)
this.repeatHistory.push(config.repeat_used)
this.pauseHistory.push(config.is_paused)
}
/**
* Get the sequence of indices that were actually used for execution
*/
getExecutionIndices(): number[] {
return this.indexHistory
}
/**
* Verify that indices cycle correctly through totalCount
*/
verifyCyclePattern(expectedPattern: number[]): boolean {
if (this.indexHistory.length !== expectedPattern.length) {
return false
}
return this.indexHistory.every((idx, i) => idx === expectedPattern[i])
}
}