Files
ComfyUI-Lora-Manager/tests/utils/test_metadata_manager.py
T
Will Miao c16de68640 test: make the backend suite pass on Windows
The new informational Windows job surfaced 91 failures: four real bugs (fixed in
the previous two commits) and a long tail of POSIX assumptions in the tests
themselves. None of them assert behaviour that differs on Windows, so they are
fixed rather than skipped wherever the behaviour is the same:

* Path spelling: expectations built with `str(tmp_path)` are compared through an
  identity helper (`normalize_root_identity`) or the platform's separator, since
  the code hands out forward-slashed business paths while `str()` is native.
* Root labels: `_root_display_labels` prefixes the drive on Windows ("C: loras"),
  so label assertions compare the final path component or derive the expected
  label from the same helper.
* `~` expansion reads USERPROFILE (ntpath), not HOME; timer granularity rounds
  0.1s down; a case-only rename counts as "already named" via `os.path.normcase`.
* Genuinely POSIX-only strategies are skipped with a reason instead of failing:
  the headless clipboard fallback, dbus file-manager selection, xdg-open, and
  the case-fold index test that simulates a case-sensitive filesystem.
2026-10-12 11:35:33 +08:00

258 lines
9.0 KiB
Python

from __future__ import annotations
import json
import pytest
import os
from py.utils.metadata_manager import MetadataManager
from py.utils.models import LoraMetadata
@pytest.mark.asyncio
async def test_save_metadata_fills_missing_file_facts(tmp_path) -> None:
"""A payload missing file_name/size/modified is healed on write."""
model_path = tmp_path / "MyModel.safetensors"
model_path.write_bytes(b"fake model data")
payload = {"file_path": str(model_path), "model_name": "My Model"}
result = await MetadataManager.save_metadata(str(model_path), payload)
assert result is True
metadata_path = tmp_path / "MyModel.metadata.json"
saved = json.loads(metadata_path.read_text(encoding="utf-8"))
stat_result = model_path.stat()
assert saved["file_name"] == "MyModel"
assert saved["size"] == stat_result.st_size
assert saved["modified"] == stat_result.st_mtime
@pytest.mark.asyncio
async def test_save_metadata_keeps_existing_file_facts(tmp_path) -> None:
"""Existing file facts are never overwritten on write."""
model_path = tmp_path / "MyModel.safetensors"
model_path.write_bytes(b"fake model data")
payload = {
"file_path": str(model_path),
"file_name": "CustomName",
"size": 123,
"modified": 456.0,
}
result = await MetadataManager.save_metadata(str(model_path), payload)
assert result is True
metadata_path = tmp_path / "MyModel.metadata.json"
saved = json.loads(metadata_path.read_text(encoding="utf-8"))
assert saved["file_name"] == "CustomName"
assert saved["size"] == 123
assert saved["modified"] == 456.0
@pytest.mark.asyncio
async def test_load_metadata_payload_restores_file_facts_when_sidecar_missing(
tmp_path,
) -> None:
"""Missing sidecar: the payload is rebuilt with local file facts."""
model_path = tmp_path / "MyModel.safetensors"
model_path.write_bytes(b"fake model data")
payload = await MetadataManager.load_metadata_payload(str(model_path))
stat_result = model_path.stat()
assert payload["file_path"] == str(model_path)
assert payload["file_name"] == "MyModel"
assert payload["size"] == stat_result.st_size
assert payload["modified"] == stat_result.st_mtime
@pytest.mark.asyncio
async def test_hydrate_model_data_restores_required_fields_when_sidecar_missing(
tmp_path,
) -> None:
"""Self-heal repro: sidecar deleted, cache entry hydrated, base fields kept."""
model_path = tmp_path / "MyModel.safetensors"
model_path.write_bytes(b"fake model data")
model_data = {
"file_path": str(model_path),
"folder": "extra_loras",
"file_name": "MyModel",
"model_name": "My Model",
"size": 10,
"modified": 100.0,
"sha256": "abc123",
"base_model": "Illustrious",
"preview_url": "",
"civitai": {"id": 123},
}
await MetadataManager.hydrate_model_data(model_data)
stat_result = model_path.stat()
assert model_data["file_name"] == "MyModel"
assert model_data["model_name"] == "My Model"
assert model_data["size"] == stat_result.st_size
# `modified` is the import timestamp per schema; the cache value wins.
assert model_data["modified"] == 100.0
assert model_data["sha256"] == "abc123"
assert model_data["base_model"] == "Illustrious"
assert model_data["folder"] == "extra_loras"
assert model_data["civitai"] == {"id": 123}
@pytest.mark.asyncio
async def test_hydrate_model_data_disk_wins_when_sidecar_exists(tmp_path) -> None:
"""Existing sidecar stays authoritative; no cache key is resurrected."""
model_path = tmp_path / "MyModel.safetensors"
model_path.write_bytes(b"fake model data")
stat_result = model_path.stat()
sidecar = {
"file_path": str(model_path),
"file_name": "MyModel",
"model_name": "Disk Name",
"size": stat_result.st_size,
"modified": stat_result.st_mtime,
"sha256": "diskhash",
"base_model": "SDXL",
"preview_url": "",
}
(tmp_path / "MyModel.metadata.json").write_text(
json.dumps(sidecar), encoding="utf-8"
)
model_data = {
"file_path": str(model_path),
"folder": "extra_loras",
"model_name": "Cache Name",
"sha256": "cachehash",
"civitai": {"id": 999},
}
await MetadataManager.hydrate_model_data(model_data)
assert model_data["model_name"] == "Disk Name"
assert model_data["sha256"] == "diskhash"
# civitai is present only as the dataclass default {}; the cache's value
# ({"id": 999}) is not resurrected.
assert model_data["civitai"] == {}
assert model_data["folder"] == "extra_loras"
@pytest.mark.asyncio
async def test_hydrate_model_data_keeps_sha256_missing_for_caller_persist_fix(
tmp_path,
) -> None:
"""Sidecar exists but lacks sha256: hydrate leaves it missing so the
caller's self-heal persist block still has work to do."""
model_path = tmp_path / "MyModel.safetensors"
model_path.write_bytes(b"fake model data")
stat_result = model_path.stat()
sidecar = {
"file_path": str(model_path),
"file_name": "MyModel",
"model_name": "My Model",
"size": stat_result.st_size,
"modified": stat_result.st_mtime,
"base_model": "SDXL",
"preview_url": "",
# sha256 deliberately absent
}
(tmp_path / "MyModel.metadata.json").write_text(
json.dumps(sidecar), encoding="utf-8"
)
model_data = {
"file_path": str(model_path),
"sha256": "cachehash",
"model_name": "Cache Name",
}
await MetadataManager.hydrate_model_data(model_data)
assert "sha256" not in model_data
@pytest.mark.asyncio
async def test_load_metadata_payload_tolerates_missing_model_file(tmp_path) -> None:
"""A nonexistent model file must not crash payload loading."""
missing_path = tmp_path / "Ghost.safetensors"
payload = await MetadataManager.load_metadata_payload(str(missing_path))
assert payload["file_path"] == str(missing_path).replace(os.sep, "/")
assert "file_name" not in payload
assert "size" not in payload
assert "modified" not in payload
@pytest.mark.asyncio
async def test_self_healed_sidecar_is_parseable(tmp_path) -> None:
"""End-to-end repro: deleted sidecar + refresh recreates a parseable file."""
model_path = tmp_path / "MyModel.safetensors"
model_path.write_bytes(b"fake model data")
model_data = {
"file_path": str(model_path),
"folder": "extra_loras",
"file_name": "MyModel",
"model_name": "My Model",
"size": 10,
"modified": 100.0,
"sha256": "abc123",
"base_model": "Illustrious",
"preview_url": "",
"civitai": {"id": 123},
}
# Simulate the self-heal flow: hydrate from (missing) sidecar, then persist.
await MetadataManager.hydrate_model_data(model_data)
data_to_save = model_data.copy()
data_to_save.pop("folder", None)
await MetadataManager.save_metadata(str(model_path), data_to_save)
metadata, should_skip = await MetadataManager.load_metadata(
str(model_path), LoraMetadata
)
assert should_skip is False
assert metadata is not None
assert metadata.file_name == "MyModel"
assert metadata.model_name == "My Model"
assert metadata.sha256 == "abc123"
@pytest.mark.asyncio
async def test_provenance_fields_survive_a_load_save_round_trip(tmp_path) -> None:
"""Non-underscore extras must persist; underscore keys are ephemeral.
Regression test for `llm_confidence`: it was written as `_llm_confidence`,
which `BaseModelMetadata.from_dict()` drops (and `to_dict()` strips), so the
value was erased by the next metadata write and was invisible to
`read_metadata()`. The enrichment evaluation harness depends on it.
"""
model_path = tmp_path / "Model.safetensors"
model_path.write_bytes(b"fake model data")
metadata_path = tmp_path / "Model.metadata.json"
payload = {
"file_path": str(model_path),
"file_name": "Model",
"model_name": "Model",
"sha256": "deadbeef",
"base_model": "Krea 2",
"preview_url": "",
"metadata_source": "agent:enrich_hf_metadata",
"llm_enriched_at": "2026-01-01T00:00:00+00:00",
"llm_confidence": "medium",
"_llm_confidence": "medium",
}
assert await MetadataManager.save_metadata(str(model_path), payload) is True
# A read must surface the supported key...
loaded = await MetadataManager.load_metadata_payload(str(model_path))
assert loaded["llm_confidence"] == "medium"
assert loaded["metadata_source"] == "agent:enrich_hf_metadata"
# ...but the underscore alias is intentionally not persisted.
assert "_llm_confidence" not in loaded
# Re-saving what we read must not lose the confidence.
assert await MetadataManager.save_metadata(str(model_path), loaded) is True
saved = json.loads(metadata_path.read_text(encoding="utf-8"))
assert saved["llm_confidence"] == "medium"
assert saved["llm_enriched_at"] == "2026-01-01T00:00:00+00:00"