mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 11:11:26 -03:00
adeb40bfff
A model could have CivitAI metadata and a HuggingFace link at the same time, but only one of the two "View on ..." entries ever rendered, because both the model modal and the card globe asked the `from_civitai` provenance flag which source to show. `set_hf_url` wrote `false` and a CivitAI refresh wrote `true`, so whichever ran last erased the other: linking HF hid "View on CivitAI" even though the civitai payload was still in the sidecar, and (on the card) a later refresh pointed the single globe icon back at CivitAI, hiding the HF entry. Decide the links from the data itself instead: - `set_hf_url` no longer touches `from_civitai`; it records where the metadata came from, and HF provenance is already tracked by `hf_url`. - Add `hasCivitaiSource(civitai)` in the shared card/modal utils and gate the modal's CivitAI link, the card globe (title, enabled state, click target, new `data-has_civitai`) and the context-menu `civitai` action on actual CivitAI data (`modelId` / `model_id` / `id`). A dual-source model now shows both links, and a CivitAI-only model with no `hf_url` stays as before. - Agent HF enrichment (`PostProcessor.is_hf_model`) keyed off `not from_civitai`, which stopped being a synonym for "has an HF source" once both sources can coexist (and already broke after a CivitAI refresh flipped the flag back to true). Key it off `hf_url` directly; the post-processor tests move to that discriminator and gain a dual-source case. Regression tests: the set-hf-url handler preserves civitai + `from_civitai` and no longer forces the flag false, the modal renders both links (including with `from_civitai: false`), and the card globe targets/opens the right source and is disabled when neither is available. Backend: 2749 passed. Frontend: 1098 JS + 91 Vue tests passed.
157 lines
4.8 KiB
Python
157 lines
4.8 KiB
Python
"""Tests for the HuggingFace link handler (``set_hf_url``).
|
|
|
|
Regression coverage for issue #1094: linking a model to HuggingFace must not
|
|
clear its CivitAI provenance or metadata, so both "View on CivitAI" and
|
|
"View on Hugging Face" can coexist.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from py.routes.handlers import hf_handlers
|
|
from py.routes.handlers.hf_handlers import HfHandler
|
|
from py.utils.metadata_manager import MetadataManager
|
|
|
|
|
|
def _json_payload(response) -> dict[str, Any]:
|
|
assert response.text is not None
|
|
return json.loads(response.text)
|
|
|
|
|
|
class FakeRequest:
|
|
def __init__(self, *, json_data=None):
|
|
self._json_data = json_data or {}
|
|
|
|
async def json(self):
|
|
return self._json_data
|
|
|
|
|
|
def _sidecar_path(model_path) -> str:
|
|
return f"{os.path.splitext(str(model_path))[0]}.metadata.json"
|
|
|
|
|
|
@pytest.fixture
|
|
def hf_env(tmp_path, monkeypatch):
|
|
"""Point HF linking at *tmp_path* and stub the scanner cache write."""
|
|
monkeypatch.setattr(hf_handlers, "_find_matching_root", lambda _dir: str(tmp_path))
|
|
cache_write = AsyncMock()
|
|
monkeypatch.setattr(hf_handlers, "_add_to_scanner_cache", cache_write)
|
|
return {"root": tmp_path, "cache_write": cache_write}
|
|
|
|
|
|
async def _write_model(model_path, payload: dict[str, Any]) -> None:
|
|
model_path.write_bytes(b"x" * 32)
|
|
await MetadataManager.save_metadata(str(model_path), payload)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_hf_url_keeps_civitai_metadata_and_provenance(tmp_path, hf_env):
|
|
model_path = tmp_path / "civitai_model.safetensors"
|
|
await _write_model(
|
|
model_path,
|
|
{
|
|
"file_name": "civitai_model",
|
|
"model_name": "CivitAI Model",
|
|
"file_path": str(model_path),
|
|
"size": 32,
|
|
"modified": 1.0,
|
|
"sha256": "a" * 64,
|
|
"base_model": "SDXL 1.0",
|
|
"preview_url": "",
|
|
"from_civitai": True,
|
|
"civitai": {"id": 111, "modelId": 222, "name": "v1", "trainedWords": []},
|
|
},
|
|
)
|
|
|
|
response = await HfHandler().set_hf_url(
|
|
FakeRequest(
|
|
json_data={
|
|
"file_path": str(model_path),
|
|
"hf_url": "https://huggingface.co/user/repo",
|
|
}
|
|
)
|
|
)
|
|
|
|
assert response.status == 200
|
|
assert _json_payload(response)["success"] is True
|
|
|
|
saved = json.loads(open(_sidecar_path(model_path), encoding="utf-8").read())
|
|
assert saved["hf_url"] == "https://huggingface.co/user/repo"
|
|
# Linking HF must not erase the model's CivitAI provenance or data.
|
|
assert saved["from_civitai"] is True
|
|
assert saved["civitai"]["modelId"] == 222
|
|
assert saved["civitai"]["id"] == 111
|
|
|
|
hf_env["cache_write"].assert_awaited_once()
|
|
cached_metadata = hf_env["cache_write"].await_args.args[1]
|
|
assert cached_metadata["hf_url"] == "https://huggingface.co/user/repo"
|
|
assert cached_metadata["from_civitai"] is True
|
|
assert cached_metadata["civitai"]["modelId"] == 222
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_hf_url_does_not_force_from_civitai_false(tmp_path, hf_env):
|
|
"""A model without CivitAI data keeps its existing provenance flag."""
|
|
model_path = tmp_path / "hf_only.safetensors"
|
|
await _write_model(
|
|
model_path,
|
|
{
|
|
"file_name": "hf_only",
|
|
"model_name": "HF Only",
|
|
"file_path": str(model_path),
|
|
"size": 32,
|
|
"modified": 1.0,
|
|
"sha256": "b" * 64,
|
|
"base_model": "Unknown",
|
|
"preview_url": "",
|
|
"from_civitai": True,
|
|
},
|
|
)
|
|
|
|
response = await HfHandler().set_hf_url(
|
|
FakeRequest(
|
|
json_data={
|
|
"file_path": str(model_path),
|
|
"hf_url": "https://huggingface.co/user/repo",
|
|
}
|
|
)
|
|
)
|
|
|
|
assert response.status == 200
|
|
saved = json.loads(open(_sidecar_path(model_path), encoding="utf-8").read())
|
|
assert saved["hf_url"] == "https://huggingface.co/user/repo"
|
|
assert saved["from_civitai"] is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_hf_url_rejects_non_repo_url(tmp_path, hf_env):
|
|
model_path = tmp_path / "model.safetensors"
|
|
await _write_model(
|
|
model_path,
|
|
{
|
|
"file_name": "model",
|
|
"model_name": "model",
|
|
"file_path": str(model_path),
|
|
"size": 32,
|
|
"modified": 1.0,
|
|
"sha256": "c" * 64,
|
|
"base_model": "Unknown",
|
|
"preview_url": "",
|
|
},
|
|
)
|
|
|
|
response = await HfHandler().set_hf_url(
|
|
FakeRequest(json_data={"file_path": str(model_path), "hf_url": "https://example.com/x"})
|
|
)
|
|
|
|
assert response.status == 400
|
|
payload = _json_payload(response)
|
|
assert payload["success"] is False
|
|
hf_env["cache_write"].assert_not_awaited()
|