Files
malkuthro-ComfyUI-Koolook/tests/sidebar/test_setup_runner_simulator.py
T
malkuthro c3c34f0171 Capture API prompt when publishing setups (#233)
* feat(sidebar): capture API prompt when publishing setups

* feat(setups): preview and load canonical setup records

* fix(setups): close publish review gaps
2026-06-08 20:28:35 +02:00

408 lines
14 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_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