Item 71: comfy_nodes_workflow.py is an exact Python port of the Revise view's "copy as nodes" graph builder (pinned to a golden file generated from the JS, now in resources/logic_comfynodes.js) plus lossless PNG tEXt / WebP EXIF writers in the format ComfyUI's frontend reads on drop. The Builder's new Run Setting "Embed ComfyUI Nodes Workflow" (session_settings.pack_nodes_metadata) stamps every saved image at every save site (generation, remote VAE, inline/deferred upscales, dashboard upscales, worker images via a master pass after collection); export_favorites falls back to the Python builder. Item 93: the upscale modal gets step/pipeline reordering, inline validation (mirrored server-side, 400 on bad configs), a preset bar (Update / Save as / Revert / Delete, read-modify-write), "Run once (unsaved)", an output summary, separate Cancel and Close, a running lock and reattach after closing. Cancel now lands within one sampler step through a per-job unet function wrapper on a cloned model (never the process-wide interrupt flag), keeps completed outputs, and sends the final progress event. Also fixed: the dashboard received the session's OLDEST items as "new" after an upscale (slice at the wrong end), the poll targeted the wrong button, jobs were never pruned, two jobs could rewrite one session's manifest (409 guard). Known quirk recorded, not changed: a final pipeline step with several ratio/denoise values saves only the last combination; the summary and a warning now say so. Tests: 101 python, 59 node, 9 Playwright. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
48 lines
1.8 KiB
Python
48 lines
1.8 KiB
Python
"""Server-side upscale config validation: shared fixture with tests/js/upscale_edit.test.js."""
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
sys.path.insert(0, _ROOT)
|
|
|
|
from tests.upscale_runner_loader import load_runner # noqa: E402
|
|
|
|
with open(os.path.join(_ROOT, "tests", "fixtures", "upscale_validation_cases.json"), encoding="utf-8") as _f:
|
|
CASES = json.load(_f)
|
|
|
|
ur = load_runner()
|
|
|
|
|
|
@pytest.mark.parametrize("case", CASES, ids=[c["name"] for c in CASES])
|
|
def test_validation_cases(case):
|
|
errors, _warnings = ur._validate_upscale(case["cfg"])
|
|
assert (not errors) == case["expect_ok"], errors
|
|
assert sorted({e["field"] for e in errors}) == sorted(set(case["error_fields"]))
|
|
# the string form used by the route
|
|
msgs = ur.validate_upscale_config(case["cfg"])
|
|
assert (not msgs) == case["expect_ok"]
|
|
|
|
|
|
def test_error_paths_point_at_the_step():
|
|
cfg = {"pipelines": [{"steps": [{}]}, {"steps": [{}, {"hires_denoise": "2"}]}]}
|
|
errors, _ = ur._validate_upscale(cfg)
|
|
assert [e["path"] for e in errors] == ["p1.s1"]
|
|
assert ur.validate_upscale_config(cfg)[0].startswith("p1.s1:")
|
|
|
|
|
|
def test_warnings_not_errors():
|
|
cfg = {"pipelines": [{"steps": [{"mode": "florence2_hires", "hires_denoise": "0.4,0.5",
|
|
"florence2": {"text_input": "face"}}]}],
|
|
"hires_prompt_adjust": True, "hires_prompt_text": ""}
|
|
errors, warnings = ur._validate_upscale(cfg)
|
|
assert errors == []
|
|
assert {w["field"] for w in warnings} == {"hires_denoise", "hires_prompt_text"}
|
|
|
|
|
|
def test_garbage_config_does_not_raise():
|
|
for bad in (None, [], "x", {"pipelines": "nope"}, {"pipelines": [None, 3, {"steps": "x"}]}):
|
|
assert ur.validate_upscale_config(bad)
|