From 958a7f1f19db089214425c0813a77885188a667c Mon Sep 17 00:00:00 2001 From: Simon Pinfold Date: Mon, 6 Jul 2026 21:13:20 +1200 Subject: [PATCH] feat(assets): replace response file_path with persisted loader_path The old file_path response field was a namespaced storage locator (models/checkpoints/foo.safetensors): not an absolute path, not unique identity, and not the value a loader consumes. Nothing needs that shape on the wire (hash/ID-based locating is the long-term direction), so it is dropped rather than renamed; the storage-root matching stays internal, powering display_name. What loaders DO need is the in-root loader path (category dropped: models/checkpoints/foo/bar.safetensors -> foo/bar.safetensors). Serve it as a first-class loader_path field, persisted on asset_references (migration 0006) and written by every ingest pipeline at insert, so responses read the column verbatim. Like the model_type tags, loader_path is a seed-time derivative of the model folder registry, maintained by the same scan lifecycle (new files seed fresh values, pruning retires rows whose bucket disappeared). Rows predating the column serve a null loader_path; databases from before this stack already need recreating for the base branch's tag changes. loader_path resolves every registered base including extra_model_paths entries; display_name only the canonical storage roots. A file can therefore be loadable with no display name (extra-path models) or the reverse (unregistered files under the models root), and loader_path is null exactly when no loader can resolve the file. --- alembic_db/versions/0006_add_loader_path.py | 30 +++++++ app/assets/api/routes.py | 11 +-- app/assets/api/schemas_out.py | 8 +- app/assets/database/models.py | 2 + .../database/queries/asset_reference.py | 2 + app/assets/scanner.py | 6 +- app/assets/services/asset_management.py | 4 +- app/assets/services/bulk_ingest.py | 3 + app/assets/services/ingest.py | 7 +- app/assets/services/path_utils.py | 30 ++++--- app/assets/services/schemas.py | 2 + openapi.yaml | 6 +- .../assets_test/services/test_bulk_ingest.py | 2 + .../assets_test/services/test_path_utils.py | 82 +++++++++++++++---- tests-unit/assets_test/test_uploads.py | 33 ++++---- 15 files changed, 162 insertions(+), 66 deletions(-) create mode 100644 alembic_db/versions/0006_add_loader_path.py diff --git a/alembic_db/versions/0006_add_loader_path.py b/alembic_db/versions/0006_add_loader_path.py new file mode 100644 index 000000000..afa65312d --- /dev/null +++ b/alembic_db/versions/0006_add_loader_path.py @@ -0,0 +1,30 @@ +""" +Add loader_path column to asset_references. + +Stores the in-root loader path (path relative to the storage root with the +top-level model category dropped) derived from file_path at scan/ingest time, +so the assets API can return it without re-resolving against every registered +model-folder base on every request. + +Revision ID: 0006_add_loader_path +Revises: 0005_allow_case_sensitive_tags +Create Date: 2026-07-02 +""" + +from alembic import op +import sqlalchemy as sa + +revision = "0006_add_loader_path" +down_revision = "0005_allow_case_sensitive_tags" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + with op.batch_alter_table("asset_references") as batch_op: + batch_op.add_column(sa.Column("loader_path", sa.Text(), nullable=True)) + + +def downgrade() -> None: + with op.batch_alter_table("asset_references") as batch_op: + batch_op.drop_column("loader_path") diff --git a/app/assets/api/routes.py b/app/assets/api/routes.py index eca2f887f..41d43e665 100644 --- a/app/assets/api/routes.py +++ b/app/assets/api/routes.py @@ -39,7 +39,7 @@ from app.assets.services import ( upload_from_temp_path, ) from app.assets.services.cursor import InvalidCursorError -from app.assets.services.path_utils import compute_asset_response_paths +from app.assets.services.path_utils import compute_display_name from app.assets.services.tagging import list_tag_histogram ROUTES = web.RouteTableDef() @@ -162,16 +162,17 @@ def _build_asset_response(result: schemas.AssetDetailResult | schemas.UploadResu else: preview_url = _build_preview_url_from_view(result.tags, result.ref.user_metadata) if result.ref.file_path: - paths = compute_asset_response_paths(result.ref.file_path) - file_path, display_name = paths if paths else (None, None) + display_name = compute_display_name(result.ref.file_path) + # In-root loader path (model category dropped): what model loaders consume. + loader_path = result.ref.loader_path else: - file_path, display_name = None, None + display_name, loader_path = None, None asset_content_hash = result.asset.hash if result.asset else None return schemas_out.Asset( id=result.ref.id, name=result.ref.name, hash=asset_content_hash, - file_path=file_path, + loader_path=loader_path, display_name=display_name, asset_hash=asset_content_hash, size=int(result.asset.size_bytes) if result.asset else None, diff --git a/app/assets/api/schemas_out.py b/app/assets/api/schemas_out.py index d4d2c699d..da8251499 100644 --- a/app/assets/api/schemas_out.py +++ b/app/assets/api/schemas_out.py @@ -12,16 +12,16 @@ class Asset(BaseModel): name: str = Field( ..., deprecated=True, - description="Reference label, often caller-provided or derived from the filename. Deprecated for storage path/display semantics; use `file_path` and `display_name` when present.", + description="Reference label, often caller-provided or derived from the filename. Deprecated for storage path/display semantics; use `loader_path` and `display_name` when present.", ) hash: str | None = None - file_path: str | None = Field( + loader_path: str | None = Field( default=None, - description="Runtime storage locator for filesystem-backed assets, using Comfy storage namespaces such as `input/`, `output/`, `temp/`, or `models/`. Not an absolute filesystem path, unique identity, or model loader path.", + description="The value a loader consumes to load this asset. `None` when no loader can resolve the file.", ) display_name: str | None = Field( default=None, - description="Human-facing label derived from `file_path`, usually the path below the top-level storage namespace. Not unique.", + description="Human-facing label for the asset. Not unique.", ) asset_hash: str | None = None size: int | None = None diff --git a/app/assets/database/models.py b/app/assets/database/models.py index 9b61d309a..329cd483d 100644 --- a/app/assets/database/models.py +++ b/app/assets/database/models.py @@ -76,6 +76,8 @@ class AssetReference(Base): # Cache state fields (from former AssetCacheState) file_path: Mapped[str | None] = mapped_column(Text, nullable=True) + # In-root loader path derived from file_path at scan/ingest time. + loader_path: Mapped[str | None] = mapped_column(Text, nullable=True) mtime_ns: Mapped[int | None] = mapped_column(BigInteger, nullable=True) needs_verify: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_missing: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) diff --git a/app/assets/database/queries/asset_reference.py b/app/assets/database/queries/asset_reference.py index 792411800..26c75ad93 100644 --- a/app/assets/database/queries/asset_reference.py +++ b/app/assets/database/queries/asset_reference.py @@ -650,6 +650,7 @@ def upsert_reference( name: str, mtime_ns: int, owner_id: str = "", + loader_path: str | None = None, ) -> tuple[bool, bool]: """Upsert a reference by file_path. Returns (created, updated). @@ -659,6 +660,7 @@ def upsert_reference( vals = { "asset_id": asset_id, "file_path": file_path, + "loader_path": loader_path, "name": name, "owner_id": owner_id, "mtime_ns": int(mtime_ns), diff --git a/app/assets/scanner.py b/app/assets/scanner.py index b13276b49..42c4c1e9d 100644 --- a/app/assets/scanner.py +++ b/app/assets/scanner.py @@ -36,7 +36,7 @@ from app.assets.services.hashing import HashCheckpoint, compute_blake3_hash from app.assets.services.image_dimensions import extract_image_dimensions from app.assets.services.metadata_extract import extract_file_metadata from app.assets.services.path_utils import ( - compute_relative_filename, + compute_loader_path, get_comfy_models_folders, get_name_and_tags_from_asset_path, ) @@ -308,7 +308,7 @@ def build_asset_specs( if not stat_p.st_size: continue name, tags = get_name_and_tags_from_asset_path(abs_p) - rel_fname = compute_relative_filename(abs_p) + rel_fname = compute_loader_path(abs_p) # Extract metadata (tier 1: filesystem, tier 2: safetensors header) metadata = None @@ -430,7 +430,7 @@ def enrich_asset( return new_level initial_mtime_ns = get_mtime_ns(stat_p) - rel_fname = compute_relative_filename(file_path) + rel_fname = compute_loader_path(file_path) mime_type: str | None = None metadata = None diff --git a/app/assets/services/asset_management.py b/app/assets/services/asset_management.py index d4e4fc61c..a4c8b5a75 100644 --- a/app/assets/services/asset_management.py +++ b/app/assets/services/asset_management.py @@ -38,7 +38,7 @@ from app.assets.database.queries import ( update_reference_updated_at, ) from app.assets.helpers import select_best_live_path -from app.assets.services.path_utils import compute_relative_filename +from app.assets.services.path_utils import compute_loader_path from app.assets.services.schemas import ( AssetData, AssetDetailResult, @@ -91,7 +91,7 @@ def update_asset_metadata( update_reference_name(session, reference_id=reference_id, name=name) touched = True - computed_filename = compute_relative_filename(ref.file_path) if ref.file_path else None + computed_filename = compute_loader_path(ref.file_path) if ref.file_path else None new_meta: dict | None = None if user_metadata is not None: diff --git a/app/assets/services/bulk_ingest.py b/app/assets/services/bulk_ingest.py index 4036530e6..c98658bf1 100644 --- a/app/assets/services/bulk_ingest.py +++ b/app/assets/services/bulk_ingest.py @@ -56,6 +56,7 @@ class ReferenceRow(TypedDict): id: str asset_id: str file_path: str + loader_path: str | None mtime_ns: int owner_id: str name: str @@ -172,6 +173,8 @@ def batch_insert_seed_assets( "id": reference_id, "asset_id": asset_id, "file_path": absolute_path, + # spec["fname"] is compute_loader_path(abs_path) from build_asset_specs. + "loader_path": spec["fname"], "mtime_ns": spec["mtime_ns"], "owner_id": owner_id, "name": spec["info_name"], diff --git a/app/assets/services/ingest.py b/app/assets/services/ingest.py index 61359464c..6f6508207 100644 --- a/app/assets/services/ingest.py +++ b/app/assets/services/ingest.py @@ -33,7 +33,7 @@ from app.assets.services.bulk_ingest import batch_insert_seed_assets from app.assets.services.file_utils import get_size_and_mtime_ns from app.assets.services.image_dimensions import extract_image_dimensions from app.assets.services.path_utils import ( - compute_relative_filename, + compute_loader_path, get_name_and_tags_from_asset_path, get_path_derived_tags_from_path, resolve_destination_from_tags, @@ -92,6 +92,7 @@ def _ingest_file_from_path( name=info_name or os.path.basename(locator), mtime_ns=mtime_ns, owner_id=owner_id, + loader_path=compute_loader_path(locator), ) # Get the reference we just created/updated @@ -304,7 +305,7 @@ def _register_existing_asset( return result new_meta = dict(user_metadata) - computed_filename = compute_relative_filename(ref.file_path) if ref.file_path else None + computed_filename = compute_loader_path(ref.file_path) if ref.file_path else None if computed_filename: new_meta["filename"] = computed_filename @@ -351,7 +352,7 @@ def _update_metadata_with_filename( current_metadata: dict | None, user_metadata: dict[str, Any], ) -> None: - computed_filename = compute_relative_filename(file_path) if file_path else None + computed_filename = compute_loader_path(file_path) if file_path else None current_meta = current_metadata or {} new_meta = dict(current_meta) diff --git a/app/assets/services/path_utils.py b/app/assets/services/path_utils.py index 5e6c454b4..c13b86a45 100644 --- a/app/assets/services/path_utils.py +++ b/app/assets/services/path_utils.py @@ -84,11 +84,14 @@ def _is_relative_to(child: str, parent: str) -> bool: def compute_asset_response_paths(file_path: str) -> tuple[str, str | None] | None: - """Return public (file_path, display_name) response fields for a file path. + """Return (logical_path, display_name) for a file path. - These fields are storage locators, not model-loader namespaces. Registered - model-folder membership is represented by backend tags such as - ``model_type:``; response paths only use known storage roots. + ``logical_path`` is the internal namespaced storage locator (e.g. + ``models/checkpoints/foo/bar.safetensors``); ``display_name`` is the + human-facing label below that namespace, served on Asset responses. These + are storage locators, not model-loader namespaces. Registered model-folder + membership is represented by backend tags such as + ``model_type:``; these paths only use known storage roots. """ fp_abs = os.path.abspath(file_path) candidates: list[tuple[int, int, str, str]] = [] @@ -122,23 +125,26 @@ def compute_display_name(file_path: str) -> str | None: return result[1] if result else None -def compute_file_path(file_path: str) -> str | None: - """Return the asset's logical storage `file_path`, or None for unknown paths.""" +def compute_logical_path(file_path: str) -> str | None: + """Return the internal namespaced storage locator, or None for unknown paths.""" result = compute_asset_response_paths(file_path) return result[0] if result else None -def compute_relative_filename(file_path: str) -> str | None: +def compute_loader_path(file_path: str) -> str | None: """ - Return the model's path relative to the last well-known folder (the model category), - using forward slashes, eg: + Return the asset's in-root loader path: the path relative to the last + well-known folder (the model category), using forward slashes, eg: /.../models/checkpoints/flux/123/flux.safetensors -> "flux/123/flux.safetensors" /.../models/text_encoders/clip_g.safetensors -> "clip_g.safetensors" - This is legacy metadata/view filename logic, not the public Asset response - `display_name`. Response fields should use compute_asset_response_paths(). + This is the value model loaders consume (the model category is dropped). It + is persisted as ``AssetReference.loader_path`` and served as the public + Asset response `loader_path` field. The human-facing `display_name` comes + from compute_asset_response_paths(). - For non-model paths, returns None. + For input/output/temp paths the full path relative to that root is returned. + For paths outside any known root, returns None. """ try: root_category, rel_path = get_asset_category_and_relative_path(file_path) diff --git a/app/assets/services/schemas.py b/app/assets/services/schemas.py index 4d2af8a02..0fda6871d 100644 --- a/app/assets/services/schemas.py +++ b/app/assets/services/schemas.py @@ -25,6 +25,7 @@ class ReferenceData: preview_id: str | None created_at: datetime updated_at: datetime + loader_path: str | None = None system_metadata: dict[str, Any] | None = None job_id: str | None = None last_access_time: datetime | None = None @@ -93,6 +94,7 @@ def extract_reference_data(ref: AssetReference) -> ReferenceData: id=ref.id, name=ref.name, file_path=ref.file_path, + loader_path=ref.loader_path, user_metadata=ref.user_metadata, preview_id=ref.preview_id, system_metadata=ref.system_metadata, diff --git a/openapi.yaml b/openapi.yaml index 1f1378c19..0cf177815 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -11,12 +11,12 @@ components: description: Blake3 hash of the asset content. pattern: ^blake3:[a-f0-9]{64}$ type: string - file_path: - description: Runtime storage locator for filesystem-backed assets, using Comfy storage namespaces such as `input/`, `output/`, `temp/`, or `models/`. Not an absolute filesystem path, unique identity, or model loader path. + loader_path: + description: The value a loader consumes to load this asset. Null when no loader can resolve the file. nullable: true type: string display_name: - description: Human-facing label derived from `file_path`, usually the path below the top-level storage namespace. Not unique. + description: Human-facing label for the asset. Not unique. nullable: true type: string id: diff --git a/tests-unit/assets_test/services/test_bulk_ingest.py b/tests-unit/assets_test/services/test_bulk_ingest.py index 07cd5510d..c174fce44 100644 --- a/tests-unit/assets_test/services/test_bulk_ingest.py +++ b/tests-unit/assets_test/services/test_bulk_ingest.py @@ -191,6 +191,8 @@ class TestBatchInsertSeedAssets: refs = session.query(AssetReference).all() assert len(refs) == 1 assert refs[0].file_path == absolute_path + # loader_path is persisted from the spec's fname (compute_loader_path). + assert refs[0].loader_path == "same-file.safetensors" assert set(get_reference_tags(session, reference_id=refs[0].id)) == { "models", "model_type:checkpoints", diff --git a/tests-unit/assets_test/services/test_path_utils.py b/tests-unit/assets_test/services/test_path_utils.py index 3c4db067c..a14487193 100644 --- a/tests-unit/assets_test/services/test_path_utils.py +++ b/tests-unit/assets_test/services/test_path_utils.py @@ -8,7 +8,8 @@ import pytest from app.assets.services.path_utils import ( compute_display_name, - compute_file_path, + compute_loader_path, + compute_logical_path, get_asset_category_and_relative_path, get_known_input_subfolder_tags_from_path, get_known_subfolder_tags, @@ -258,7 +259,7 @@ class TestResponseStoragePaths: f = sub / "image.png" f.touch() - assert compute_file_path(str(f)) == "input/some/folder/image.png" + assert compute_logical_path(str(f)) == "input/some/folder/image.png" assert compute_display_name(str(f)) == "some/folder/image.png" def test_output_file_path_and_display_name_include_subfolder(self, fake_dirs): @@ -267,18 +268,18 @@ class TestResponseStoragePaths: f = sub / "ComfyUI_00001_.png" f.touch() - assert compute_file_path(str(f)) == "output/renders/ComfyUI_00001_.png" + assert compute_logical_path(str(f)) == "output/renders/ComfyUI_00001_.png" assert compute_display_name(str(f)) == "renders/ComfyUI_00001_.png" def test_temp_file_path_and_display_name(self, fake_dirs): f = fake_dirs["temp"] / "preview.png" f.touch() - assert compute_file_path(str(f)) == "temp/preview.png" + assert compute_logical_path(str(f)) == "temp/preview.png" assert compute_display_name(str(f)) == "preview.png" def test_exact_storage_root_has_no_display_name(self, fake_dirs): - assert compute_file_path(str(fake_dirs["input"])) == "input" + assert compute_logical_path(str(fake_dirs["input"])) == "input" assert compute_display_name(str(fake_dirs["input"])) is None def test_longest_matching_builtin_root_wins(self, fake_dirs, tmp_path: Path): @@ -293,7 +294,7 @@ class TestResponseStoragePaths: mock_fp.get_temp_directory.return_value = str(tmp_path / "temp") mock_fp.models_dir = str(fake_dirs["models_root"]) - assert compute_file_path(str(f)) == "output/image.png" + assert compute_logical_path(str(f)) == "output/image.png" assert compute_display_name(str(f)) == "image.png" def test_model_file_path_is_relative_to_physical_models_root(self, fake_dirs): @@ -302,7 +303,7 @@ class TestResponseStoragePaths: f = sub / "model.safetensors" f.touch() - assert compute_file_path(str(f)) == "models/checkpoints/flux/model.safetensors" + assert compute_logical_path(str(f)) == "models/checkpoints/flux/model.safetensors" assert compute_display_name(str(f)) == "checkpoints/flux/model.safetensors" name, tags = get_name_and_tags_from_asset_path(str(f)) @@ -330,7 +331,7 @@ class TestResponseStoragePaths: (folder_name, [str(default_model_dir), str(output_model_dir)], {".safetensors"}) ], ): - assert compute_file_path(str(f)) == f"output/{folder_name}/saved.safetensors" + assert compute_logical_path(str(f)) == f"output/{folder_name}/saved.safetensors" assert compute_display_name(str(f)) == f"{folder_name}/saved.safetensors" name, tags = get_name_and_tags_from_asset_path(str(f)) @@ -353,7 +354,7 @@ class TestResponseStoragePaths: return_value=[(folder_name, [str(output_model_dir)], {".safetensors"})], ): assert ( - compute_file_path(str(f)) + compute_logical_path(str(f)) == "output/loras/experiments/my_lora.safetensors" ) assert compute_display_name(str(f)) == "loras/experiments/my_lora.safetensors" @@ -376,7 +377,7 @@ class TestResponseStoragePaths: "app.assets.services.path_utils.get_comfy_models_folders", return_value=[("checkpoints", [str(external_checkpoints_dir)], {".safetensors"})], ): - assert compute_file_path(str(f)) is None + assert compute_logical_path(str(f)) is None assert compute_display_name(str(f)) is None name, tags = get_name_and_tags_from_asset_path(str(f)) @@ -400,8 +401,8 @@ class TestResponseStoragePaths: "app.assets.services.path_utils.get_comfy_models_folders", return_value=[("checkpoints", [str(foo_dir), str(bar_dir)], {".safetensors"})], ): - assert compute_file_path(str(foo_file)) is None - assert compute_file_path(str(bar_file)) is None + assert compute_logical_path(str(foo_file)) is None + assert compute_logical_path(str(bar_file)) is None assert compute_display_name(str(foo_file)) is None assert compute_display_name(str(bar_file)) is None @@ -415,7 +416,7 @@ class TestResponseStoragePaths: "app.assets.services.path_utils.get_comfy_models_folders", return_value=[("text_encoders", [str(output_clip_dir)], {".safetensors"})], ): - assert compute_file_path(str(f)) == "output/clip/clip_l.safetensors" + assert compute_logical_path(str(f)) == "output/clip/clip_l.safetensors" assert compute_display_name(str(f)) == "clip/clip_l.safetensors" name, tags = get_name_and_tags_from_asset_path(str(f)) @@ -439,7 +440,7 @@ class TestResponseStoragePaths: ("diffusion_models", [str(unet_dir), str(diffusion_models_dir)], {".safetensors"}) ], ): - assert compute_file_path(str(f)) == "models/unet/wan.safetensors" + assert compute_logical_path(str(f)) == "models/unet/wan.safetensors" assert compute_display_name(str(f)) == "unet/wan.safetensors" name, tags = get_name_and_tags_from_asset_path(str(f)) @@ -453,7 +454,7 @@ class TestResponseStoragePaths: f.parent.mkdir() f.touch() - assert compute_file_path(str(f)) == "models/not_registered/orphan.bin" + assert compute_logical_path(str(f)) == "models/not_registered/orphan.bin" assert compute_display_name(str(f)) == "not_registered/orphan.bin" def test_output_checkpoint_folder_without_registration_has_only_output_tag(self, fake_dirs): @@ -465,7 +466,7 @@ class TestResponseStoragePaths: "app.assets.services.path_utils.get_comfy_models_folders", return_value=[], ): - assert compute_file_path(str(f)) == "output/checkpoints/saved.safetensors" + assert compute_logical_path(str(f)) == "output/checkpoints/saved.safetensors" assert compute_display_name(str(f)) == "checkpoints/saved.safetensors" name, tags = get_name_and_tags_from_asset_path(str(f)) @@ -475,10 +476,57 @@ class TestResponseStoragePaths: assert not any(tag.startswith("model_type:") for tag in tags) def test_unknown_path_returns_none(self): - assert compute_file_path("/some/random/path.png") is None + assert compute_logical_path("/some/random/path.png") is None assert compute_display_name("/some/random/path.png") is None +class TestLoaderPath: + """In-root loader path: relative to the storage root, model category dropped.""" + + def test_model_loader_path_drops_category(self, fake_dirs): + sub = fake_dirs["models"] / "flux" + sub.mkdir() + f = sub / "model.safetensors" + f.touch() + + # logical_path keeps the category, file_path (loader) drops it + assert compute_logical_path(str(f)) == "models/checkpoints/flux/model.safetensors" + assert compute_loader_path(str(f)) == "flux/model.safetensors" + + def test_model_loader_path_flat_file(self, fake_dirs): + f = fake_dirs["models"] / "model.safetensors" + f.touch() + + assert compute_loader_path(str(f)) == "model.safetensors" + + def test_input_loader_path_keeps_subfolders(self, fake_dirs): + sub = fake_dirs["input"] / "some" / "folder" + sub.mkdir(parents=True) + f = sub / "image.png" + f.touch() + + assert compute_loader_path(str(f)) == "some/folder/image.png" + + def test_temp_loader_path(self, fake_dirs): + f = fake_dirs["temp"] / "preview.png" + f.touch() + + assert compute_loader_path(str(f)) == "preview.png" + + def test_unregistered_file_under_models_root_has_no_loader_path(self, fake_dirs): + # Under models_root but not within any registered category base. + f = fake_dirs["models_root"] / "not_registered" / "orphan.bin" + f.parent.mkdir() + f.touch() + + # It still has a namespaced logical_path, but no loader path. + assert compute_logical_path(str(f)) == "models/not_registered/orphan.bin" + assert compute_loader_path(str(f)) is None + + def test_unknown_path_returns_none(self): + assert compute_loader_path("/some/random/path.png") is None + + class TestResolveDestinationFromTags: def test_extra_tags_are_not_path_components(self, fake_dirs): base_dir, subdirs = resolve_destination_from_tags(["input", "unit-tests", "foo"]) diff --git a/tests-unit/assets_test/test_uploads.py b/tests-unit/assets_test/test_uploads.py index 09262b25e..7be7b0935 100644 --- a/tests-unit/assets_test/test_uploads.py +++ b/tests-unit/assets_test/test_uploads.py @@ -55,7 +55,7 @@ def test_upload_ok_duplicate_reference(http: requests.Session, api_base: str, ma assert a2["asset_hash"] == a1["asset_hash"] assert a2["hash"] == a1["hash"] assert a2["id"] != a1["id"] # new reference with same content - assert a2.get("file_path") is None + assert a2.get("loader_path") is None assert a2.get("display_name") is None # Third upload with the same data but different name also creates new AssetReference @@ -67,7 +67,7 @@ def test_upload_ok_duplicate_reference(http: requests.Session, api_base: str, ma assert a3["asset_hash"] == a1["asset_hash"] assert a3["id"] != a1["id"] assert a3["id"] != a2["id"] - assert a3.get("file_path") is None + assert a3.get("loader_path") is None assert a3.get("display_name") is None @@ -102,17 +102,17 @@ def test_upload_fastpath_from_existing_hash_no_file(http: requests.Session, api_ assert "checkpoints" in b2["tags"] assert "uploaded" not in b2["tags"] assert not any(tag.startswith("model_type:") for tag in b2["tags"]) - assert b2.get("file_path") is None + assert b2.get("loader_path") is None assert b2.get("display_name") is None rg = http.get(f"{api_base}/api/assets/{b2['id']}", timeout=120) detail = rg.json() assert rg.status_code == 200, detail - assert detail.get("file_path") is None + assert detail.get("loader_path") is None assert detail.get("display_name") is None -def test_create_from_hash_with_model_tags_does_not_synthesize_file_path( +def test_create_from_hash_with_model_tags_does_not_synthesize_loader_path( http: requests.Session, api_base: str ): seed_name = "from_hash_seed.safetensors" @@ -137,13 +137,13 @@ def test_create_from_hash_with_model_tags_does_not_synthesize_file_path( assert created_r.status_code == 201, created assert created["created_new"] is False assert created["asset_hash"] == seed["asset_hash"] - assert created.get("file_path") is None + assert created.get("loader_path") is None assert created.get("display_name") is None detail_r = http.get(f"{api_base}/api/assets/{created['id']}", timeout=120) detail = detail_r.json() assert detail_r.status_code == 200, detail - assert detail.get("file_path") is None + assert detail.get("loader_path") is None assert detail.get("display_name") is None @@ -204,7 +204,7 @@ def test_duplicate_byte_upload_is_reference_only_and_does_not_need_destination( assert "not-a-destination" in duplicate["tags"] assert "uploaded" not in duplicate["tags"] assert "input" not in duplicate["tags"] - assert duplicate.get("file_path") is None + assert duplicate.get("loader_path") is None assert duplicate.get("display_name") is None @@ -233,23 +233,20 @@ def test_upload_multiple_tags_fields_are_merged(http: requests.Session, api_base ( "tags", "extension", - "expected_prefix", "expected_display_prefix", ), [ - (["input", "unit-tests"], ".png", "input", ""), + (["input", "unit-tests"], ".png", ""), ( ["models", "model_type:checkpoints", "unit-tests"], ".safetensors", - "models/checkpoints", "checkpoints/", ), ], ) -def test_upload_response_includes_file_path_and_display_name( +def test_upload_response_includes_loader_path_and_display_name( tags: list[str], extension: str, - expected_prefix: str, expected_display_prefix: str, http: requests.Session, api_base: str, @@ -270,16 +267,18 @@ def test_upload_response_includes_file_path_and_display_name( assert created_r.status_code in (200, 201), created stored_filename = get_asset_filename(created["asset_hash"], extension) expected_suffix = stored_filename - expected_file_path = f"{expected_prefix}/{expected_suffix}" expected_display_name = f"{expected_display_prefix}{expected_suffix}" + # In-root loader path: model category dropped, no subfolders here -> just the filename. + expected_loader_path = expected_suffix - assert created["file_path"] == expected_file_path + assert created["loader_path"] == expected_loader_path assert created["display_name"] == expected_display_name + assert "logical_path" not in created detail_r = http.get(f"{api_base}/api/assets/{created['id']}", timeout=120) detail = detail_r.json() assert detail_r.status_code == 200, detail - assert detail["file_path"] == expected_file_path + assert detail["loader_path"] == expected_loader_path assert detail["display_name"] == expected_display_name list_r = http.get( @@ -290,7 +289,7 @@ def test_upload_response_includes_file_path_and_display_name( listed = list_r.json() assert list_r.status_code == 200, listed match = next(a for a in listed["assets"] if a["id"] == created["id"]) - assert match["file_path"] == expected_file_path + assert match["loader_path"] == expected_loader_path assert match["display_name"] == expected_display_name