Files
ComfyUI/tests-unit/assets_test/services/test_asset_response_loader_path.py
Simon Pinfoldandguill 3aba3daef3 Derive asset preview URLs from the file path (#15509)
preview_url was assembled from a /api/view link whose type was chosen by
matching the asset's tags against "input" then "output". Anything written
anywhere else - temp above all, where preview nodes put their images - fell
off the end of that chain and came back with no preview at all. Tags are
user-editable, so removing one also silently destroyed the URL.

Derive the URL from where the file actually sits instead. That covers every
root /api/view serves, temp included, and no longer depends on tags or on a
filename in user_metadata. A file outside those roots, or content no client
can render from its own bytes, gets no preview URL rather than one that
cannot work.

Nominated previews are resolved a page at a time rather than per row, so a
list costs one extra query however long it is. A preview that is soft-deleted
or not visible to the caller drops out of that lookup and is no longer
advertised.

Co-authored-by: guill <jacob.e.segal@gmail.com>
2026-08-18 21:00:28 -07:00

84 lines
2.8 KiB
Python

"""Tests for how _build_asset_response derives the response `loader_path`.
Guards the persist-and-read contract: the response reads the stored
`loader_path` verbatim, with no read-time recomputation. Like tags, the
value is a seed-time derivative healed by the scan lifecycle.
"""
from datetime import datetime
from pathlib import Path
from unittest.mock import patch
from app.assets.api.routes import _build_asset_response
from app.assets.services.schemas import AssetDetailResult, ReferenceData
_TS = datetime(2024, 1, 1, 0, 0, 0)
def _make_result(
*, file_path: str | None, loader_path: str | None
) -> AssetDetailResult:
ref = ReferenceData(
id="ref-1",
name="model.safetensors",
file_path=file_path,
loader_path=loader_path,
user_metadata=None,
preview_id=None,
created_at=_TS,
updated_at=_TS,
last_access_time=_TS,
)
return AssetDetailResult(ref=ref, asset=None, tags=[])
def test_uses_persisted_loader_path_without_recomputing():
"""A stored loader_path is returned verbatim, not re-derived from file_path.
The sentinel value could never be produced by compute_loader_path for this
file_path, so seeing it in the response proves the stored column is read.
"""
result = _make_result(
file_path="/unmatched/root/model.safetensors",
loader_path="SENTINEL/stored.safetensors",
)
resp = _build_asset_response(result, {})
assert resp.loader_path == "SENTINEL/stored.safetensors"
def test_null_stored_loader_path_is_served_as_null(tmp_path: Path):
"""No read-time recomputation: a NULL column is served as null even when
the path would resolve."""
models = tmp_path / "models"
ckpt = models / "checkpoints"
ckpt.mkdir(parents=True)
f = ckpt / "bar.safetensors"
f.touch()
with patch("app.assets.services.path_utils.folder_paths") as mock_fp, patch(
"app.assets.services.path_utils.get_comfy_models_folders",
return_value=[("checkpoints", [str(ckpt)], {".safetensors"})],
):
mock_fp.get_input_directory.return_value = str(tmp_path / "in")
mock_fp.get_output_directory.return_value = str(tmp_path / "out")
mock_fp.get_temp_directory.return_value = str(tmp_path / "tmp")
mock_fp.models_dir = str(models)
result = _make_result(file_path=str(f), loader_path=None)
resp = _build_asset_response(result, {})
assert resp.loader_path is None
assert resp.display_name == "checkpoints/bar.safetensors"
def test_all_path_fields_null_without_file_path():
"""API-created / hash-only references (no file_path) expose no paths."""
result = _make_result(file_path=None, loader_path=None)
resp = _build_asset_response(result, {})
assert resp.loader_path is None
assert resp.display_name is None