Download ComfyUI/custom_nodes/koolook/tests/server/test_published_setup_runner.py from vidfom/aimusic: direct link, hf CLI and curl.
- Browser
- Download file 46 kB
-
https://huggingface.co/vidfom/aimusic/resolve/main/ComfyUI/custom_nodes/koolook/tests/server/test_published_setup_runner.py
- Command line
-
hf download hf://vidfom/aimusic/ComfyUI/custom_nodes/koolook/tests/server/test_published_setup_runner.py
-
curl -L -o test_published_setup_runner.py https://huggingface.co/vidfom/aimusic/resolve/main/ComfyUI/custom_nodes/koolook/tests/server/test_published_setup_runner.py
46 kB
| from __future__ import annotations | |
| import asyncio | |
| from copy import deepcopy | |
| import pytest | |
| from koolook_setup_runner import PublishedSetupRunner, SetupRunError | |
| from koolook_setups import PublishedSetupRegistry, StaticSetupStorage | |
| from tests.server.test_published_setup_registry import _valid_setup | |
| class FakeComfyClient: | |
| def __init__(self, prompt_response=None, *, history=None, queue=None, queue_error=None) -> None: | |
| self.prompt_response = prompt_response or {"prompt_id": "comfy-prompt-1"} | |
| self.submitted_prompts: list[dict] = [] | |
| self.history = history or {} | |
| self.queue = queue or {"queue_running": [], "queue_pending": []} | |
| self.queue_error = queue_error | |
| async def queue_prompt(self, prompt: dict) -> dict: | |
| if self.queue_error is not None: | |
| raise self.queue_error | |
| self.submitted_prompts.append(prompt) | |
| return self.prompt_response | |
| async def get_history(self, prompt_id: str) -> dict: | |
| return {prompt_id: self.history[prompt_id]} if prompt_id in self.history else {} | |
| async def get_queue(self) -> dict: | |
| return self.queue | |
| def test_run_setup_injects_declared_inputs_into_cloned_prompt() -> None: | |
| async def exercise() -> None: | |
| setup = _valid_setup() | |
| original_prompt = deepcopy(setup["apiPrompt"]) | |
| registry = PublishedSetupRegistry(StaticSetupStorage([setup])) | |
| comfy = FakeComfyClient() | |
| runner = PublishedSetupRunner(registry, comfy) | |
| result = await runner.runSetup("ltx-director-demo", {"prompt": "external prompt"}) | |
| assert result == { | |
| "runId": "run-000001", | |
| "promptId": "comfy-prompt-1", | |
| "status": "queued", | |
| } | |
| assert comfy.submitted_prompts == [ | |
| {"12": {"class_type": "Text Multiline", "inputs": {"text": "external prompt"}}} | |
| ] | |
| assert setup["apiPrompt"] == original_prompt | |
| asyncio.run(exercise()) | |
| def test_run_setup_rejects_inputs_not_declared_by_contract() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([_valid_setup()])) | |
| runner = PublishedSetupRunner(registry, FakeComfyClient()) | |
| with pytest.raises(SetupRunError) as exc_info: | |
| await runner.runSetup("ltx-director-demo", {"prompt": "ok", "seed": 123}) | |
| assert exc_info.value.status_code == 400 | |
| assert exc_info.value.errors == ["input 'seed' is not declared by this setup"] | |
| asyncio.run(exercise()) | |
| def test_run_setup_rejects_non_callable_setup() -> None: | |
| async def exercise() -> None: | |
| setup = _valid_setup() | |
| setup["apiPrompt"] = None | |
| setup["validation"] = {"status": "draft", "diagnostics": ["API prompt conversion pending."]} | |
| registry = PublishedSetupRegistry(StaticSetupStorage([setup])) | |
| runner = PublishedSetupRunner(registry, FakeComfyClient()) | |
| with pytest.raises(SetupRunError) as exc_info: | |
| await runner.runSetup("ltx-director-demo", {"prompt": "external prompt"}) | |
| assert exc_info.value.status_code == 400 | |
| assert exc_info.value.errors == ["Published setup 'ltx-director-demo' is not callable."] | |
| asyncio.run(exercise()) | |
| def test_run_setup_translates_comfy_queue_failures() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([_valid_setup()])) | |
| runner = PublishedSetupRunner(registry, FakeComfyClient(queue_error=RuntimeError("server down"))) | |
| with pytest.raises(SetupRunError) as exc_info: | |
| await runner.runSetup("ltx-director-demo", {"prompt": "external prompt"}) | |
| assert exc_info.value.status_code == 502 | |
| assert exc_info.value.errors == ["ComfyUI queue request failed: server down"] | |
| asyncio.run(exercise()) | |
| def test_get_run_reports_running_state_from_comfy_queue() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([_valid_setup()])) | |
| runner = PublishedSetupRunner( | |
| registry, | |
| FakeComfyClient(queue={"queue_running": [[7, "comfy-prompt-1", {}]], "queue_pending": []}), | |
| ) | |
| queued = await runner.runSetup("ltx-director-demo", {"prompt": "external prompt"}) | |
| status = await runner.getRun(queued["runId"]) | |
| assert status == { | |
| "runId": "run-000001", | |
| "setupId": "ltx-director-demo", | |
| "promptId": "comfy-prompt-1", | |
| "status": "running", | |
| "outputs": [], | |
| } | |
| asyncio.run(exercise()) | |
| def test_get_run_reports_lost_when_prompt_is_missing_from_history_and_queue() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([_valid_setup()])) | |
| runner = PublishedSetupRunner( | |
| registry, | |
| FakeComfyClient(queue={"queue_running": [], "queue_pending": []}), | |
| ) | |
| queued = await runner.runSetup("ltx-director-demo", {"prompt": "external prompt"}) | |
| status = await runner.getRun(queued["runId"]) | |
| assert status == { | |
| "runId": "run-000001", | |
| "setupId": "ltx-director-demo", | |
| "promptId": "comfy-prompt-1", | |
| "status": "lost", | |
| "outputs": [], | |
| } | |
| asyncio.run(exercise()) | |
| def test_get_run_preserves_comfy_history_status_payload() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([_valid_setup()])) | |
| comfy_status = { | |
| "completed": False, | |
| "status_str": "error", | |
| "messages": [["execution_error", {"node_id": "12", "exception_message": "missing file"}]], | |
| } | |
| runner = PublishedSetupRunner( | |
| registry, | |
| FakeComfyClient( | |
| history={ | |
| "comfy-prompt-1": { | |
| "status": comfy_status, | |
| "outputs": {}, | |
| } | |
| } | |
| ), | |
| ) | |
| queued = await runner.runSetup("ltx-director-demo", {"prompt": "external prompt"}) | |
| status = await runner.getRun(queued["runId"]) | |
| assert status == { | |
| "runId": "run-000001", | |
| "setupId": "ltx-director-demo", | |
| "promptId": "comfy-prompt-1", | |
| "status": "failed", | |
| "comfyStatus": comfy_status, | |
| "outputs": [{"key": "video", "label": "Video", "type": "video", "items": []}], | |
| } | |
| asyncio.run(exercise()) | |
| def test_run_setup_accepts_inputs_declared_by_app_surface_contract() -> None: | |
| async def exercise() -> None: | |
| setup = _valid_setup() | |
| setup["inputContract"] = {"inputs": []} | |
| setup["outputContract"] = {"outputs": []} | |
| setup["visualGraph"]["groups"] = [ | |
| {"title": "Koolook Input", "bounding": [0, 0, 300, 200]}, | |
| {"title": "Koolook Output", "bounding": [400, 0, 300, 200]}, | |
| ] | |
| setup["visualGraph"]["nodes"][0]["pos"] = [20, 20] | |
| setup["setupSurface"] = { | |
| "sourceInputs": [ | |
| { | |
| "group": "Koolook Input", | |
| "nodes": [{"id": "12", "type": "Text Multiline", "title": "Prompt"}], | |
| } | |
| ], | |
| "outputs": [ | |
| { | |
| "group": "Koolook Output", | |
| "nodes": [{"id": "20", "type": "Preview Image", "title": "Preview"}], | |
| } | |
| ], | |
| "controls": [], | |
| "app": { | |
| "inputs": [ | |
| { | |
| "key": "prompt", | |
| "label": "Prompt", | |
| "visible": True, | |
| "target": {"node": "12", "input": "text"}, | |
| "default": "demo prompt", | |
| } | |
| ], | |
| "outputs": [], | |
| "results": [], | |
| }, | |
| } | |
| registry = PublishedSetupRegistry(StaticSetupStorage([setup])) | |
| comfy = FakeComfyClient() | |
| runner = PublishedSetupRunner(registry, comfy) | |
| await runner.runSetup("ltx-director-demo", {"prompt": "app contract prompt"}) | |
| assert comfy.submitted_prompts == [ | |
| {"12": {"class_type": "Text Multiline", "inputs": {"text": "app contract prompt"}}} | |
| ] | |
| asyncio.run(exercise()) | |
| def test_run_setup_accepts_output_controls_declared_by_app_surface() -> None: | |
| async def exercise() -> None: | |
| setup = _valid_setup() | |
| setup["inputContract"] = {"inputs": []} | |
| setup["outputContract"] = {"outputs": []} | |
| setup["visualGraph"] = { | |
| "nodes": [ | |
| { | |
| "id": 200, | |
| "type": "Koolook_PublishOutput", | |
| "title": "Koolook Publish Output", | |
| "pos": [20, 20], | |
| "size": [320, 160], | |
| "inputs": [], | |
| "widgets_values": ["/shots/example/output", "demo", "1"], | |
| } | |
| ], | |
| "links": [], | |
| "groups": [ | |
| {"title": "Koolook Input", "bounding": [0, 0, 80, 80]}, | |
| {"title": "Koolook Output", "bounding": [0, 0, 380, 220]}, | |
| ], | |
| } | |
| setup["apiPrompt"] = { | |
| "200": { | |
| "class_type": "Koolook_PublishOutput", | |
| "inputs": { | |
| "folder": "/shots/example/output", | |
| "name": "demo", | |
| "version": "1", | |
| }, | |
| } | |
| } | |
| setup["setupSurface"] = { | |
| "sourceInputs": [{"group": "Koolook Input", "nodes": [{"id": "200", "type": "Koolook_PublishOutput", "title": "Koolook Publish Output"}]}], | |
| "outputs": [{"group": "Koolook Output", "nodes": [{"id": "200", "type": "Koolook_PublishOutput", "title": "Koolook Publish Output"}]}], | |
| "controls": [], | |
| "app": { | |
| "inputs": [], | |
| "outputs": [ | |
| { | |
| "key": "folder", | |
| "label": "Output folder", | |
| "visible": True, | |
| "target": {"node": "200", "input": "folder"}, | |
| "default": "/shots/example/output", | |
| } | |
| ], | |
| "results": [], | |
| }, | |
| } | |
| registry = PublishedSetupRegistry(StaticSetupStorage([setup])) | |
| comfy = FakeComfyClient() | |
| runner = PublishedSetupRunner(registry, comfy) | |
| await runner.runSetup("ltx-director-demo", {"folder": "/shots/custom-output"}) | |
| assert comfy.submitted_prompts == [ | |
| { | |
| "200": { | |
| "class_type": "Koolook_PublishOutput", | |
| "inputs": { | |
| "folder": "/shots/custom-output", | |
| "name": "demo", | |
| "version": "1", | |
| }, | |
| } | |
| } | |
| ] | |
| asyncio.run(exercise()) | |
| def test_get_run_reports_app_surface_results_without_output_contract() -> None: | |
| async def exercise() -> None: | |
| setup = _valid_setup() | |
| setup["inputContract"] = {"inputs": []} | |
| setup["outputContract"] = {"outputs": []} | |
| setup["visualGraph"] = { | |
| "nodes": [ | |
| { | |
| "id": 100, | |
| "type": "Koolook_PublishInput", | |
| "title": "Koolook Publish Input", | |
| "pos": [20, 20], | |
| "size": [320, 240], | |
| "inputs": [], | |
| "widgets_values": ["Img", "", "", "", ""], | |
| }, | |
| { | |
| "id": 200, | |
| "type": "Koolook_PublishOutput", | |
| "title": "Koolook Publish Output", | |
| "pos": [420, 20], | |
| "size": [320, 160], | |
| "inputs": [], | |
| "widgets_values": ["/shots/example/output", "demo", "1"], | |
| }, | |
| { | |
| "id": 300, | |
| "type": "Koolook_PublishResult", | |
| "title": "Koolook Publish Result", | |
| "pos": [420, 220], | |
| "size": [320, 120], | |
| "inputs": [], | |
| "widgets_values": ["/shots/example/output/default.mov"], | |
| }, | |
| ], | |
| "links": [], | |
| "groups": [ | |
| {"title": "Koolook Input", "bounding": [0, 0, 360, 300]}, | |
| {"title": "Koolook Output", "bounding": [400, 0, 380, 380]}, | |
| ], | |
| } | |
| setup["apiPrompt"] = { | |
| "100": { | |
| "class_type": "Koolook_PublishInput", | |
| "inputs": { | |
| "mode": "Img", | |
| "sequence_folder": "", | |
| "qt_file": "", | |
| "single_file": "", | |
| "prompt": "", | |
| }, | |
| }, | |
| "200": { | |
| "class_type": "Koolook_PublishOutput", | |
| "inputs": { | |
| "folder": "/shots/example/output", | |
| "name": "demo", | |
| "version": "1", | |
| }, | |
| }, | |
| "300": { | |
| "class_type": "Koolook_PublishResult", | |
| "inputs": {"result": "/shots/example/output/default.mov"}, | |
| }, | |
| } | |
| setup["setupSurface"] = { | |
| "sourceInputs": [ | |
| { | |
| "group": "Koolook Input", | |
| "nodes": [{"id": "100", "type": "Koolook_PublishInput", "title": "Koolook Publish Input"}], | |
| } | |
| ], | |
| "outputs": [ | |
| { | |
| "group": "Koolook Output", | |
| "nodes": [ | |
| {"id": "200", "type": "Koolook_PublishOutput", "title": "Koolook Publish Output"}, | |
| {"id": "300", "type": "Koolook_PublishResult", "title": "Koolook Publish Result"}, | |
| ], | |
| } | |
| ], | |
| "controls": [], | |
| "app": { | |
| "inputs": [], | |
| "outputs": [ | |
| { | |
| "key": "folder", | |
| "label": "Output folder", | |
| "visible": True, | |
| "target": {"node": "200", "input": "folder"}, | |
| "default": "/shots/example/output", | |
| } | |
| ], | |
| "results": [ | |
| { | |
| "key": "result", | |
| "label": "Result", | |
| "visible": True, | |
| "target": {"node": "300", "input": "result"}, | |
| "default": "/shots/example/output/default.mov", | |
| } | |
| ], | |
| }, | |
| } | |
| registry = PublishedSetupRegistry(StaticSetupStorage([setup])) | |
| runner = PublishedSetupRunner( | |
| registry, | |
| FakeComfyClient( | |
| history={ | |
| "comfy-prompt-1": { | |
| "status": {"completed": True, "status_str": "success"}, | |
| "outputs": { | |
| "300": { | |
| "text": ["/shots/example/output/generated.mov"] | |
| } | |
| }, | |
| } | |
| } | |
| ), | |
| ) | |
| queued = await runner.runSetup("ltx-director-demo", {}) | |
| status = await runner.getRun(queued["runId"]) | |
| assert status["outputs"] == [ | |
| { | |
| "key": "folder", | |
| "label": "Output folder", | |
| "type": "output", | |
| "visible": True, | |
| "target": {"node": "200", "input": "folder"}, | |
| "default": "/shots/example/output", | |
| "items": [], | |
| }, | |
| { | |
| "key": "result", | |
| "label": "Result", | |
| "type": "result", | |
| "visible": True, | |
| "target": {"node": "300", "input": "result"}, | |
| "default": "/shots/example/output/default.mov", | |
| "items": [ | |
| { | |
| "nodeId": "300", | |
| "kind": "text", | |
| "value": "/shots/example/output/generated.mov", | |
| } | |
| ], | |
| }, | |
| ] | |
| asyncio.run(exercise()) | |
| def test_run_setup_uses_execution_map_router_to_keep_selected_writer() -> None: | |
| async def exercise() -> None: | |
| setup = _valid_setup() | |
| setup["inputContract"] = {"inputs": []} | |
| setup["outputContract"] = {"outputs": []} | |
| setup["visualGraph"] = { | |
| "nodes": [ | |
| { | |
| "id": 100, | |
| "type": "Koolook_PublishInput", | |
| "inputs": [ | |
| {"name": "mode", "widget": {"name": "mode"}}, | |
| {"name": "single_file", "widget": {"name": "single_file"}}, | |
| ], | |
| }, | |
| {"id": 200, "type": "Koolook_PublishOutput", "inputs": []}, | |
| {"id": 300, "type": "RMBG", "inputs": [{"name": "image", "link": 1}]}, | |
| {"id": 400, "type": "Koolook_PublishRouter", "inputs": [{"name": "selector"}, {"name": "payload"}]}, | |
| {"id": 301, "type": "EasyAIPipeline", "inputs": []}, | |
| {"id": 302, "type": "EasyAIPipeline", "inputs": []}, | |
| {"id": 303, "type": "EasyAIPipeline", "inputs": []}, | |
| {"id": 311, "type": "SaveEXRFrames", "inputs": []}, | |
| {"id": 312, "type": "Easy_VideoCombine", "inputs": []}, | |
| {"id": 313, "type": "SaveImageAndPromptExact", "inputs": [{"name": "filepath", "widget": {"name": "filepath"}}]}, | |
| ], | |
| "links": [], | |
| "groups": [], | |
| } | |
| setup["apiPrompt"] = { | |
| "100": { | |
| "class_type": "Koolook_PublishInput", | |
| "inputs": { | |
| "mode": "Img", | |
| "sequence_folder": "", | |
| "qt_file": "", | |
| "single_file": "/shots/source/input.png", | |
| "prompt": "", | |
| }, | |
| }, | |
| "200": { | |
| "class_type": "Koolook_PublishOutput", | |
| "inputs": {"folder": "/shots/output", "name": "publish-OUT", "version": "2"}, | |
| }, | |
| "300": {"class_type": "RMBG", "inputs": {"image": ["100", 2]}}, | |
| "301": {"class_type": "EasyAIPipeline", "inputs": {"base_directory_path": ["200", 0], "extension": ".exr"}}, | |
| "302": {"class_type": "EasyAIPipeline", "inputs": {"base_directory_path": ["200", 0], "extension": ".mov"}}, | |
| "303": {"class_type": "EasyAIPipeline", "inputs": {"base_directory_path": ["200", 0], "extension": ".png"}}, | |
| "400": {"class_type": "Koolook_PublishRouter", "inputs": {"selector": ["100", 4], "payload": ["300", 0]}}, | |
| "311": {"class_type": "SaveEXRFrames", "inputs": {"filepath": ["301", 0], "images": ["400", 0]}}, | |
| "312": {"class_type": "Easy_VideoCombine", "inputs": {"filepath": ["302", 0], "images": ["400", 1]}}, | |
| "313": {"class_type": "SaveImageAndPromptExact", "inputs": {"filepath": ["303", 0], "image": ["400", 2]}}, | |
| } | |
| setup["setupSurface"] = { | |
| "sourceInputs": [ | |
| { | |
| "group": "Koolook Input", | |
| "nodes": [{"id": "100", "type": "Koolook_PublishInput", "title": "Koolook Publish Input"}], | |
| } | |
| ], | |
| "outputs": [ | |
| { | |
| "group": "Koolook Output", | |
| "nodes": [ | |
| {"id": "200", "type": "Koolook_PublishOutput", "title": "Koolook Publish Output"}, | |
| {"id": "400", "type": "Koolook_PublishRouter", "title": "Koolook Publish Router"}, | |
| {"id": "311", "type": "SaveEXRFrames", "title": "EXR writer"}, | |
| {"id": "312", "type": "Easy_VideoCombine", "title": "QT writer"}, | |
| {"id": "313", "type": "SaveImageAndPromptExact", "title": "Image writer"}, | |
| ], | |
| } | |
| ], | |
| "controls": [], | |
| "app": { | |
| "inputs": [ | |
| { | |
| "key": "single_file", | |
| "label": "Single file", | |
| "visible": True, | |
| "target": {"node": "100", "input": "single_file"}, | |
| "default": "/shots/source/input.png", | |
| } | |
| ], | |
| "outputs": [ | |
| { | |
| "key": "folder", | |
| "label": "Output folder", | |
| "visible": True, | |
| "target": {"node": "200", "input": "folder"}, | |
| "default": "/shots/output", | |
| }, | |
| { | |
| "key": "name", | |
| "label": "Output name", | |
| "visible": True, | |
| "target": {"node": "200", "input": "name"}, | |
| "default": "publish-OUT", | |
| }, | |
| { | |
| "key": "version", | |
| "label": "Version", | |
| "visible": True, | |
| "target": {"node": "200", "input": "version"}, | |
| "default": "2", | |
| }, | |
| ], | |
| "results": [], | |
| "switch": { | |
| "key": "switch", | |
| "label": "Input type", | |
| "visible": True, | |
| "target": {"node": "100", "input": "mode"}, | |
| "default": 2, | |
| "options": [ | |
| {"value": 0, "label": "EXR", "visible": False, "input": "single_file"}, | |
| {"value": 1, "label": "QT", "visible": False, "input": "single_file"}, | |
| {"value": 2, "label": "Img", "visible": True, "input": "single_file"}, | |
| {"value": 3, "label": "Prompt", "visible": False, "input": "single_file"}, | |
| ], | |
| }, | |
| }, | |
| } | |
| setup["executionMap"] = { | |
| "version": 1, | |
| "routers": [ | |
| { | |
| "node": "400", | |
| "switchKey": "switch", | |
| "selector": {"node": "100", "output": 4}, | |
| "payload": {"node": "300", "output": 0}, | |
| "branches": { | |
| "0": {"label": "EXR", "output": 0, "writerNodes": ["311"]}, | |
| "1": {"label": "QT", "output": 1, "writerNodes": ["312"]}, | |
| "2": {"label": "Img", "output": 2, "writerNodes": ["313"]}, | |
| "3": {"label": "Prompt", "output": 3, "writerNodes": []}, | |
| }, | |
| } | |
| ], | |
| } | |
| registry = PublishedSetupRegistry(StaticSetupStorage([setup])) | |
| comfy = FakeComfyClient() | |
| runner = PublishedSetupRunner(registry, comfy) | |
| await runner.runSetup( | |
| "ltx-director-demo", | |
| { | |
| "switch": 2, | |
| "single_file": "/shots/source/input.png", | |
| "folder": "/shots/output", | |
| "name": "external", | |
| "version": "7", | |
| }, | |
| ) | |
| assert set(comfy.submitted_prompts[0]) == {"100", "200", "300", "303", "400", "313"} | |
| assert comfy.submitted_prompts[0]["100"]["inputs"]["mode"] == "Img" | |
| assert comfy.submitted_prompts[0]["313"]["inputs"] == { | |
| "filepath": ["303", 0], | |
| "image": ["400", 2], | |
| } | |
| asyncio.run(exercise()) | |
| def test_run_setup_rejects_invalid_switch_values_before_queueing(switch_value: object) -> None: | |
| async def exercise() -> None: | |
| setup = _valid_setup() | |
| setup["inputContract"] = {"inputs": []} | |
| setup["outputContract"] = {"outputs": []} | |
| setup["apiPrompt"] = { | |
| "100": { | |
| "class_type": "Koolook_PublishInput", | |
| "inputs": { | |
| "mode": "Img", | |
| "sequence_folder": "", | |
| "qt_file": "", | |
| "single_file": "/shots/source/input.png", | |
| "prompt": "", | |
| }, | |
| }, | |
| "300": {"class_type": "RMBG", "inputs": {"image": ["100", 2]}}, | |
| "400": {"class_type": "Koolook_PublishRouter", "inputs": {"selector": ["100", 4], "payload": ["300", 0]}}, | |
| "313": {"class_type": "SaveImageAndPromptExact", "inputs": {"image": ["400", 2]}}, | |
| } | |
| setup["setupSurface"] = { | |
| "sourceInputs": [], | |
| "outputs": [], | |
| "controls": [], | |
| "app": { | |
| "inputs": [], | |
| "outputs": [], | |
| "results": [], | |
| "switch": { | |
| "key": "switch", | |
| "label": "Input type", | |
| "visible": True, | |
| "target": {"node": "100", "input": "mode"}, | |
| "default": 2, | |
| "options": [ | |
| {"value": 0, "label": "EXR", "visible": False, "input": "single_file"}, | |
| {"value": 1, "label": "QT", "visible": False, "input": "single_file"}, | |
| {"value": 2, "label": "Img", "visible": True, "input": "single_file"}, | |
| ], | |
| }, | |
| }, | |
| } | |
| setup["executionMap"] = { | |
| "version": 1, | |
| "routers": [ | |
| { | |
| "node": "400", | |
| "switchKey": "switch", | |
| "selector": {"node": "100", "output": 4}, | |
| "payload": {"node": "300", "output": 0}, | |
| "branches": { | |
| "0": {"label": "EXR", "output": 0, "writerNodes": []}, | |
| "1": {"label": "QT", "output": 1, "writerNodes": []}, | |
| "2": {"label": "Img", "output": 2, "writerNodes": ["313"]}, | |
| }, | |
| } | |
| ], | |
| } | |
| comfy = FakeComfyClient() | |
| runner = PublishedSetupRunner( | |
| type("Registry", (), {"getSetup": lambda self, setup_id: setup if setup_id == "ltx-director-demo" else None})(), | |
| comfy, | |
| ) | |
| with pytest.raises(SetupRunError) as exc_info: | |
| await runner.runSetup("ltx-director-demo", {"switch": switch_value}) | |
| assert exc_info.value.status_code == 400 | |
| assert exc_info.value.errors == [ | |
| "input 'switch' must be one of: 0 (EXR), 1 (QT), 2 (Img)" | |
| ] | |
| assert comfy.submitted_prompts == [] | |
| asyncio.run(exercise()) | |
| def test_run_setup_rejects_execution_map_branch_with_stale_writer_nodes() -> None: | |
| async def exercise() -> None: | |
| setup = _valid_setup() | |
| setup["inputContract"] = {"inputs": []} | |
| setup["outputContract"] = {"outputs": []} | |
| setup["apiPrompt"] = { | |
| "100": { | |
| "class_type": "Koolook_PublishInput", | |
| "inputs": { | |
| "mode": "Img", | |
| "sequence_folder": "", | |
| "qt_file": "", | |
| "single_file": "/shots/source/input.png", | |
| "prompt": "", | |
| }, | |
| }, | |
| "300": {"class_type": "RMBG", "inputs": {"image": ["100", 2]}}, | |
| "400": {"class_type": "Koolook_PublishRouter", "inputs": {"selector": ["100", 4], "payload": ["300", 0]}}, | |
| "313": {"class_type": "SaveImageAndPromptExact", "inputs": {"image": ["400", 2]}}, | |
| } | |
| setup["setupSurface"] = { | |
| "sourceInputs": [], | |
| "outputs": [], | |
| "controls": [], | |
| "app": { | |
| "inputs": [], | |
| "outputs": [], | |
| "results": [], | |
| "switch": { | |
| "key": "switch", | |
| "label": "Input type", | |
| "visible": True, | |
| "target": {"node": "100", "input": "mode"}, | |
| "default": 2, | |
| "options": [ | |
| {"value": 0, "label": "EXR", "visible": False, "input": "single_file"}, | |
| {"value": 1, "label": "QT", "visible": False, "input": "single_file"}, | |
| {"value": 2, "label": "Img", "visible": True, "input": "single_file"}, | |
| ], | |
| }, | |
| }, | |
| } | |
| setup["executionMap"] = { | |
| "version": 1, | |
| "routers": [ | |
| { | |
| "node": "400", | |
| "switchKey": "switch", | |
| "selector": {"node": "100", "output": 4}, | |
| "payload": {"node": "300", "output": 0}, | |
| "branches": { | |
| "2": {"label": "Img", "output": 2, "writerNodes": ["999"]}, | |
| }, | |
| } | |
| ], | |
| } | |
| comfy = FakeComfyClient() | |
| runner = PublishedSetupRunner( | |
| type("Registry", (), {"getSetup": lambda self, setup_id: setup if setup_id == "ltx-director-demo" else None})(), | |
| comfy, | |
| ) | |
| with pytest.raises(SetupRunError) as exc_info: | |
| await runner.runSetup("ltx-director-demo", {"switch": 2}) | |
| assert exc_info.value.status_code == 400 | |
| assert exc_info.value.errors == [ | |
| "execution map branch 2 for switch 'switch' references writer node(s) not present in the prompt: 999" | |
| ] | |
| assert comfy.submitted_prompts == [] | |
| asyncio.run(exercise()) | |
| def test_get_run_reports_execution_map_writer_filepath_when_saver_history_is_empty( | |
| monkeypatch: pytest.MonkeyPatch, | |
| tmp_path, | |
| ) -> None: | |
| async def exercise() -> None: | |
| import k_ai_pipeline | |
| makedirs_calls = [] | |
| monkeypatch.setattr( | |
| k_ai_pipeline.os, | |
| "makedirs", | |
| lambda *args, **kwargs: makedirs_calls.append((args, kwargs)), | |
| ) | |
| setup = _valid_setup() | |
| output_folder = tmp_path / "missing-output" | |
| setup["inputContract"] = {"inputs": []} | |
| setup["outputContract"] = {"outputs": []} | |
| setup["apiPrompt"] = { | |
| "100": { | |
| "class_type": "Koolook_PublishInput", | |
| "inputs": { | |
| "mode": "Img", | |
| "sequence_folder": "", | |
| "qt_file": "", | |
| "single_file": "/shots/source/input.png", | |
| "prompt": "", | |
| }, | |
| }, | |
| "200": { | |
| "class_type": "Koolook_PublishOutput", | |
| "inputs": {"folder": str(output_folder), "name": "publish-OUT", "version": "2"}, | |
| }, | |
| "300": {"class_type": "RMBG", "inputs": {"image": ["100", 2]}}, | |
| "303": { | |
| "class_type": "EasyAIPipeline", | |
| "inputs": { | |
| "shot_duration": 81, | |
| "seed_value": 1, | |
| "instruction": "", | |
| "base_directory_path": ["200", 0], | |
| "extension": ".png", | |
| "shot_name": ["200", 1], | |
| "ai_method": "", | |
| "version": ["200", 2], | |
| "disable_versioning": False, | |
| "enable_overwrite": True, | |
| "no_subfolders": True, | |
| }, | |
| }, | |
| "400": {"class_type": "Koolook_PublishRouter", "inputs": {"selector": ["100", 4], "payload": ["300", 0]}}, | |
| "313": {"class_type": "SaveImageAndPromptExact", "inputs": {"filepath": ["303", 0], "image": ["400", 2]}}, | |
| "999": {"class_type": "Koolook_PublishResult", "inputs": {"result": ""}}, | |
| } | |
| setup["setupSurface"] = { | |
| "sourceInputs": [ | |
| { | |
| "group": "Koolook Input", | |
| "nodes": [{"id": "100", "type": "Koolook_PublishInput", "title": "Koolook Publish Input"}], | |
| } | |
| ], | |
| "outputs": [ | |
| { | |
| "group": "Koolook Output", | |
| "nodes": [ | |
| {"id": "200", "type": "Koolook_PublishOutput", "title": "Koolook Publish Output"}, | |
| {"id": "400", "type": "Koolook_PublishRouter", "title": "Koolook Publish Router"}, | |
| {"id": "313", "type": "SaveImageAndPromptExact", "title": "Image writer"}, | |
| {"id": "999", "type": "Koolook_PublishResult", "title": "Koolook Publish Result"}, | |
| ], | |
| } | |
| ], | |
| "controls": [], | |
| "app": { | |
| "inputs": [ | |
| { | |
| "key": "single_file", | |
| "label": "Single file", | |
| "visible": True, | |
| "target": {"node": "100", "input": "single_file"}, | |
| "default": "/shots/source/input.png", | |
| } | |
| ], | |
| "outputs": [ | |
| {"key": "folder", "label": "Output folder", "visible": True, "target": {"node": "200", "input": "folder"}, "default": str(output_folder)}, | |
| {"key": "name", "label": "Output name", "visible": True, "target": {"node": "200", "input": "name"}, "default": "publish-OUT"}, | |
| {"key": "version", "label": "Version", "visible": True, "target": {"node": "200", "input": "version"}, "default": "2"}, | |
| ], | |
| "results": [ | |
| {"key": "result", "label": "Result", "visible": True, "target": {"node": "999", "input": "result"}, "default": ""} | |
| ], | |
| "switch": { | |
| "key": "switch", | |
| "label": "Input type", | |
| "visible": True, | |
| "target": {"node": "100", "input": "mode"}, | |
| "default": 2, | |
| "options": [ | |
| {"value": 0, "label": "EXR", "visible": False, "input": "single_file"}, | |
| {"value": 1, "label": "QT", "visible": False, "input": "single_file"}, | |
| {"value": 2, "label": "Img", "visible": True, "input": "single_file"}, | |
| {"value": 3, "label": "Prompt", "visible": False, "input": "single_file"}, | |
| ], | |
| }, | |
| }, | |
| } | |
| setup["executionMap"] = { | |
| "version": 1, | |
| "routers": [ | |
| { | |
| "node": "400", | |
| "switchKey": "switch", | |
| "selector": {"node": "100", "output": 4}, | |
| "payload": {"node": "300", "output": 0}, | |
| "branches": { | |
| "2": {"label": "Img", "output": 2, "writerNodes": ["313"]}, | |
| }, | |
| } | |
| ], | |
| } | |
| comfy = FakeComfyClient() | |
| runner = PublishedSetupRunner( | |
| type("Registry", (), {"getSetup": lambda self, setup_id: setup if setup_id == "ltx-director-demo" else None})(), | |
| comfy, | |
| ) | |
| queued = await runner.runSetup( | |
| "ltx-director-demo", | |
| {"switch": 2, "folder": str(output_folder), "name": "external", "version": "7"}, | |
| ) | |
| comfy.history[queued["promptId"]] = { | |
| "status": {"completed": True, "status_str": "success"}, | |
| "prompt": [2, queued["promptId"], comfy.submitted_prompts[0], {}], | |
| "outputs": {"313": {"images": []}}, | |
| } | |
| status = await runner.getRun(queued["runId"]) | |
| # build_pipeline_outputs canonicalizes paths to forward slashes on | |
| # every platform, so the expected prefix must match — str(tmp_path) | |
| # yields backslashes on Windows. | |
| canonical_folder = str(output_folder).replace("\\", "/") | |
| assert status["outputs"][-1]["items"] == [ | |
| { | |
| "nodeId": "313", | |
| "kind": "text", | |
| "value": f"{canonical_folder}/v007/external_v007.png", | |
| } | |
| ] | |
| assert makedirs_calls == [] | |
| assert not output_folder.exists() | |
| asyncio.run(exercise()) | |
| def test_get_run_resolves_selected_switch_result_when_result_node_history_is_missing( | |
| switch_value: int, | |
| mode_label: str, | |
| branch_node: str, | |
| source_node: str, | |
| saver_node: str, | |
| result_path: str, | |
| ) -> None: | |
| async def exercise() -> None: | |
| setup = _valid_setup() | |
| setup["inputContract"] = {"inputs": []} | |
| setup["outputContract"] = {"outputs": []} | |
| setup["visualGraph"] = { | |
| "nodes": [ | |
| { | |
| "id": 188, | |
| "type": "Koolook_PublishInput", | |
| "inputs": [ | |
| {"name": "mode", "widget": {"name": "mode"}}, | |
| {"name": "sequence_folder", "widget": {"name": "sequence_folder"}}, | |
| {"name": "qt_file", "widget": {"name": "qt_file"}}, | |
| {"name": "single_file", "widget": {"name": "single_file"}}, | |
| ], | |
| }, | |
| { | |
| "id": 205, | |
| "type": "Koolook_PublishResult", | |
| "inputs": [{"name": "result", "link": 900}], | |
| }, | |
| ], | |
| "links": [[900, 179, 0, 205, 0, "STRING"]], | |
| "groups": [], | |
| } | |
| setup["apiPrompt"] = { | |
| "188": { | |
| "class_type": "Koolook_PublishInput", | |
| "inputs": { | |
| "mode": "Img", | |
| "sequence_folder": "", | |
| "qt_file": "", | |
| "single_file": "/shots/source/input.png", | |
| "prompt": "", | |
| }, | |
| }, | |
| "179": { | |
| "class_type": "Switch any [Crystools]", | |
| "inputs": { | |
| "index": ["188", 4], | |
| "value0": ["301", 0], | |
| "value1": ["302", 0], | |
| "value2": ["303", 0], | |
| }, | |
| }, | |
| "180": { | |
| "class_type": "Switch any [Crystools]", | |
| "inputs": { | |
| "index": ["188", 4], | |
| "value0": ["401", 0], | |
| "value1": ["402", 0], | |
| "value2": ["403", 0], | |
| }, | |
| }, | |
| "301": { | |
| "class_type": "EasyAIPipeline", | |
| "inputs": {"WRITE_file_path": "/shots/output/publish-OUT_v002.exr"}, | |
| }, | |
| "302": { | |
| "class_type": "Easy_VideoCombine", | |
| "inputs": {"video_path": "/shots/output/publish-OUT_v002.mov"}, | |
| }, | |
| "303": { | |
| "class_type": "EasyAIPipeline", | |
| "inputs": {"WRITE_file_path": "/shots/output/publish-OUT_v002.png"}, | |
| }, | |
| "311": { | |
| "class_type": "SaveEXRFrames", | |
| "inputs": {"filepath": ["301", 0], "images": ["180", 0]}, | |
| }, | |
| "312": { | |
| "class_type": "Easy_VideoCombine", | |
| "inputs": {"filepath": ["302", 0], "images": ["180", 0]}, | |
| }, | |
| "313": { | |
| "class_type": "SaveImageAndPromptExact", | |
| "inputs": {"filepath": ["303", 0], "images": ["180", 0]}, | |
| }, | |
| "401": { | |
| "class_type": "LoadEXR", | |
| "inputs": {"path": "/shots/source/input.exr"}, | |
| }, | |
| "402": { | |
| "class_type": "VHS_LoadVideoPath", | |
| "inputs": {"video": "/shots/source/input.mov"}, | |
| }, | |
| "403": { | |
| "class_type": "VHS_LoadImagePath", | |
| "inputs": {"image": "/shots/source/input.png"}, | |
| }, | |
| "205": { | |
| "class_type": "Koolook_PublishResult", | |
| "inputs": {"result": ["179", 0]}, | |
| }, | |
| "900": { | |
| "class_type": "Preview Image", | |
| "inputs": {"images": ["301", 0]}, | |
| }, | |
| } | |
| setup["setupSurface"] = { | |
| "sourceInputs": [ | |
| { | |
| "group": "Koolook Input", | |
| "nodes": [{"id": "188", "type": "Koolook_PublishInput", "title": "Koolook Publish Input"}], | |
| } | |
| ], | |
| "outputs": [ | |
| { | |
| "group": "Koolook Output", | |
| "nodes": [ | |
| {"id": "205", "type": "Koolook_PublishResult", "title": "Koolook Publish Result"}, | |
| {"id": "301", "type": "EasyAIPipeline", "title": "EXR path"}, | |
| {"id": "302", "type": "EasyAIPipeline", "title": "QT path"}, | |
| {"id": "303", "type": "EasyAIPipeline", "title": "Img path"}, | |
| {"id": "311", "type": "SaveEXRFrames", "title": "EXR saver"}, | |
| {"id": "312", "type": "Easy_VideoCombine", "title": "QT saver"}, | |
| {"id": "313", "type": "SaveImageAndPromptExact", "title": "Image saver"}, | |
| ], | |
| } | |
| ], | |
| "controls": [], | |
| "app": { | |
| "inputs": [ | |
| { | |
| "key": "sequence_folder", | |
| "label": "Sequence folder", | |
| "visible": True, | |
| "target": {"node": "188", "input": "sequence_folder"}, | |
| "default": "", | |
| }, | |
| { | |
| "key": "qt_file", | |
| "label": "QT file", | |
| "visible": True, | |
| "target": {"node": "188", "input": "qt_file"}, | |
| "default": "", | |
| }, | |
| { | |
| "key": "single_file", | |
| "label": "Single file", | |
| "visible": True, | |
| "target": {"node": "188", "input": "single_file"}, | |
| "default": "/shots/source/input.png", | |
| } | |
| ], | |
| "outputs": [], | |
| "results": [ | |
| { | |
| "key": "result", | |
| "label": "Result", | |
| "visible": True, | |
| "target": {"node": "205", "input": "result"}, | |
| "default": "", | |
| } | |
| ], | |
| "switch": { | |
| "key": "switch", | |
| "label": "Input type", | |
| "visible": True, | |
| "target": {"node": "188", "input": "mode"}, | |
| "default": 2, | |
| "options": [ | |
| {"value": 0, "label": "EXR", "visible": True, "input": "sequence_folder"}, | |
| {"value": 1, "label": "QT", "visible": True, "input": "qt_file"}, | |
| {"value": 2, "label": "Img", "visible": True, "input": "single_file"}, | |
| ], | |
| }, | |
| }, | |
| } | |
| registry = PublishedSetupRegistry(StaticSetupStorage([setup])) | |
| comfy = FakeComfyClient( | |
| history={ | |
| "comfy-prompt-1": { | |
| "status": {"completed": True, "status_str": "success"}, | |
| "outputs": { | |
| branch_node: { | |
| "text": [result_path], | |
| } | |
| }, | |
| } | |
| } | |
| ) | |
| runner = PublishedSetupRunner( | |
| registry, | |
| comfy, | |
| ) | |
| queued = await runner.runSetup("ltx-director-demo", {"switch": switch_value}) | |
| status = await runner.getRun(queued["runId"]) | |
| assert set(comfy.submitted_prompts[0]) == { | |
| "188", | |
| "179", | |
| "180", | |
| "205", | |
| branch_node, | |
| source_node, | |
| saver_node, | |
| } | |
| assert comfy.submitted_prompts[0]["188"]["inputs"]["mode"] == mode_label | |
| assert comfy.submitted_prompts[0]["179"]["inputs"] == { | |
| "index": ["188", 4], | |
| f"value{switch_value}": [branch_node, 0], | |
| } | |
| assert comfy.submitted_prompts[0]["180"]["inputs"] == { | |
| "index": ["188", 4], | |
| f"value{switch_value}": [source_node, 0], | |
| } | |
| assert status["outputs"] == [ | |
| { | |
| "key": "result", | |
| "label": "Result", | |
| "type": "result", | |
| "visible": True, | |
| "target": {"node": "205", "input": "result"}, | |
| "default": "", | |
| "items": [ | |
| { | |
| "nodeId": branch_node, | |
| "kind": "text", | |
| "value": result_path, | |
| } | |
| ], | |
| } | |
| ] | |
| asyncio.run(exercise()) | |