Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion comfy_cli/command/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,20 @@ class _ResponseTooLarge(Exception):
"""A ``/userdata`` response exceeded ``_USERDATA_MAX_BYTES`` — refuse to truncate."""


# What an oversize ``/userdata`` response means depends on the verb, so the hint
# has to as well. ``list`` fetches the whole ``workflows/`` listing, so oversize
# means *too many* workflows — not one big one. ``save``/``delete`` have already
# sent their request by the time the body is read, so the write may well have
# landed: their hints must not imply a clean failure the user should retry.
_LOCAL_TOO_LARGE_HINTS = {
"list": "too many saved workflows on the server; prune the ComfyUI `workflows/` userdata directory "
"(`--limit`/`--name` filter client-side, so they cannot shrink the response)",
"get": "the saved workflow is unexpectedly large; inspect it directly on the server",
"save": "the workflow may still have been saved; confirm with `comfy --json --where local workflow list`",
"delete": "the workflow may still have been deleted; confirm with `comfy --json --where local workflow list`",
}


# Map the cloud ``--sort`` fields onto local FileInfo keys (client-side sort;
# ComfyUI's /userdata listing has no server-side sort/limit/filter).
_LOCAL_SORT_KEYS = {"create_time": "created", "update_time": "modified", "name": "path"}
Expand Down Expand Up @@ -530,7 +544,7 @@ def _handle_local_http_error(renderer, e, *, operation: str, workflow_id: str |
code="workflow_too_large",
message=f"local ComfyUI /userdata response during {operation} exceeded the "
f"{_USERDATA_MAX_BYTES // (1024 * 1024)} MiB cap",
hint="the saved workflow is unexpectedly large; inspect it directly on the server",
hint=_LOCAL_TOO_LARGE_HINTS.get(operation, "the local response was unexpectedly large"),
details={"operation": operation, "limit_bytes": _USERDATA_MAX_BYTES},
)
elif isinstance(e, urllib.error.HTTPError) and e.code == 404:
Expand Down
52 changes: 52 additions & 0 deletions tests/comfy_cli/command/test_workflow_saved.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from typing import Any

import pytest
import typer
from typer.testing import CliRunner

from comfy_cli.caller import Caller
Expand Down Expand Up @@ -418,6 +419,57 @@ def test_rejects_absolute_path(self, local_target, monkeypatch, capsys):
assert env["error"]["code"] == "invalid_argument"


class TestLocalTooLargeHints:
"""An oversize /userdata response means something different per verb, so the
hint must too — a `list` overflow is too many workflows, and save/delete have
already sent their write by the time the body is read."""

def _args(self, verb: str, tmp_path) -> list[str]:
if verb == "list":
return ["list", "--where", "local"]
if verb == "get":
return ["get", "flux.json", "--out", str(tmp_path / "o.json"), "--where", "local"]
if verb == "save":
src = tmp_path / "src.json"
src.write_text(json.dumps(_LOCAL_WORKFLOW))
return ["save", str(src), "--name", "flux", "--where", "local"]
return ["delete", "flux.json", "--where", "local"]

@pytest.mark.parametrize("verb", ["list", "get", "save", "delete"])
def test_hint_is_per_operation(self, verb, local_target, tmp_path, monkeypatch, capsys):
monkeypatch.setattr(workflow_cmd, "_USERDATA_MAX_BYTES", 4)
_patch_urlopen(monkeypatch, {"/userdata": (b'[{"path": "flux.json"}]', 200)})
env = _run(self._args(verb, tmp_path), capsys)
assert env["ok"] is False
assert env["error"]["code"] == "workflow_too_large"
assert env["error"]["details"]["operation"] == verb
assert env["error"]["hint"] == workflow_cmd._LOCAL_TOO_LARGE_HINTS[verb]

def test_list_hint_does_not_name_a_single_workflow(self):
# The listing is the whole workflows/ dir — "the saved workflow" (the old
# hardcoded hint) names a thing that doesn't exist for this verb.
assert "the saved workflow is" not in workflow_cmd._LOCAL_TOO_LARGE_HINTS["list"]

@pytest.mark.parametrize("verb", ["save", "delete"])
def test_write_verb_hints_do_not_imply_the_write_was_rejected(self, verb):
# The request is already on the wire before the body is read, so the
# mutation may have landed. The hint must send the user to confirm, not
# imply a clean failure they should retry.
hint = workflow_cmd._LOCAL_TOO_LARGE_HINTS[verb]
assert "may still have been" in hint
assert "workflow list" in hint

def test_unknown_operation_falls_back(self, capsys):
# Defensive: a future call site with a new operation still gets a hint,
# and one that claims nothing about what did or didn't happen.
renderer = _force_json_renderer()
exit_exc = workflow_cmd._handle_local_http_error(renderer, workflow_cmd._ResponseTooLarge(), operation="purge")
assert isinstance(exit_exc, typer.Exit)
env = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
assert env["error"]["code"] == "workflow_too_large"
assert env["error"]["hint"] == "the local response was unexpectedly large"


# ---------------------------------------------------------------------------
# list
# ---------------------------------------------------------------------------
Expand Down
Loading