mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 13:12:12 -03:00
- 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
231 lines
6.6 KiB
TypeScript
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])
|
|
}
|
|
}
|