Files
willmiao-ComfyUI-Lora-Manager/tests/utils/test_workflow_extraction.py
T
Will Miao 7d071ab3aa feat(workflows): accept zip/png/webp artifacts with derived workflow documents
Library entries are the original artifact bytes (SHA256 = CivitAI identity);
container artifacts (.zip, .png, .webp) get their extracted workflow document
materialized as a *.workflow.json sidecar that follows rename/move/delete.

- workflow_extraction: json direct-read, zip candidate pick, png/webp metadata
- scanner whitelist extended; non-workflow files rejected; adjust_metadata
  fills analysis fields for downloaded entries; image artifacts self-preview
- download_manager: workflow branch end-to-end, no zip unpacking, file
  selection prefers primary then json/zip/image
- lifecycle: delete/rename/pending-delete carry the derived document
- send-to-comfy reads the derived document for container artifacts
- fix: force_refresh with no cache falls back to full initialization
  (first scan before deferred background init no longer crashes)
2026-10-28 16:47:11 +08:00

201 lines
6.6 KiB
Python

"""Tests for workflow document extraction from library artifacts."""
import json
import os
import zipfile
import pytest
from py.utils.workflow_extraction import (
WORKFLOW_DOCUMENT_SUFFIX,
extract_workflow_document,
get_workflow_document_path,
load_artifact_workflow_document,
materialize_workflow_document,
)
API_WORKFLOW = {
"1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "base.safetensors"}},
"2": {"class_type": "LoraLoader", "inputs": {"lora_name": "lora.safetensors"}},
}
UI_WORKFLOW = {
"nodes": [{"id": 1, "type": "CheckpointLoaderSimple", "widgets_values": ["base.safetensors"]}],
"links": [],
}
def _write_json(path, payload):
path.write_text(json.dumps(payload), encoding="utf-8")
def _write_zip(path, members):
with zipfile.ZipFile(path, "w") as archive:
for name, payload in members.items():
archive.writestr(name, payload)
def _write_png_with_workflow(path, workflow):
PIL = pytest.importorskip("PIL.Image")
from py.utils.exif_utils import ExifUtils
img = PIL.new("RGB", (8, 8), color=(0, 0, 0))
img.save(path, format="PNG")
ExifUtils.embed_workflow(str(path), json.dumps(workflow))
def _write_webp_with_workflow(path, workflow):
PIL = pytest.importorskip("PIL.Image")
from py.utils.exif_utils import ExifUtils
img = PIL.new("RGB", (8, 8), color=(0, 0, 0))
img.save(path, format="WEBP")
ExifUtils.embed_workflow(str(path), json.dumps(workflow))
def test_document_path_identity_for_json(tmp_path):
artifact = tmp_path / "flow.json"
assert get_workflow_document_path(str(artifact)) == str(artifact)
def test_document_path_derived_for_containers(tmp_path):
artifact = tmp_path / "flow.zip"
derived = get_workflow_document_path(str(artifact))
assert derived == str(tmp_path / f"flow{WORKFLOW_DOCUMENT_SUFFIX}")
image = tmp_path / "flow.png"
assert get_workflow_document_path(str(image)) == str(
tmp_path / f"flow{WORKFLOW_DOCUMENT_SUFFIX}"
)
def test_extract_json_artifact(tmp_path):
artifact = tmp_path / "flow.json"
_write_json(artifact, API_WORKFLOW)
document = extract_workflow_document(str(artifact))
assert document == API_WORKFLOW
def test_extract_rejects_non_workflow_json(tmp_path):
artifact = tmp_path / "settings.json"
_write_json(artifact, {"some": "config"})
assert extract_workflow_document(str(artifact)) is None
assert materialize_workflow_document(str(artifact)) is None
def test_extract_from_zip_top_level(tmp_path):
artifact = tmp_path / "pack.zip"
_write_zip(
artifact,
{
"workflow.json": json.dumps(UI_WORKFLOW),
"readme.txt": "hello",
},
)
document = extract_workflow_document(str(artifact))
assert document == UI_WORKFLOW
def test_extract_from_zip_prefers_shallow_candidates(tmp_path):
artifact = tmp_path / "pack.zip"
_write_zip(
artifact,
{
"nested/deep/config.json": json.dumps(API_WORKFLOW),
"top.json": json.dumps(UI_WORKFLOW),
},
)
document = extract_workflow_document(str(artifact))
assert document == UI_WORKFLOW
def test_extract_from_zip_skips_macosx_and_hidden(tmp_path):
artifact = tmp_path / "pack.zip"
_write_zip(
artifact,
{
"__MACOSX/workflow.json": json.dumps(UI_WORKFLOW),
".hidden/workflow.json": json.dumps(UI_WORKFLOW),
"real.json": json.dumps(API_WORKFLOW),
},
)
document = extract_workflow_document(str(artifact))
assert document == API_WORKFLOW
def test_extract_from_zip_without_workflow_returns_none(tmp_path):
artifact = tmp_path / "images.zip"
_write_zip(artifact, {"note.txt": "no graphs here", "config.json": "{}"})
assert extract_workflow_document(str(artifact)) is None
def test_extract_from_corrupt_zip_returns_none(tmp_path):
artifact = tmp_path / "broken.zip"
artifact.write_bytes(b"not a zip at all")
assert extract_workflow_document(str(artifact)) is None
def test_extract_from_png_with_embedded_workflow(tmp_path):
artifact = tmp_path / "flow.png"
_write_png_with_workflow(artifact, UI_WORKFLOW)
document = extract_workflow_document(str(artifact))
assert document == UI_WORKFLOW
def test_extract_from_webp_with_embedded_workflow(tmp_path):
artifact = tmp_path / "flow.webp"
_write_webp_with_workflow(artifact, UI_WORKFLOW)
document = extract_workflow_document(str(artifact))
assert document == UI_WORKFLOW
def test_extract_from_plain_image_returns_none(tmp_path):
PIL = pytest.importorskip("PIL.Image")
artifact = tmp_path / "plain.png"
PIL.new("RGB", (8, 8)).save(artifact, format="PNG")
assert extract_workflow_document(str(artifact)) is None
assert materialize_workflow_document(str(artifact)) is None
def test_materialize_writes_derived_document_once(tmp_path):
artifact = tmp_path / "pack.zip"
_write_zip(artifact, {"workflow.json": json.dumps(API_WORKFLOW)})
derived = materialize_workflow_document(str(artifact))
assert derived == str(tmp_path / f"pack{WORKFLOW_DOCUMENT_SUFFIX}")
with open(derived, "r", encoding="utf-8") as handle:
assert json.load(handle) == API_WORKFLOW
# A fresh derived file is reused without re-extracting: corrupt the zip
# and confirm the cached document is still returned.
artifact_mtime = os.path.getmtime(artifact)
os.utime(derived, (artifact_mtime + 1, artifact_mtime + 1))
artifact.write_bytes(b"corrupted")
assert materialize_workflow_document(str(artifact)) == derived
def test_materialize_reextracts_when_artifact_is_newer(tmp_path):
artifact = tmp_path / "pack.zip"
_write_zip(artifact, {"workflow.json": json.dumps(API_WORKFLOW)})
derived = materialize_workflow_document(str(artifact))
# Replace the artifact with a newer mtime than the derived document.
_write_zip(artifact, {"workflow.json": json.dumps(UI_WORKFLOW)})
artifact_mtime = os.path.getmtime(artifact)
os.utime(artifact, (artifact_mtime + 10, artifact_mtime + 10))
assert materialize_workflow_document(str(artifact)) == derived
with open(derived, "r", encoding="utf-8") as handle:
assert json.load(handle) == UI_WORKFLOW
def test_load_artifact_workflow_document_round_trip(tmp_path):
artifact = tmp_path / "pack.zip"
_write_zip(artifact, {"workflow.json": json.dumps(API_WORKFLOW)})
assert load_artifact_workflow_document(str(artifact)) == API_WORKFLOW
bare = tmp_path / "bare.json"
_write_json(bare, UI_WORKFLOW)
assert load_artifact_workflow_document(str(bare)) == UI_WORKFLOW