From 255429c1b2b8228f24b7cb8ff3eca1782e6d0a11 Mon Sep 17 00:00:00 2001 From: sundar Date: Thu, 23 Jul 2026 13:54:01 -0700 Subject: [PATCH 1/6] fix: resolve host-relative follow-up links against the origin, not base_url MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A serverless gateway serves the v2 contract under /deployment/{id}/api/v2 and returns job.urls.* links that already include that mount prefix. Resolving them against base_url (which carries the same prefix) doubled it and 404'd every poll after submit. Follow-up links (leading slash + containing /api/) now resolve against scheme+authority; internal shorthand paths and Cloud / self-hosted behavior are byte-identical to before. Adds unit regression tests and an env-gated live integration suite (tests/integration/test_gateway_e2e.py) that exercises upload → dedup fast-path → img2img submit → poll → output download against a real gateway deployment; the asset-handle case is xfail until the gateway resolves core/ASSET references. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KV7wUb2hyzKHsR52ZJTvAm --- src/comfy_low/transport.py | 13 ++- tests/integration/test_gateway_e2e.py | 147 ++++++++++++++++++++++++++ tests/test_follow_up_links.py | 48 +++++++++ 3 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 tests/integration/test_gateway_e2e.py create mode 100644 tests/test_follow_up_links.py diff --git a/src/comfy_low/transport.py b/src/comfy_low/transport.py index 1a894ff..0f728a0 100644 --- a/src/comfy_low/transport.py +++ b/src/comfy_low/transport.py @@ -100,14 +100,21 @@ def __init__(self, base_url: str, api_key: str | None, client_info: str | None = self.base_url = base_url.rstrip("/") self.api_key = api_key self._base_origin = _origin(self.base_url) + parts = urlsplit(self.base_url) + self._origin_url = f"{parts.scheme}://{parts.netloc}" self._user_agent = _build_user_agent(client_info) def url(self, path: str) -> str: - # A path may be an absolute follow-up link (job.urls.*) or an API path. + # A path may be a follow-up link (job.urls.*) or an internal API path. + # Follow-up links are host-relative and already carry the mount prefix + # the server serves under (e.g. a gateway's /deployment/{id}/api/v2), + # so they resolve against the origin, not base_url — resolving against + # base_url would double the prefix. Internal shorthand paths (/jobs/…, + # /assets…) never contain /api/ and keep resolving under base_url. if path.startswith("http"): return path - if path.startswith("/api/"): - return self.base_url + path + if path.startswith("/") and "/api/" in path: + return self._origin_url + path return self.base_url + _API + path def headers(self, url: str, extra: dict[str, str] | None = None) -> dict[str, str]: diff --git a/tests/integration/test_gateway_e2e.py b/tests/integration/test_gateway_e2e.py new file mode 100644 index 0000000..d2dac4f --- /dev/null +++ b/tests/integration/test_gateway_e2e.py @@ -0,0 +1,147 @@ +"""Live end-to-end test of the SDK against a serverless gateway deployment. + +Exercises the image-edit flow the platform team verified by hand on +2026-07-23: upload an input asset, submit an SD1.5 img2img workflow that +references it, poll to a terminal state, download the output. + +Skipped unless pointed at a live deployment: + + export COMFY_BASE_URL="https://stagingplatformapi.comfy.org/deployment/" + export COMFY_API_KEY="comfyui-..." + pytest tests/integration/test_gateway_e2e.py -v + +Optional: COMFY_CKPT_NAME (defaults to the staging test distribution's +SD1.5 checkpoint). First run streams a full multipart upload; reruns hit +the by-hash dedup fast-path (the input image is deterministic), so both +upload paths get coverage across two runs. +""" + +from __future__ import annotations + +import os +import struct +import zlib + +import pytest + +from comfy_sdk import Comfy + +BASE_URL = os.environ.get("COMFY_BASE_URL") +API_KEY = os.environ.get("COMFY_API_KEY") +CKPT_NAME = os.environ.get("COMFY_CKPT_NAME", "v1-5-pruned-emaonly.safetensors") +INPUT_NAME = "sdk_e2e_input.png" +JOB_TIMEOUT_S = 600 # cold start on a scale-to-zero deployment takes minutes + +pytestmark = pytest.mark.skipif( + not (BASE_URL and API_KEY), + reason="set COMFY_BASE_URL and COMFY_API_KEY to run gateway e2e tests", +) + + +def _gradient_png(width: int = 512, height: int = 512) -> bytes: + """A deterministic RGB gradient PNG, stdlib-only (no PIL dependency).""" + + def chunk(tag: bytes, data: bytes) -> bytes: + return ( + struct.pack(">I", len(data)) + + tag + + data + + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF) + ) + + raw = b"".join( + b"\x00" + + bytes( + v + for x in range(width) + for v in (x * 255 // width, y * 255 // height, 128) + ) + for y in range(height) + ) + ihdr = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0) + return ( + b"\x89PNG\r\n\x1a\n" + + chunk(b"IHDR", ihdr) + + chunk(b"IDAT", zlib.compress(raw, 6)) + + chunk(b"IEND", b"") + ) + + +def _img2img_workflow(image_ref: object) -> dict: + return { + "1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": CKPT_NAME}}, + "2": {"class_type": "LoadImage", "inputs": {"image": image_ref}}, + "3": {"class_type": "VAEEncode", "inputs": {"pixels": ["2", 0], "vae": ["1", 2]}}, + "4": { + "class_type": "CLIPTextEncode", + "inputs": {"text": "oil painting, thick brushstrokes, vivid colors", "clip": ["1", 1]}, + }, + "5": {"class_type": "CLIPTextEncode", "inputs": {"text": "blurry, low quality", "clip": ["1", 1]}}, + "6": { + "class_type": "KSampler", + "inputs": { + "model": ["1", 0], + "positive": ["4", 0], + "negative": ["5", 0], + "latent_image": ["3", 0], + "seed": 42, + "steps": 20, + "cfg": 7, + "sampler_name": "euler", + "scheduler": "normal", + "denoise": 0.6, + }, + }, + "7": {"class_type": "VAEDecode", "inputs": {"samples": ["6", 0], "vae": ["1", 2]}}, + "8": {"class_type": "SaveImage", "inputs": {"filename_prefix": "sdk_e2e", "images": ["7", 0]}}, + } + + +@pytest.fixture(scope="module") +def client() -> Comfy: + c = Comfy(BASE_URL, api_key=API_KEY) + yield c + c.close() + + +@pytest.fixture(scope="module") +def input_asset(client: Comfy, tmp_path_factory: pytest.TempPathFactory): + path = tmp_path_factory.mktemp("inputs") / INPUT_NAME + path.write_bytes(_gradient_png()) + asset = client.assets.from_file(path) + asset.commit() + return asset + + +def test_upload_dedup_roundtrip(client: Comfy, input_asset) -> None: + again = client.assets.from_bytes(_gradient_png(), filename=INPUT_NAME) + assert again.commit() == input_asset.commit() + + +def test_image_edit_by_name(client: Comfy, input_asset, tmp_path) -> None: + wf = client.workflows.from_json(_img2img_workflow(INPUT_NAME)) + job = client.run(wf).wait(timeout=JOB_TIMEOUT_S) + + assert job.status == "succeeded", f"job {job.id} ended {job.status}: {job.error}" + outputs = job.get_outputs("8") or job.outputs + assert outputs, f"job {job.id} succeeded with no outputs" + + out_path = tmp_path / "sdk_e2e_out.png" + outputs[0].to_file(out_path) + data = out_path.read_bytes() + assert data[:8] == b"\x89PNG\r\n\x1a\n" + assert len(data) > 10_000 + + +@pytest.mark.xfail( + reason="gateway does not yet resolve core/ASSET references in the graph; " + "it resolves string filename references only", + strict=False, +) +def test_image_edit_by_asset_handle(client: Comfy, input_asset, tmp_path) -> None: + wf = client.workflows.from_json(_img2img_workflow(INPUT_NAME)) + wf.set_input("2", "image", input_asset) + job = client.run(wf).wait(timeout=JOB_TIMEOUT_S) + + assert job.status == "succeeded", f"job {job.id} ended {job.status}: {job.error}" + assert job.outputs, f"job {job.id} succeeded with no outputs" diff --git a/tests/test_follow_up_links.py b/tests/test_follow_up_links.py new file mode 100644 index 0000000..e78d396 --- /dev/null +++ b/tests/test_follow_up_links.py @@ -0,0 +1,48 @@ +"""Follow-up links (``job.urls.*``) resolve against the origin, not base_url. + +A server mounts the v2 contract wherever it likes — the serverless gateway +serves it under ``/deployment/{id}/api/v2`` — and its host-relative follow-up +links already include that mount prefix. Resolving them against ``base_url`` +(which carries the same prefix) doubles it and 404s; they must resolve against +the scheme+authority only. Internal shorthand paths (``/jobs/…``, ``/assets…``) +keep resolving under ``base_url + /api/v2``. + +Regression for the SDK↔gateway conformance bug found 2026-07-23: polling a +job submitted through a deployment-scoped base URL 404'd on refresh. +""" + +from __future__ import annotations + +from comfy_low.transport import _Prepared + +GATEWAY_BASE = "https://stagingplatformapi.comfy.org/deployment/dep_123" +CLOUD_BASE = "https://api.comfy.org" + + +def test_gateway_self_link_resolves_against_origin() -> None: + p = _Prepared(GATEWAY_BASE, "comfyui-k") + link = "/deployment/dep_123/api/v2/jobs/j1" + assert p.url(link) == "https://stagingplatformapi.comfy.org" + link + + +def test_gateway_internal_path_keeps_deployment_prefix() -> None: + p = _Prepared(GATEWAY_BASE, "comfyui-k") + assert p.url("/jobs/j1") == GATEWAY_BASE + "/api/v2/jobs/j1" + assert p.url("/assets") == GATEWAY_BASE + "/api/v2/assets" + + +def test_cloud_self_link_unchanged() -> None: + p = _Prepared(CLOUD_BASE, "comfyui-k") + assert p.url("/api/v2/jobs/j1") == CLOUD_BASE + "/api/v2/jobs/j1" + + +def test_absolute_link_passes_through() -> None: + p = _Prepared(GATEWAY_BASE, "comfyui-k") + url = "https://elsewhere.example/api/v2/jobs/j1" + assert p.url(url) == url + + +def test_auth_still_attaches_to_origin_resolved_links() -> None: + p = _Prepared(GATEWAY_BASE, "comfyui-k") + url = p.url("/deployment/dep_123/api/v2/jobs/j1") + assert p.headers(url)["Authorization"] == "Bearer comfyui-k" From 32be4fea8ca1289657066650822ab2c695b0b09f Mon Sep 17 00:00:00 2001 From: sundar Date: Thu, 23 Jul 2026 13:59:32 -0700 Subject: [PATCH 2/6] trim url() comment to the load-bearing why Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KV7wUb2hyzKHsR52ZJTvAm --- src/comfy_low/transport.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/comfy_low/transport.py b/src/comfy_low/transport.py index 0f728a0..207f6bf 100644 --- a/src/comfy_low/transport.py +++ b/src/comfy_low/transport.py @@ -105,12 +105,9 @@ def __init__(self, base_url: str, api_key: str | None, client_info: str | None = self._user_agent = _build_user_agent(client_info) def url(self, path: str) -> str: - # A path may be a follow-up link (job.urls.*) or an internal API path. - # Follow-up links are host-relative and already carry the mount prefix - # the server serves under (e.g. a gateway's /deployment/{id}/api/v2), - # so they resolve against the origin, not base_url — resolving against - # base_url would double the prefix. Internal shorthand paths (/jobs/…, - # /assets…) never contain /api/ and keep resolving under base_url. + # A server link (job.urls.*, marked by containing /api/) already carries + # the server's mount prefix, so it resolves against the origin — joining + # it to base_url would double the prefix on a prefix-mounted surface. if path.startswith("http"): return path if path.startswith("/") and "/api/" in path: From eb4628bf923120967f046442aaf651414dfed489 Mon Sep 17 00:00:00 2001 From: sundar Date: Thu, 23 Jul 2026 14:02:04 -0700 Subject: [PATCH 3/6] drop dated/hand-verification notes from test docstrings Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KV7wUb2hyzKHsR52ZJTvAm --- tests/integration/test_gateway_e2e.py | 8 +++----- tests/test_follow_up_links.py | 3 --- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/integration/test_gateway_e2e.py b/tests/integration/test_gateway_e2e.py index d2dac4f..ba59ab7 100644 --- a/tests/integration/test_gateway_e2e.py +++ b/tests/integration/test_gateway_e2e.py @@ -1,8 +1,6 @@ -"""Live end-to-end test of the SDK against a serverless gateway deployment. - -Exercises the image-edit flow the platform team verified by hand on -2026-07-23: upload an input asset, submit an SD1.5 img2img workflow that -references it, poll to a terminal state, download the output. +"""Live end-to-end test of the SDK against a serverless gateway deployment: +upload an input asset, submit an SD1.5 img2img workflow that references it, +poll to a terminal state, download the output. Skipped unless pointed at a live deployment: diff --git a/tests/test_follow_up_links.py b/tests/test_follow_up_links.py index e78d396..c5d6ab7 100644 --- a/tests/test_follow_up_links.py +++ b/tests/test_follow_up_links.py @@ -6,9 +6,6 @@ (which carries the same prefix) doubles it and 404s; they must resolve against the scheme+authority only. Internal shorthand paths (``/jobs/…``, ``/assets…``) keep resolving under ``base_url + /api/v2``. - -Regression for the SDK↔gateway conformance bug found 2026-07-23: polling a -job submitted through a deployment-scoped base URL 404'd on refresh. """ from __future__ import annotations From 80c06a3b174c67ee86faebb4dd72758c0accf836 Mon Sep 17 00:00:00 2001 From: sundar Date: Thu, 23 Jul 2026 14:11:15 -0700 Subject: [PATCH 4/6] make the gateway e2e workflow model-free (core nodes only) LoadImage -> ImageInvert -> SaveImage proves the same protocol surface as the img2img graph but runs on any deployment with nothing baked, in seconds; drops the COMFY_CKPT_NAME knob. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KV7wUb2hyzKHsR52ZJTvAm --- tests/integration/test_gateway_e2e.py | 51 ++++++++------------------- 1 file changed, 14 insertions(+), 37 deletions(-) diff --git a/tests/integration/test_gateway_e2e.py b/tests/integration/test_gateway_e2e.py index ba59ab7..5cb8f98 100644 --- a/tests/integration/test_gateway_e2e.py +++ b/tests/integration/test_gateway_e2e.py @@ -1,6 +1,6 @@ """Live end-to-end test of the SDK against a serverless gateway deployment: -upload an input asset, submit an SD1.5 img2img workflow that references it, -poll to a terminal state, download the output. +upload an input asset, submit a workflow that references it, poll to a +terminal state, download the output. Skipped unless pointed at a live deployment: @@ -8,10 +8,10 @@ export COMFY_API_KEY="comfyui-..." pytest tests/integration/test_gateway_e2e.py -v -Optional: COMFY_CKPT_NAME (defaults to the staging test distribution's -SD1.5 checkpoint). First run streams a full multipart upload; reruns hit -the by-hash dedup fast-path (the input image is deterministic), so both -upload paths get coverage across two runs. +The workflow uses only core nodes (no models), so any deployment works. +First run streams a full multipart upload; reruns hit the by-hash dedup +fast-path (the input image is deterministic), so both upload paths get +coverage across two runs. """ from __future__ import annotations @@ -26,7 +26,6 @@ BASE_URL = os.environ.get("COMFY_BASE_URL") API_KEY = os.environ.get("COMFY_API_KEY") -CKPT_NAME = os.environ.get("COMFY_CKPT_NAME", "v1-5-pruned-emaonly.safetensors") INPUT_NAME = "sdk_e2e_input.png" JOB_TIMEOUT_S = 600 # cold start on a scale-to-zero deployment takes minutes @@ -65,33 +64,11 @@ def chunk(tag: bytes, data: bytes) -> bytes: ) -def _img2img_workflow(image_ref: object) -> dict: +def _edit_workflow(image_ref: object) -> dict: return { - "1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": CKPT_NAME}}, - "2": {"class_type": "LoadImage", "inputs": {"image": image_ref}}, - "3": {"class_type": "VAEEncode", "inputs": {"pixels": ["2", 0], "vae": ["1", 2]}}, - "4": { - "class_type": "CLIPTextEncode", - "inputs": {"text": "oil painting, thick brushstrokes, vivid colors", "clip": ["1", 1]}, - }, - "5": {"class_type": "CLIPTextEncode", "inputs": {"text": "blurry, low quality", "clip": ["1", 1]}}, - "6": { - "class_type": "KSampler", - "inputs": { - "model": ["1", 0], - "positive": ["4", 0], - "negative": ["5", 0], - "latent_image": ["3", 0], - "seed": 42, - "steps": 20, - "cfg": 7, - "sampler_name": "euler", - "scheduler": "normal", - "denoise": 0.6, - }, - }, - "7": {"class_type": "VAEDecode", "inputs": {"samples": ["6", 0], "vae": ["1", 2]}}, - "8": {"class_type": "SaveImage", "inputs": {"filename_prefix": "sdk_e2e", "images": ["7", 0]}}, + "1": {"class_type": "LoadImage", "inputs": {"image": image_ref}}, + "2": {"class_type": "ImageInvert", "inputs": {"image": ["1", 0]}}, + "3": {"class_type": "SaveImage", "inputs": {"filename_prefix": "sdk_e2e", "images": ["2", 0]}}, } @@ -117,11 +94,11 @@ def test_upload_dedup_roundtrip(client: Comfy, input_asset) -> None: def test_image_edit_by_name(client: Comfy, input_asset, tmp_path) -> None: - wf = client.workflows.from_json(_img2img_workflow(INPUT_NAME)) + wf = client.workflows.from_json(_edit_workflow(INPUT_NAME)) job = client.run(wf).wait(timeout=JOB_TIMEOUT_S) assert job.status == "succeeded", f"job {job.id} ended {job.status}: {job.error}" - outputs = job.get_outputs("8") or job.outputs + outputs = job.get_outputs("3") or job.outputs assert outputs, f"job {job.id} succeeded with no outputs" out_path = tmp_path / "sdk_e2e_out.png" @@ -137,8 +114,8 @@ def test_image_edit_by_name(client: Comfy, input_asset, tmp_path) -> None: strict=False, ) def test_image_edit_by_asset_handle(client: Comfy, input_asset, tmp_path) -> None: - wf = client.workflows.from_json(_img2img_workflow(INPUT_NAME)) - wf.set_input("2", "image", input_asset) + wf = client.workflows.from_json(_edit_workflow(INPUT_NAME)) + wf.set_input("1", "image", input_asset) job = client.run(wf).wait(timeout=JOB_TIMEOUT_S) assert job.status == "succeeded", f"job {job.id} ended {job.status}: {job.error}" From 52391bd7b5d2119176dc6e61e64d1f0dde3bec64 Mon Sep 17 00:00:00 2001 From: sundar Date: Thu, 23 Jul 2026 14:18:07 -0700 Subject: [PATCH 5/6] gateway e2e: allow overriding the test workflow via COMFY_WORKFLOW_FILE Default stays the model-free core-node graph; an override supplies any API-format workflow, whose first LoadImage node is rewired to the uploaded test input. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KV7wUb2hyzKHsR52ZJTvAm --- tests/integration/test_gateway_e2e.py | 56 +++++++++++++++++++-------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/tests/integration/test_gateway_e2e.py b/tests/integration/test_gateway_e2e.py index 5cb8f98..7dbce68 100644 --- a/tests/integration/test_gateway_e2e.py +++ b/tests/integration/test_gateway_e2e.py @@ -8,7 +8,12 @@ export COMFY_API_KEY="comfyui-..." pytest tests/integration/test_gateway_e2e.py -v -The workflow uses only core nodes (no models), so any deployment works. +The default workflow uses only core nodes (no models), so any deployment +works. Point COMFY_WORKFLOW_FILE at an API-format workflow JSON to run your +own instead: its first LoadImage node is rewired to the uploaded test input, +so it must contain one (and its distribution's models must be baked into the +target deployment). + First run streams a full multipart upload; reruns hit the by-hash dedup fast-path (the input image is deterministic), so both upload paths get coverage across two runs. @@ -16,9 +21,11 @@ from __future__ import annotations +import json import os import struct import zlib +from pathlib import Path import pytest @@ -27,6 +34,7 @@ BASE_URL = os.environ.get("COMFY_BASE_URL") API_KEY = os.environ.get("COMFY_API_KEY") INPUT_NAME = "sdk_e2e_input.png" +WORKFLOW_FILE = os.environ.get("COMFY_WORKFLOW_FILE") JOB_TIMEOUT_S = 600 # cold start on a scale-to-zero deployment takes minutes pytestmark = pytest.mark.skipif( @@ -64,12 +72,24 @@ def chunk(tag: bytes, data: bytes) -> bytes: ) -def _edit_workflow(image_ref: object) -> dict: - return { - "1": {"class_type": "LoadImage", "inputs": {"image": image_ref}}, - "2": {"class_type": "ImageInvert", "inputs": {"image": ["1", 0]}}, - "3": {"class_type": "SaveImage", "inputs": {"filename_prefix": "sdk_e2e", "images": ["2", 0]}}, - } +DEFAULT_WORKFLOW = { + "1": {"class_type": "LoadImage", "inputs": {"image": ""}}, + "2": {"class_type": "ImageInvert", "inputs": {"image": ["1", 0]}}, + "3": {"class_type": "SaveImage", "inputs": {"filename_prefix": "sdk_e2e", "images": ["2", 0]}}, +} + + +def _edit_workflow(image_ref: object) -> tuple[dict, str]: + """The graph to run and the node id of the LoadImage wired to ``image_ref``.""" + if WORKFLOW_FILE: + graph = json.loads(Path(WORKFLOW_FILE).read_text()) + else: + graph = json.loads(json.dumps(DEFAULT_WORKFLOW)) + for node_id, node in graph.items(): + if node.get("class_type") == "LoadImage": + node["inputs"]["image"] = image_ref + return graph, node_id + raise AssertionError("workflow has no LoadImage node to receive the test input") @pytest.fixture(scope="module") @@ -94,18 +114,19 @@ def test_upload_dedup_roundtrip(client: Comfy, input_asset) -> None: def test_image_edit_by_name(client: Comfy, input_asset, tmp_path) -> None: - wf = client.workflows.from_json(_edit_workflow(INPUT_NAME)) - job = client.run(wf).wait(timeout=JOB_TIMEOUT_S) + graph, _ = _edit_workflow(INPUT_NAME) + job = client.run(client.workflows.from_json(graph)).wait(timeout=JOB_TIMEOUT_S) assert job.status == "succeeded", f"job {job.id} ended {job.status}: {job.error}" - outputs = job.get_outputs("3") or job.outputs - assert outputs, f"job {job.id} succeeded with no outputs" + assert job.outputs, f"job {job.id} succeeded with no outputs" - out_path = tmp_path / "sdk_e2e_out.png" - outputs[0].to_file(out_path) + out_path = tmp_path / "sdk_e2e_out" + job.outputs[0].to_file(out_path) data = out_path.read_bytes() - assert data[:8] == b"\x89PNG\r\n\x1a\n" - assert len(data) > 10_000 + assert data, "downloaded output is empty" + if not WORKFLOW_FILE: + assert data[:8] == b"\x89PNG\r\n\x1a\n" + assert len(data) > 10_000 @pytest.mark.xfail( @@ -114,8 +135,9 @@ def test_image_edit_by_name(client: Comfy, input_asset, tmp_path) -> None: strict=False, ) def test_image_edit_by_asset_handle(client: Comfy, input_asset, tmp_path) -> None: - wf = client.workflows.from_json(_edit_workflow(INPUT_NAME)) - wf.set_input("1", "image", input_asset) + graph, load_node = _edit_workflow(INPUT_NAME) + wf = client.workflows.from_json(graph) + wf.set_input(load_node, "image", input_asset) job = client.run(wf).wait(timeout=JOB_TIMEOUT_S) assert job.status == "succeeded", f"job {job.id} ended {job.status}: {job.error}" From 5c08487767b8c33227d18738a4030bc12eaecd1e Mon Sep 17 00:00:00 2001 From: sundar Date: Thu, 23 Jul 2026 14:21:41 -0700 Subject: [PATCH 6/6] assert output dimensions, not compressed size An inverted gradient re-encoded with PNG row filters legitimately compresses to ~2KB; size thresholds are encoder trivia. Match the IHDR dimensions against the input instead. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KV7wUb2hyzKHsR52ZJTvAm --- tests/integration/test_gateway_e2e.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_gateway_e2e.py b/tests/integration/test_gateway_e2e.py index 7dbce68..184aa97 100644 --- a/tests/integration/test_gateway_e2e.py +++ b/tests/integration/test_gateway_e2e.py @@ -126,7 +126,8 @@ def test_image_edit_by_name(client: Comfy, input_asset, tmp_path) -> None: assert data, "downloaded output is empty" if not WORKFLOW_FILE: assert data[:8] == b"\x89PNG\r\n\x1a\n" - assert len(data) > 10_000 + width, height = struct.unpack(">II", data[16:24]) + assert (width, height) == (512, 512) @pytest.mark.xfail(