mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
fix(links): let CivitAI and HuggingFace links coexist (#1094)
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.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user