Files
malkuthro-ComfyUI-Koolook/tests/sidebar/test_setup_runner_simulator.py
T
malkuthro 7b00a551d9 feat(publish): independent output type for published setups; fix(pipeline): auto-versioning re-runs each queue (#267)
Publish: Koolook_PublishOutput output_mode/input_switch/switch; setupSurface.app.outputSwitch; routers auto-bound as the output selector; unwired output types hidden; 'Same as input' offered only when every input type has a wired writer; result path follows the output switch. Pipeline: EasyAIPipeline IS_CHANGED keeps version=auto/next perpetually dirty so the vNNN disk scan re-runs every queue; literal versions stay cacheable.
2026-07-19 23:34:30 +02:00

531 lines
18 KiB
Python

from __future__ import annotations
import subprocess
import textwrap
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
def run_node_scenario(source: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["node", "--input-type=module"],
input=source,
cwd=REPO_ROOT,
text=True,
capture_output=True,
check=False,
)
def test_simulator_lists_setup_and_polls_public_run_routes() -> None:
script = textwrap.dedent(
"""
import assert from "node:assert/strict";
import {
listPublishedSetups,
getPublishedSetup,
runAndPollPublishedSetup,
formatRun,
} from "./web/setup_runner_simulator.js";
const calls = [];
const responses = [
{ ok: true, status: 200, body: { ok: true, setups: [{ id: "director-demo", title: "Director Demo" }] } },
{ ok: true, status: 200, body: { id: "director-demo", metadata: { title: "Director Demo" } } },
{ ok: true, status: 200, body: { ok: true, run: { runId: "run-000001", promptId: "prompt-1", status: "queued" } } },
{ ok: true, status: 200, body: { ok: true, run: { runId: "run-000001", setupId: "director-demo", promptId: "prompt-1", status: "running", outputs: [] } } },
{ ok: true, status: 200, body: { ok: true, run: { runId: "run-000001", setupId: "director-demo", promptId: "prompt-1", status: "succeeded", comfyStatus: { completed: true }, outputs: [{ key: "video" }] } } },
];
const fetchImpl = async (url, options = {}) => {
calls.push({ url, options });
const response = responses.shift();
return {
ok: response.ok,
status: response.status,
async json() { return response.body; },
};
};
const setups = await listPublishedSetups({ baseUrl: "http://127.0.0.1:8188/", fetchImpl });
assert.deepEqual(setups, [{ id: "director-demo", title: "Director Demo" }]);
const setup = await getPublishedSetup({ setupId: "director-demo", baseUrl: "http://127.0.0.1:8188/", fetchImpl });
assert.equal(setup.id, "director-demo");
const updates = [];
const finalRun = await runAndPollPublishedSetup({
setupId: "director-demo",
inputs: { prompt: "A close-up" },
baseUrl: "http://127.0.0.1:8188/",
intervalMs: 1,
timeoutMs: 1000,
sleepImpl: async () => {},
onUpdate: run => updates.push(run.status),
fetchImpl,
});
assert.equal(finalRun.status, "succeeded");
assert.deepEqual(updates, ["queued", "running", "succeeded"]);
assert.equal(calls[0].url, "http://127.0.0.1:8188/koolook/api/setups");
assert.equal(calls[1].url, "http://127.0.0.1:8188/koolook/api/setups/director-demo");
assert.equal(calls[2].url, "http://127.0.0.1:8188/koolook/api/setups/director-demo/run");
assert.deepEqual(JSON.parse(calls[2].options.body), { inputs: { prompt: "A close-up" } });
assert.equal(calls[3].url, "http://127.0.0.1:8188/koolook/api/runs/run-000001");
assert.equal(calls[4].url, "http://127.0.0.1:8188/koolook/api/runs/run-000001");
assert.match(formatRun(finalRun), /Details:/);
assert.match(formatRun(finalRun), /comfyStatus/);
"""
)
result = run_node_scenario(script)
assert result.returncode == 0, result.stderr
def test_simulator_supports_independent_output_switch() -> None:
"""An app surface with an outputSwitch submits an output_switch value. The
'Same as input' sentinel (-1) follows the input switch; a concrete choice
overrides it — so an EXR source can be routed to a QT writer."""
script = textwrap.dedent(
"""
import assert from "node:assert/strict";
import {
defaultRunInputsFromSetup,
runInputsFromAppValues,
} from "./web/setup_runner_simulator.js";
const setup = {
setupSurface: {
app: {
switch: {
key: "switch",
default: 0,
options: [
{ value: 0, label: "EXR", input: "sequence_folder", visible: true },
{ value: 1, label: "QT", input: "qt_file", visible: true },
{ value: 2, label: "Img", input: "single_file", visible: true },
],
},
outputSwitch: {
key: "output_switch",
default: -1,
sameAsInput: true,
options: [
{ value: 0, label: "EXR", visible: true },
{ value: 1, label: "QT", visible: true },
{ value: 2, label: "Img", visible: true },
],
},
inputs: [{ key: "sequence_folder", default: "/plates/demo" }],
outputs: [{ key: "folder", default: "/renders/demo" }],
},
},
};
// Default: output follows input (EXR=0) via the "Same as input" sentinel.
assert.deepEqual(defaultRunInputsFromSetup(setup), {
switch: 0,
sequence_folder: "/plates/demo",
folder: "/renders/demo",
output_switch: 0,
});
// Explicit override: EXR source (switch 0), QT writer (output_switch 1).
assert.deepEqual(runInputsFromAppValues(setup, {
switch: 0,
output_switch: 1,
sequence_folder: "/plates/demo",
folder: "/renders/demo",
}), {
switch: 0,
sequence_folder: "/plates/demo",
folder: "/renders/demo",
output_switch: 1,
});
// "Same as input" (-1) submitted -> resolves to the chosen input (QT=1).
assert.deepEqual(runInputsFromAppValues(setup, {
switch: 1,
output_switch: -1,
sequence_folder: "/plates/demo",
folder: "/renders/demo",
}), {
switch: 1,
sequence_folder: "/plates/demo",
folder: "/renders/demo",
output_switch: 1,
});
"""
)
result = run_node_scenario(script)
assert result.returncode == 0, result.stderr
def test_simulator_times_out_when_active_status_never_progresses() -> None:
script = textwrap.dedent(
"""
import assert from "node:assert/strict";
import { runAndPollPublishedSetup } from "./web/setup_runner_simulator.js";
let now = 0;
const originalNow = Date.now;
Date.now = () => now;
try {
const fetchImpl = async (url, options = {}) => {
const isRunRequest = options.method === "POST";
return {
ok: true,
status: 200,
async json() {
return isRunRequest
? { ok: true, run: { runId: "run-000001", promptId: "prompt-1", status: "queued" } }
: { ok: true, run: { runId: "run-000001", promptId: "prompt-1", status: "running", outputs: [] } };
},
};
};
await assert.rejects(
() => runAndPollPublishedSetup({
setupId: "director-demo",
fetchImpl,
intervalMs: 1,
timeoutMs: 5,
sleepImpl: async ms => { now += ms; },
}),
/Timed out waiting for run/
);
} finally {
Date.now = originalNow;
}
"""
)
result = run_node_scenario(script)
assert result.returncode == 0, result.stderr
def test_simulator_accepts_bare_array_catalog_response() -> None:
script = textwrap.dedent(
"""
import assert from "node:assert/strict";
import { listPublishedSetups } from "./web/setup_runner_simulator.js";
const setups = await listPublishedSetups({
fetchImpl: async () => ({
ok: true,
status: 200,
async json() {
return [{ id: "mask_rmgb", metadata: { title: "Mask_RMGB" } }];
},
}),
});
assert.deepEqual(setups, [{ id: "mask_rmgb", metadata: { title: "Mask_RMGB" } }]);
"""
)
result = run_node_scenario(script)
assert result.returncode == 0, result.stderr
def test_simulator_accepts_canonical_setup_file_shapes() -> None:
script = textwrap.dedent(
"""
import assert from "node:assert/strict";
import { setupsFromPublishedSetupFile } from "./web/setup_runner_simulator.js";
const setup = {
schemaVersion: 1,
id: "rmgb-publish-v04",
metadata: { title: "RMGB Publish v04" },
visualGraph: { nodes: [] },
apiPrompt: { "12": { class_type: "Text Multiline", inputs: { text: "demo" } } },
inputContract: { inputs: [] },
outputContract: { outputs: [] },
setupSurface: { app: { inputs: [], outputs: [], results: [] } },
source: { kind: "sidebar-workflow", path: "Models/RMGB/rmgb-publish-v04", inventoryPath: ["Models", "RMGB"], name: "rmgb-publish-v04" },
validation: { status: "valid", diagnostics: [] },
};
assert.deepEqual(setupsFromPublishedSetupFile(setup), [setup]);
assert.deepEqual(setupsFromPublishedSetupFile({ setups: [setup] }), [setup]);
assert.deepEqual(setupsFromPublishedSetupFile([setup]), [setup]);
assert.throws(
() => setupsFromPublishedSetupFile({ nope: [] }),
/published setup record/
);
assert.throws(
() => setupsFromPublishedSetupFile({ ...setup, apiPrompt: null }),
/apiPrompt/
);
assert.throws(
() => setupsFromPublishedSetupFile({ ...setup, apiPrompt: [] }),
/apiPrompt/
);
"""
)
result = run_node_scenario(script)
assert result.returncode == 0, result.stderr
def test_simulator_marks_local_setup_files_as_not_runnable() -> None:
script = textwrap.dedent(
"""
import assert from "node:assert/strict";
import {
localSetupFileRecords,
setupCanRunFromRegistry,
localSetupRunMessage,
} from "./web/setup_runner_simulator.js";
const setup = {
schemaVersion: 1,
id: "file-only",
metadata: { title: "File Only" },
visualGraph: { nodes: [] },
apiPrompt: { "1": { class_type: "Example", inputs: {} } },
inputContract: { inputs: [] },
outputContract: { outputs: [] },
setupSurface: { app: { inputs: [], outputs: [], results: [] } },
source: { kind: "sidebar-workflow", path: "File/file-only", inventoryPath: ["File"], name: "file-only" },
validation: { status: "valid", diagnostics: [] },
};
assert.equal(setupCanRunFromRegistry(setup), true);
const [local] = localSetupFileRecords({ setups: [setup] });
assert.equal(setupCanRunFromRegistry(local), false);
assert.match(localSetupRunMessage(local), /loaded from a file/i);
assert.deepEqual(setupCanRunFromRegistry(setup), true);
"""
)
result = run_node_scenario(script)
assert result.returncode == 0, result.stderr
def test_simulator_preserves_error_payload_for_debugging() -> None:
script = textwrap.dedent(
"""
import assert from "node:assert/strict";
import { runPublishedSetup, formatError } from "./web/setup_runner_simulator.js";
await assert.rejects(
() => runPublishedSetup({
setupId: "director-demo",
fetchImpl: async () => ({
ok: false,
status: 400,
async json() {
return { ok: false, errors: ["required input 'prompt' is missing"] };
},
}),
}),
error => {
assert.equal(error.message, "required input 'prompt' is missing");
assert.equal(error.status, 400);
assert.deepEqual(error.payload, {
ok: false,
errors: ["required input 'prompt' is missing"],
});
assert.match(formatError(error), /required input/);
return true;
}
);
"""
)
result = run_node_scenario(script)
assert result.returncode == 0, result.stderr
def test_simulator_rejects_malformed_success_responses() -> None:
script = textwrap.dedent(
"""
import assert from "node:assert/strict";
import {
listPublishedSetups,
runPublishedSetup,
getPublishedSetupRun,
} from "./web/setup_runner_simulator.js";
await assert.rejects(
() => listPublishedSetups({
fetchImpl: async () => ({
ok: true,
status: 200,
async json() { throw new Error("invalid json"); },
}),
}),
/Response was not valid JSON/
);
await assert.rejects(
() => listPublishedSetups({
fetchImpl: async () => ({
ok: true,
status: 200,
async json() { return { ok: true }; },
}),
}),
/setups array/
);
await assert.rejects(
() => runPublishedSetup({
setupId: "director-demo",
fetchImpl: async () => ({
ok: true,
status: 200,
async json() { return { ok: true }; },
}),
}),
/run object/
);
await assert.rejects(
() => runPublishedSetup({
setupId: "director-demo",
fetchImpl: async () => ({
ok: true,
status: 200,
async json() { return { ok: true, run: {} }; },
}),
}),
/Missing runId/
);
await assert.rejects(
() => runPublishedSetup({
setupId: "director-demo",
fetchImpl: async () => ({
ok: true,
status: 200,
async json() { return { ok: true, run: { runId: "run-000001", status: "queued" } }; },
}),
}),
/Missing promptId/
);
await assert.rejects(
() => getPublishedSetupRun({
runId: "run-000001",
fetchImpl: async () => ({
ok: true,
status: 200,
async json() { return { ok: true }; },
}),
}),
/run object/
);
await assert.rejects(
() => getPublishedSetupRun({
runId: "run-000001",
fetchImpl: async () => ({
ok: true,
status: 200,
async json() { return { ok: true, run: {} }; },
}),
}),
/Missing runId/
);
"""
)
result = run_node_scenario(script)
assert result.returncode == 0, result.stderr
def test_simulator_prefers_catalog_metadata_title() -> None:
script = textwrap.dedent(
"""
import assert from "node:assert/strict";
import { setupDisplayTitle } from "./web/setup_runner_simulator.js";
assert.equal(setupDisplayTitle({
id: "rmgb-publish-v04",
metadata: { title: "RMGB Publish v04" },
}), "RMGB Publish v04");
assert.equal(setupDisplayTitle({ id: "fallback-id", title: "Legacy Title" }), "Legacy Title");
assert.equal(setupDisplayTitle({ id: "fallback-id" }), "fallback-id");
"""
)
result = run_node_scenario(script)
assert result.returncode == 0, result.stderr
def test_simulator_builds_external_app_inputs_from_setup_surface() -> None:
script = textwrap.dedent(
"""
import assert from "node:assert/strict";
import {
defaultRunInputsFromSetup,
runInputsFromAppValues,
setupHasAppSurface,
summarizeRunResultLines,
} from "./web/setup_runner_simulator.js";
const setup = {
setupSurface: {
app: {
switch: {
key: "switch",
default: 2,
options: [
{ value: 0, label: "EXR", input: "sequence_folder", visible: true },
{ value: 1, label: "QT", input: "qt_file", visible: true },
{ value: 2, label: "Img", input: "single_file", visible: true },
{ value: 3, label: "Prompt", input: "prompt", visible: false },
],
},
inputs: [
{ key: "sequence_folder", default: "/plates/demo" },
{ key: "qt_file", default: "/plates/demo/source.mov" },
{ key: "single_file", default: "/plates/demo/source.png" },
{ key: "prompt", visible: false, default: "hidden prompt" },
],
outputs: [
{ key: "folder", default: "/renders/demo" },
{ key: "name", default: "demo" },
{ key: "version", default: "1" },
],
},
},
};
assert.equal(setupHasAppSurface(setup), true);
assert.deepEqual(defaultRunInputsFromSetup(setup), {
switch: 2,
single_file: "/plates/demo/source.png",
folder: "/renders/demo",
name: "demo",
version: "1",
});
assert.deepEqual(runInputsFromAppValues(setup, {
switch: 1,
qt_file: "/plates/demo/shot_010.mov",
folder: "/custom/out",
}), {
switch: 1,
qt_file: "/plates/demo/shot_010.mov",
folder: "/custom/out",
name: "demo",
version: "1",
});
assert.deepEqual(summarizeRunResultLines({
outputs: [
{
key: "result",
label: "Result",
items: [{ nodeId: "300", kind: "text", value: "/custom/out/demo_v001.mov" }],
},
],
}), ["Result: /custom/out/demo_v001.mov"]);
"""
)
result = run_node_scenario(script)
assert result.returncode == 0, result.stderr