Files
ComfyUI-Lora-Manager/tests/services/test_recipe_import_info.py
T
Will Miao bccd494a56 feat(recipes): explain empty LoRA lists with collapsible "Why no LoRAs?" panel
Record import provenance on every recipe: a new import_info block
(channel, machine-readable no-LoRA reason, diagnostic details) built at
import time across all channels (batch import, single URL, local file,
upload, widget save, re-imports) and persisted in the recipe JSON plus
the SQLite persistent cache (new import_info_json column with ALTER
TABLE migration).

The recipe modal renders the empty LoRA list with a collapsed details
panel showing the import method, the reason (CivitAI API returned no
LoRA resource data, API meta missing, no embedded metadata, ComfyUI
workflow metadata, video, unparsable format), and recorded diagnostics.
Legacy recipes without import_info fall back to heuristics labeled as
inferred. Genuine no-LoRA generations show no panel.

CivitAI images are always classified by API meta shape: the onsite
generator writes A1111-style EXIF without LoRA references, so parsed
EXIF cannot prove "no LoRAs used".

Adds recipes.resources.noLoras* i18n keys (all 10 locales) plus
frontend vitest and backend pytest coverage.
2026-08-30 16:28:41 +08:00

189 lines
6.5 KiB
Python

"""Tests for the recipe import_info helpers (no-LoRA reason computation)."""
from py.services.recipes.import_info import (
CHANNEL_BATCH_IMPORT_LOCAL,
CHANNEL_BATCH_IMPORT_URL,
CHANNEL_LOCAL,
CHANNEL_REIMPORT_URL,
CHANNEL_UPLOAD,
CHANNEL_URL,
CHANNEL_WIDGET,
REASON_API_META_MISSING,
REASON_API_NO_LORA_RESOURCES,
REASON_METADATA_UNSUPPORTED,
REASON_NO_EMBEDDED_METADATA,
REASON_NO_LORAS_USED,
REASON_VIDEO_NO_METADATA,
REASON_WORKFLOW_METADATA_LIMITED,
build_import_info,
compute_no_loras_reason,
)
class TestComputeNoLorasReason:
def test_video_takes_priority(self):
diag = {"is_video": True, "exif_parser": "ComfyMetadataParser"}
assert (
compute_no_loras_reason(CHANNEL_BATCH_IMPORT_URL, diag)
== REASON_VIDEO_NO_METADATA
)
def test_comfy_workflow_parser(self):
diag = {"exif_parser": "ComfyMetadataParser", "exif_present": True}
assert (
compute_no_loras_reason(CHANNEL_UPLOAD, diag)
== REASON_WORKFLOW_METADATA_LIMITED
)
def test_merged_comfy_parser(self):
# ComfyUI workflow parsed from the merged dict (no string EXIF).
diag = {"parser": "ComfyMetadataParser"}
assert (
compute_no_loras_reason(CHANNEL_LOCAL, diag)
== REASON_WORKFLOW_METADATA_LIMITED
)
def test_civitai_url_with_api_meta_but_no_lora_resources(self):
diag = {
"civitai_image": True,
"api_meta_keys": ["prompt"],
"api_model_version_ids": 0,
"exif_present": False,
}
assert (
compute_no_loras_reason(CHANNEL_BATCH_IMPORT_URL, diag)
== REASON_API_NO_LORA_RESOURCES
)
def test_civitai_url_with_model_version_ids_only(self):
diag = {
"civitai_image": True,
"api_meta_keys": [],
"api_model_version_ids": 2,
"exif_present": False,
}
assert (
compute_no_loras_reason(CHANNEL_URL, diag)
== REASON_API_NO_LORA_RESOURCES
)
def test_civitai_url_with_no_meta_at_all(self):
diag = {
"civitai_image": True,
"api_meta_keys": [],
"api_model_version_ids": 0,
"exif_present": False,
}
assert (
compute_no_loras_reason(CHANNEL_REIMPORT_URL, diag)
== REASON_API_META_MISSING
)
def test_civitai_url_with_parsed_exif_still_reports_api_gap(self):
# CivitAI's onsite generator writes A1111-style EXIF WITHOUT LoRA
# references (LoRA usage lives in CivitAI-internal data), so cleanly
# parsed EXIF must NOT be read as "no LoRAs were used".
diag = {
"civitai_image": True,
"api_meta_keys": ["prompt", "steps", "seed", "resources"],
"api_model_version_ids": 1,
"exif_present": True,
"exif_parser": "AutomaticMetadataParser",
"parser": "CivitaiApiMetadataParser",
}
assert (
compute_no_loras_reason(CHANNEL_BATCH_IMPORT_URL, diag)
== REASON_API_NO_LORA_RESOURCES
)
def test_generic_url_without_embedded_metadata(self):
diag = {"civitai_image": False, "exif_present": False}
assert (
compute_no_loras_reason(CHANNEL_URL, diag) == REASON_NO_EMBEDDED_METADATA
)
def test_generic_url_with_unsupported_metadata(self):
diag = {"civitai_image": False, "exif_present": True}
assert (
compute_no_loras_reason(CHANNEL_URL, diag) == REASON_METADATA_UNSUPPORTED
)
def test_generic_url_with_parsed_metadata_means_no_loras(self):
diag = {
"civitai_image": False,
"exif_present": True,
"exif_parser": "AutomaticMetadataParser",
}
assert compute_no_loras_reason(CHANNEL_URL, diag) == REASON_NO_LORAS_USED
def test_widget(self):
assert compute_no_loras_reason(CHANNEL_WIDGET, None) == REASON_NO_LORAS_USED
def test_local_without_embedded_metadata(self):
diag = {"exif_present": False}
assert (
compute_no_loras_reason(CHANNEL_LOCAL, diag)
== REASON_NO_EMBEDDED_METADATA
)
assert (
compute_no_loras_reason(CHANNEL_BATCH_IMPORT_LOCAL, diag)
== REASON_NO_EMBEDDED_METADATA
)
def test_local_with_unsupported_metadata(self):
diag = {"exif_present": True}
assert (
compute_no_loras_reason(CHANNEL_UPLOAD, diag)
== REASON_METADATA_UNSUPPORTED
)
def test_missing_diagnostics_falls_back_safely(self):
assert (
compute_no_loras_reason(CHANNEL_LOCAL, None)
== REASON_NO_EMBEDDED_METADATA
)
# URL channel without diagnostics is treated as a generic URL (the
# civitai_image flag defaults to False).
assert (
compute_no_loras_reason(CHANNEL_URL, None)
== REASON_NO_EMBEDDED_METADATA
)
class TestBuildImportInfo:
def test_channel_always_recorded(self):
info = build_import_info(
CHANNEL_URL, None, loras=[{"file_name": "x", "hash": "abc"}]
)
assert info == {"channel": CHANNEL_URL}
def test_reason_and_details_when_no_loras(self):
diag = {
"civitai_image": True,
"api_meta_keys": ["prompt"],
"api_model_version_ids": 0,
"exif_present": False,
"exif_parser": None,
}
info = build_import_info(CHANNEL_BATCH_IMPORT_URL, diag, loras=[])
assert info["channel"] == CHANNEL_BATCH_IMPORT_URL
assert info["reason"] == REASON_API_NO_LORA_RESOURCES
assert info["details"]["api_meta_keys"] == ["prompt"]
assert info["details"]["api_model_version_ids"] == 0
assert info["details"]["exif_present"] is False
# Empty exif_parser must not leak into details.
assert "exif_parser" not in info["details"]
def test_details_omitted_when_nothing_to_report(self):
info = build_import_info(CHANNEL_WIDGET, None, loras=[])
assert info == {"channel": CHANNEL_WIDGET, "reason": REASON_NO_LORAS_USED}
def test_api_meta_keys_capped(self):
diag = {
"civitai_image": True,
"api_meta_keys": [f"k{i}" for i in range(50)],
"api_model_version_ids": 1,
}
info = build_import_info(CHANNEL_URL, diag, loras=[])
assert len(info["details"]["api_meta_keys"]) == 12