Download ComfyUI/custom_nodes/koolook/tests/server/test_published_setup_api.py from vidfom/aimusic: direct link, hf CLI and curl.
- Browser
- Download file 23.8 kB
-
https://huggingface.co/vidfom/aimusic/resolve/main/ComfyUI/custom_nodes/koolook/tests/server/test_published_setup_api.py
- Command line
-
hf download hf://vidfom/aimusic/ComfyUI/custom_nodes/koolook/tests/server/test_published_setup_api.py
-
curl -L -o test_published_setup_api.py https://huggingface.co/vidfom/aimusic/resolve/main/ComfyUI/custom_nodes/koolook/tests/server/test_published_setup_api.py
23.8 kB
| from __future__ import annotations | |
| import asyncio | |
| import json | |
| from copy import deepcopy | |
| from aiohttp import web | |
| from aiohttp.test_utils import make_mocked_request | |
| import koolook_routes | |
| from koolook_setup_runner import PublishedSetupRunner | |
| from koolook_setups import FileSetupStorage, PublishedSetupRegistry, StaticSetupStorage | |
| from tests.server.test_published_setup_registry import _valid_setup | |
| def test_catalog_routes_expose_list_and_detail_contracts() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([_valid_setup()])) | |
| app = _app_with_registry(registry) | |
| list_response = await _handle(app, "GET", "/koolook/api/setups") | |
| detail_response = await _handle(app, "GET", "/koolook/api/setups/ltx-director-demo") | |
| missing_response = await _handle(app, "GET", "/koolook/api/setups/missing") | |
| assert list_response.status == 200 | |
| assert detail_response.status == 200 | |
| assert missing_response.status == 404 | |
| rows = _json_body(list_response) | |
| detail = _json_body(detail_response) | |
| assert rows[0]["id"] == "ltx-director-demo" | |
| assert "visualGraph" not in rows[0] | |
| assert "apiPrompt" not in rows[0] | |
| assert detail["visualGraph"] == _valid_setup()["visualGraph"] | |
| assert detail["inputContract"] == _valid_setup()["inputContract"] | |
| asyncio.run(exercise()) | |
| def test_setup_runner_simulator_routes_serve_html_and_js() -> None: | |
| async def exercise() -> None: | |
| app = _app_with_registry(PublishedSetupRegistry(StaticSetupStorage([]))) | |
| html_response = await _handle(app, "GET", "/koolook/setup_runner_simulator.html") | |
| js_response = await _handle(app, "GET", "/koolook/setup_runner_simulator.js") | |
| assert html_response.status == 200 | |
| assert "Koolook Setup Runner Simulator" in html_response.text | |
| assert "./setup_runner_simulator.js" in html_response.text | |
| assert js_response.status == 200 | |
| assert "listPublishedSetups" in js_response.text | |
| asyncio.run(exercise()) | |
| def test_version_route_exposes_installed_version_and_release_urls() -> None: | |
| async def exercise() -> None: | |
| app = _app_with_registry(PublishedSetupRegistry(StaticSetupStorage([]))) | |
| response = await _handle(app, "GET", "/koolook/api/version") | |
| assert response.status == 200 | |
| body = _json_body(response) | |
| assert body["version"] == "0.4.4" | |
| assert body["releasesUrl"] == "https://github.com/malkuthro/ComfyUI-Koolook/releases" | |
| assert body["latestReleaseApiUrl"].endswith("/malkuthro/ComfyUI-Koolook/releases/latest") | |
| asyncio.run(exercise()) | |
| def test_catalog_list_omits_invalid_setups() -> None: | |
| async def exercise() -> None: | |
| invalid = deepcopy(_valid_setup()) | |
| invalid["id"] = "broken" | |
| invalid.pop("metadata") | |
| registry = PublishedSetupRegistry(StaticSetupStorage([invalid, _valid_setup()])) | |
| app = _app_with_registry(registry) | |
| response = await _handle(app, "GET", "/koolook/api/setups") | |
| rows = _json_body(response) | |
| assert response.status == 200 | |
| assert [row["id"] for row in rows] == ["ltx-director-demo"] | |
| assert registry.diagnostics == ["broken: missing required field: metadata"] | |
| asyncio.run(exercise()) | |
| def test_publish_route_persists_setup_and_catalog_returns_it() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([])) | |
| app = _app_with_registry(registry) | |
| payload = { | |
| "visualGraph": { | |
| "nodes": [ | |
| { | |
| "id": 12, | |
| "type": "Text Multiline", | |
| "inputs": [{"name": "text", "widget": {"name": "text"}}], | |
| "widgets_values": ["published prompt"], | |
| } | |
| ], | |
| "links": [], | |
| }, | |
| "metadata": { | |
| "id": "published-from-sidebar", | |
| "title": "Published From Sidebar", | |
| "description": "Published through the API.", | |
| "category": "Video", | |
| "tags": ["publish"], | |
| "previewImage": "", | |
| }, | |
| "inputContract": { | |
| "inputs": [ | |
| { | |
| "key": "prompt", | |
| "label": "Prompt", | |
| "type": "text", | |
| "required": True, | |
| "target": {"node": "12", "input": "text"}, | |
| } | |
| ] | |
| }, | |
| "outputContract": {"outputs": [{"key": "preview", "type": "image"}]}, | |
| "source": {"kind": "sidebar-workflow", "path": "Demos/Published From Sidebar"}, | |
| } | |
| publish_response = await _handle_json(app, "POST", "/koolook/api/setups", payload) | |
| detail_response = await _handle(app, "GET", "/koolook/api/setups/published-from-sidebar") | |
| assert publish_response.status == 200 | |
| body = _json_body(publish_response) | |
| assert body["ok"] is True | |
| assert body["setup"]["id"] == "published-from-sidebar" | |
| detail = _json_body(detail_response) | |
| assert detail["source"]["path"] == "Demos/Published From Sidebar" | |
| assert detail["apiPrompt"] == { | |
| "12": {"class_type": "Text Multiline", "inputs": {"text": "published prompt"}} | |
| } | |
| assert detail["validation"] == {"status": "valid", "diagnostics": []} | |
| asyncio.run(exercise()) | |
| def test_publish_route_returns_storage_path(tmp_path) -> None: | |
| """The publish response surfaces where the setup was saved on disk so | |
| the sidebar can show / copy / open the registry location (issue #227).""" | |
| async def exercise() -> None: | |
| storage_path = tmp_path / "koolook-published-setups" / "setups.json" | |
| registry = PublishedSetupRegistry(FileSetupStorage(storage_path)) | |
| app = _app_with_registry(registry) | |
| payload = { | |
| "visualGraph": { | |
| "nodes": [ | |
| { | |
| "id": 12, | |
| "type": "Text Multiline", | |
| "inputs": [{"name": "text", "widget": {"name": "text"}}], | |
| "widgets_values": ["published prompt"], | |
| } | |
| ], | |
| "links": [], | |
| }, | |
| "metadata": { | |
| "id": "published-with-path", | |
| "title": "Published With Path", | |
| "description": "Surfaces its storage path.", | |
| "category": "Video", | |
| "tags": ["publish"], | |
| "previewImage": "", | |
| }, | |
| "inputContract": { | |
| "inputs": [ | |
| { | |
| "key": "prompt", | |
| "label": "Prompt", | |
| "type": "text", | |
| "required": True, | |
| "target": {"node": "12", "input": "text"}, | |
| } | |
| ] | |
| }, | |
| "outputContract": {"outputs": [{"key": "preview", "type": "image"}]}, | |
| "source": {"kind": "sidebar-workflow", "path": "Demos/Published With Path"}, | |
| } | |
| publish_response = await _handle_json(app, "POST", "/koolook/api/setups", payload) | |
| assert publish_response.status == 200 | |
| body = _json_body(publish_response) | |
| assert body["ok"] is True | |
| assert body["setup"]["id"] == "published-with-path" | |
| assert body["storagePath"] == str(storage_path) | |
| asyncio.run(exercise()) | |
| def test_reveal_published_setup_folder_opens_storage_dir(tmp_path, monkeypatch) -> None: | |
| """Open folder on the success card reveals the published-setups | |
| directory (not the snapshot library) in the OS file manager.""" | |
| async def exercise() -> None: | |
| storage_path = tmp_path / "koolook-published-setups" / "setups.json" | |
| storage_path.parent.mkdir(parents=True) | |
| registry = PublishedSetupRegistry(FileSetupStorage(storage_path)) | |
| app = _app_with_registry(registry) | |
| opened: list = [] | |
| monkeypatch.setattr( | |
| koolook_routes.subprocess, | |
| "Popen", | |
| lambda args, *a, **k: opened.append(args) or object(), | |
| ) | |
| response = await _handle(app, "POST", "/koolook/api/setups/reveal") | |
| assert response.status == 200 | |
| assert _json_body(response) == {"ok": True, "path": str(storage_path.parent)} | |
| assert len(opened) == 1 | |
| # Launcher binary is platform-specific; the revealed path is the dir. | |
| assert opened[0][-1] == str(storage_path.parent) | |
| asyncio.run(exercise()) | |
| def test_reveal_published_setup_folder_missing_dir_returns_404(tmp_path, monkeypatch) -> None: | |
| """Reveal fails cleanly (no file-manager spawn) when nothing has been | |
| published yet, so the storage directory does not exist on disk.""" | |
| async def exercise() -> None: | |
| storage_path = tmp_path / "koolook-published-setups" / "setups.json" | |
| registry = PublishedSetupRegistry(FileSetupStorage(storage_path)) | |
| app = _app_with_registry(registry) | |
| opened: list = [] | |
| monkeypatch.setattr( | |
| koolook_routes.subprocess, | |
| "Popen", | |
| lambda args, *a, **k: opened.append(args) or object(), | |
| ) | |
| response = await _handle(app, "POST", "/koolook/api/setups/reveal") | |
| assert response.status == 404 | |
| assert opened == [] | |
| asyncio.run(exercise()) | |
| def test_publish_route_preserves_supplied_api_prompt() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([])) | |
| app = _app_with_registry(registry) | |
| payload = { | |
| "visualGraph": { | |
| "nodes": [ | |
| { | |
| "id": 12, | |
| "type": "Text Multiline", | |
| "inputs": [{"name": "text", "widget": {"name": "text"}}], | |
| "widgets_values": ["fallback prompt"], | |
| } | |
| ], | |
| "links": [], | |
| }, | |
| "apiPrompt": { | |
| "12": {"class_type": "Text Multiline", "inputs": {"text": "comfy api prompt"}} | |
| }, | |
| "metadata": { | |
| "id": "published-with-api-prompt", | |
| "title": "Published With API Prompt", | |
| "description": "Published with Comfy's API workflow.", | |
| }, | |
| "inputContract": { | |
| "inputs": [ | |
| { | |
| "key": "prompt", | |
| "type": "text", | |
| "target": {"node": "12", "input": "text"}, | |
| } | |
| ] | |
| }, | |
| "outputContract": {"outputs": [{"key": "preview", "type": "image"}]}, | |
| "source": {"kind": "sidebar-workflow", "path": "Demos/API Prompt"}, | |
| } | |
| publish_response = await _handle_json(app, "POST", "/koolook/api/setups", payload) | |
| detail_response = await _handle(app, "GET", "/koolook/api/setups/published-with-api-prompt") | |
| assert publish_response.status == 200 | |
| detail = _json_body(detail_response) | |
| assert detail["apiPrompt"] == payload["apiPrompt"] | |
| asyncio.run(exercise()) | |
| def test_publish_route_rejects_invalid_contract_with_clear_error() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([])) | |
| app = _app_with_registry(registry) | |
| response = await _handle_json( | |
| app, | |
| "POST", | |
| "/koolook/api/setups", | |
| { | |
| "visualGraph": { | |
| "nodes": [ | |
| { | |
| "id": 12, | |
| "type": "Text Multiline", | |
| "inputs": [{"name": "text", "widget": {"name": "text"}}], | |
| "widgets_values": ["bad prompt"], | |
| } | |
| ], | |
| "links": [], | |
| }, | |
| "metadata": {"id": "bad", "title": "Bad", "description": "Bad"}, | |
| "inputContract": { | |
| "inputs": [{"key": "prompt", "type": "text", "target": {"node": "99", "input": "text"}}] | |
| }, | |
| "outputContract": {"outputs": [{"key": "preview", "type": "image"}]}, | |
| "source": {"kind": "sidebar-workflow", "path": "Demos/Bad"}, | |
| }, | |
| ) | |
| assert response.status == 400 | |
| assert "target.node not found" in _json_body(response)["errors"][0] | |
| assert registry.listSetups() == [] | |
| asyncio.run(exercise()) | |
| def test_publish_route_rejects_missing_metadata_with_clear_error() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([])) | |
| app = _app_with_registry(registry) | |
| response = await _handle_json( | |
| app, | |
| "POST", | |
| "/koolook/api/setups", | |
| { | |
| "visualGraph": {"nodes": []}, | |
| "inputContract": {"inputs": []}, | |
| "outputContract": {"outputs": []}, | |
| "source": {"kind": "sidebar-workflow", "path": "Demos/Missing Metadata"}, | |
| }, | |
| ) | |
| assert response.status == 400 | |
| assert "metadata must be an object" in _json_body(response)["errors"][0] | |
| assert registry.listSetups() == [] | |
| asyncio.run(exercise()) | |
| def test_run_route_queues_callable_setup_and_returns_stable_run_id() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([_valid_setup()])) | |
| comfy = _FakeComfyClient() | |
| app = _app_with_registry( | |
| registry, | |
| setup_runner_factory=lambda: PublishedSetupRunner(registry, comfy), | |
| ) | |
| response = await _handle_json( | |
| app, | |
| "POST", | |
| "/koolook/api/setups/ltx-director-demo/run", | |
| {"inputs": {"prompt": "external prompt"}}, | |
| ) | |
| assert response.status == 200 | |
| assert _json_body(response) == { | |
| "ok": True, | |
| "run": { | |
| "runId": "run-000001", | |
| "promptId": "comfy-prompt-1", | |
| "status": "queued", | |
| }, | |
| } | |
| assert comfy.submitted_prompts == [ | |
| {"12": {"class_type": "Text Multiline", "inputs": {"text": "external prompt"}}} | |
| ] | |
| asyncio.run(exercise()) | |
| def test_run_status_route_reports_succeeded_outputs_from_comfy_history() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([_valid_setup()])) | |
| comfy = _FakeComfyClient( | |
| history={ | |
| "comfy-prompt-1": { | |
| "status": {"completed": True, "status_str": "success"}, | |
| "outputs": { | |
| "20": { | |
| "videos": [ | |
| { | |
| "filename": "demo.mp4", | |
| "subfolder": "koolook", | |
| "type": "output", | |
| } | |
| ] | |
| } | |
| }, | |
| } | |
| } | |
| ) | |
| runner = PublishedSetupRunner(registry, comfy) | |
| app = _app_with_registry(registry, setup_runner_factory=lambda: runner) | |
| run_response = await _handle_json( | |
| app, | |
| "POST", | |
| "/koolook/api/setups/ltx-director-demo/run", | |
| {"inputs": {"prompt": "external prompt"}}, | |
| ) | |
| run_id = _json_body(run_response)["run"]["runId"] | |
| status_response = await _handle(app, "GET", f"/koolook/api/runs/{run_id}") | |
| assert status_response.status == 200 | |
| assert _json_body(status_response) == { | |
| "ok": True, | |
| "run": { | |
| "runId": "run-000001", | |
| "setupId": "ltx-director-demo", | |
| "promptId": "comfy-prompt-1", | |
| "status": "succeeded", | |
| "comfyStatus": {"completed": True, "status_str": "success"}, | |
| "outputs": [ | |
| { | |
| "key": "video", | |
| "label": "Video", | |
| "type": "video", | |
| "items": [ | |
| { | |
| "nodeId": "20", | |
| "kind": "videos", | |
| "filename": "demo.mp4", | |
| "subfolder": "koolook", | |
| "type": "output", | |
| } | |
| ], | |
| } | |
| ], | |
| }, | |
| } | |
| asyncio.run(exercise()) | |
| def test_run_status_route_reports_failed_state_from_comfy_history() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([_valid_setup()])) | |
| comfy = _FakeComfyClient( | |
| history={ | |
| "comfy-prompt-1": { | |
| "status": {"completed": True, "status_str": "error"}, | |
| "outputs": {}, | |
| } | |
| } | |
| ) | |
| runner = PublishedSetupRunner(registry, comfy) | |
| app = _app_with_registry(registry, setup_runner_factory=lambda: runner) | |
| run_response = await _handle_json( | |
| app, | |
| "POST", | |
| "/koolook/api/setups/ltx-director-demo/run", | |
| {"inputs": {"prompt": "external prompt"}}, | |
| ) | |
| run_id = _json_body(run_response)["run"]["runId"] | |
| status_response = await _handle(app, "GET", f"/koolook/api/runs/{run_id}") | |
| assert status_response.status == 200 | |
| assert _json_body(status_response)["run"]["status"] == "failed" | |
| asyncio.run(exercise()) | |
| def test_run_route_returns_clear_errors_for_invalid_inputs() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([_valid_setup()])) | |
| app = _app_with_registry( | |
| registry, | |
| setup_runner_factory=lambda: PublishedSetupRunner(registry, _FakeComfyClient()), | |
| ) | |
| response = await _handle_json( | |
| app, | |
| "POST", | |
| "/koolook/api/setups/ltx-director-demo/run", | |
| {"inputs": {"prompt": "ok", "seed": 123}}, | |
| ) | |
| assert response.status == 400 | |
| assert _json_body(response) == { | |
| "ok": False, | |
| "errors": ["input 'seed' is not declared by this setup"], | |
| } | |
| asyncio.run(exercise()) | |
| def test_run_route_returns_clear_error_for_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])) | |
| app = _app_with_registry( | |
| registry, | |
| setup_runner_factory=lambda: PublishedSetupRunner(registry, _FakeComfyClient()), | |
| ) | |
| response = await _handle_json( | |
| app, | |
| "POST", | |
| "/koolook/api/setups/ltx-director-demo/run", | |
| {"inputs": {"prompt": "ok"}}, | |
| ) | |
| assert response.status == 400 | |
| assert _json_body(response) == { | |
| "ok": False, | |
| "errors": ["Published setup 'ltx-director-demo' is not callable."], | |
| } | |
| asyncio.run(exercise()) | |
| def test_run_route_returns_clear_error_for_comfy_queue_failure() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([_valid_setup()])) | |
| app = _app_with_registry( | |
| registry, | |
| setup_runner_factory=lambda: PublishedSetupRunner( | |
| registry, | |
| _FakeComfyClient(queue_error=RuntimeError("server down")), | |
| ), | |
| ) | |
| response = await _handle_json( | |
| app, | |
| "POST", | |
| "/koolook/api/setups/ltx-director-demo/run", | |
| {"inputs": {"prompt": "ok"}}, | |
| ) | |
| assert response.status == 502 | |
| assert _json_body(response) == { | |
| "ok": False, | |
| "errors": ["ComfyUI queue request failed: server down"], | |
| } | |
| asyncio.run(exercise()) | |
| def test_run_route_returns_clear_error_for_missing_setup() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([_valid_setup()])) | |
| app = _app_with_registry( | |
| registry, | |
| setup_runner_factory=lambda: PublishedSetupRunner(registry, _FakeComfyClient()), | |
| ) | |
| response = await _handle_json( | |
| app, | |
| "POST", | |
| "/koolook/api/setups/missing/run", | |
| {"inputs": {"prompt": "ok"}}, | |
| ) | |
| assert response.status == 404 | |
| assert _json_body(response) == { | |
| "ok": False, | |
| "errors": ["Published setup 'missing' not found."], | |
| } | |
| asyncio.run(exercise()) | |
| def test_run_status_route_returns_clear_error_for_unknown_run_id() -> None: | |
| async def exercise() -> None: | |
| registry = PublishedSetupRegistry(StaticSetupStorage([_valid_setup()])) | |
| app = _app_with_registry( | |
| registry, | |
| setup_runner_factory=lambda: PublishedSetupRunner(registry, _FakeComfyClient()), | |
| ) | |
| response = await _handle(app, "GET", "/koolook/api/runs/run-missing") | |
| assert response.status == 404 | |
| assert _json_body(response) == { | |
| "ok": False, | |
| "errors": ["Koolook run 'run-missing' not found."], | |
| } | |
| asyncio.run(exercise()) | |
| class _FakeComfyClient: | |
| def __init__(self, *, history=None, queue=None, queue_error=None) -> None: | |
| 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 {"prompt_id": "comfy-prompt-1"} | |
| 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 _app_with_registry( | |
| registry: PublishedSetupRegistry, | |
| *, | |
| setup_runner_factory=None, | |
| ) -> web.Application: | |
| routes = web.RouteTableDef() | |
| koolook_routes.register_routes( | |
| routes, | |
| setup_registry_factory=lambda: registry, | |
| setup_runner_factory=setup_runner_factory, | |
| ) | |
| app = web.Application() | |
| app.add_routes(routes) | |
| return app | |
| async def _handle(app: web.Application, method: str, path: str) -> web.Response: | |
| request = make_mocked_request(method, path, app=app) | |
| try: | |
| return await app._handle(request) | |
| except web.HTTPException as exc: | |
| return exc | |
| async def _handle_json(app: web.Application, method: str, path: str, payload) -> web.Response: | |
| request = make_mocked_request( | |
| method, | |
| path, | |
| app=app, | |
| headers={"Content-Type": "application/json"}, | |
| ) | |
| request._read_bytes = json.dumps(payload).encode("utf-8") | |
| try: | |
| return await app._handle(request) | |
| except web.HTTPException as exc: | |
| return exc | |
| def _json_body(response: web.Response): | |
| return json.loads(response.text or "") | |