Files
willmiao-ComfyUI-Lora-Manager/tests/utils/test_workflow_analysis.py
T
Will Miao 720440507c feat: add opt-in workflow library management
Add a Workflows page that manages ComfyUI workflow JSON files as a
first-class library alongside loras/checkpoints/embeddings:

- opt-in via the new 'Workflow Management' settings toggle (default off)
- dedicated workflow folder roots with sidecar/preview support, separate
  from ComfyUI's own workflow storage; 'Send to ComfyUI' loads a workflow
  straight into the graph
- workflow scanner parses the graph document (UI/API format sniffing,
  node classes, model/civitai refs) and persists the analysis in the
  SQLite model cache
- settings UI: workflow folder paths editor in the Library tab and the
  standalone Model Paths page

Fixes from sandbox E2E verification:

- include the workflow scanner in startup initialization so the page no
  longer hangs on 'initializing' when enabled without folders
- add the missing workflows entry to the frontend page-state map (page
  init crashed and the sidebar fell back to the loras tree without it)
- project workflow analysis fields through the scanner cache entry and
  persist them in the model cache (new columns with auto-migration)
- keep symlinked paths intact in the directory picker (abspath instead
  of resolve), so stored library paths match what the user sees on disk

Also rebuilds the vue-widgets bundle to sync it with web/comfyui/utils.js.
2026-10-28 11:45:01 +08:00

102 lines
3.5 KiB
Python

"""Tests for py.utils.workflow_analysis: sniffing and graph extraction."""
import json
from py.utils.workflow_analysis import (
WORKFLOW_FORMAT_API,
WORKFLOW_FORMAT_UI,
analyze_workflow,
analyze_workflow_file,
load_workflow_file,
sniff_workflow_text,
)
UI_WORKFLOW = {
"nodes": [
{"id": 1, "type": "CheckpointLoaderSimple", "widgets_values": ["sd_xl_base_1.0.safetensors"]},
{"id": 2, "type": "LoraLoader", "widgets_values": ["civitai:12345@67890", 1.0, 1.0]},
{"id": 3, "type": "KSampler", "widgets_values": [42]},
],
"links": [],
"groups": [],
"version": 0.4,
}
API_WORKFLOW = {
"1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "v1-5-pruned.safetensors"}},
"2": {"class_type": "LoraLoader", "inputs": {"lora_name": "civitai:111@222", "strength_model": 1.0}},
"3": {"class_type": "SaveImage", "inputs": {"filename_prefix": "ComfyUI"}},
}
def test_sniff_ui_format():
assert sniff_workflow_text(json.dumps(UI_WORKFLOW)) == WORKFLOW_FORMAT_UI
def test_sniff_api_format():
assert sniff_workflow_text(json.dumps(API_WORKFLOW)) == WORKFLOW_FORMAT_API
def test_sniff_rejects_non_workflow_json():
assert sniff_workflow_text(json.dumps({"setting": True, "other": 1})) is None
assert sniff_workflow_text(json.dumps(["a", "b"])) is None
assert sniff_workflow_text("not json at all") is None
# A sidecar-shaped document (flat metadata fields) is not a workflow.
assert sniff_workflow_text(json.dumps({"file_name": "x", "sha256": "abc"})) is None
def test_analyze_ui_format_extracts_classes_and_refs():
analysis = analyze_workflow(UI_WORKFLOW)
assert analysis is not None
assert analysis.workflow_format == WORKFLOW_FORMAT_UI
assert analysis.node_count == 3
assert analysis.node_classes == ["CheckpointLoaderSimple", "KSampler", "LoraLoader"]
assert analysis.model_refs == ["sd_xl_base_1.0.safetensors"]
assert analysis.civitai_refs == ["civitai:12345@67890"]
def test_analyze_api_format_extracts_classes_and_refs():
analysis = analyze_workflow(API_WORKFLOW)
assert analysis is not None
assert analysis.workflow_format == WORKFLOW_FORMAT_API
assert analysis.node_count == 3
assert "SaveImage" in analysis.node_classes
assert analysis.model_refs == ["v1-5-pruned.safetensors"]
assert analysis.civitai_refs == ["civitai:111@222"]
def test_analyze_rejects_non_workflow():
assert analyze_workflow({"foo": "bar"}) is None
def test_analyze_skips_prompt_text_with_extension_like_suffix(tmp_path):
# Prompt strings contain spaces and must not become model refs.
workflow = {
"1": {
"class_type": "CLIPTextEncode",
"inputs": {"text": "a beautiful landscape, best quality.safetensors style"},
}
}
analysis = analyze_workflow(workflow)
assert analysis is not None
assert analysis.model_refs == []
def test_analyze_workflow_file_roundtrip(tmp_path):
path = tmp_path / "flow.json"
path.write_text(json.dumps(API_WORKFLOW), encoding="utf-8")
analysis = analyze_workflow_file(str(path))
assert analysis is not None
assert analysis.workflow_format == WORKFLOW_FORMAT_API
def test_load_workflow_file_rejects_sidecar(tmp_path):
sidecar = tmp_path / "flow.metadata.json"
sidecar.write_text(json.dumps({"file_name": "flow", "sha256": "x"}), encoding="utf-8")
assert load_workflow_file(str(sidecar)) is None
def test_load_workflow_file_handles_missing(tmp_path):
assert load_workflow_file(str(tmp_path / "nope.json")) is None