diff --git a/app/assets/api/routes.py b/app/assets/api/routes.py index 85ba7bd37..3b57f172a 100644 --- a/app/assets/api/routes.py +++ b/app/assets/api/routes.py @@ -306,9 +306,13 @@ def _build_asset_response( else: preview_url = None if result.ref.file_path: + paths = compute_asset_response_paths(result.ref.file_path) + logical_path = paths[0] if paths else None + display_name = paths[1] if paths else None # In-root loader path (model category dropped): what model loaders consume. loader_path = result.ref.loader_path else: + logical_path, display_name = None, None loader_path = None asset_content_hash = result.asset.hash if result.asset else None return schemas_out.Asset( @@ -316,6 +320,8 @@ def _build_asset_response( name=result.ref.name, hash=asset_content_hash, loader_path=loader_path, + display_name=display_name, + file_path=logical_path, size=int(result.asset.size_bytes) if result.asset else None, mime_type=result.asset.mime_type if result.asset else None, tags=result.tags, @@ -337,6 +343,9 @@ def _build_record_response( preview_paths: dict[str, str], ) -> schemas_out.Asset: content = record.content + paths = compute_asset_response_paths(content.path) + logical_path = paths[0] if paths else None + display_name = paths[1] if paths else None if record.preview_id: preview_url = _build_view_url(preview_paths.get(record.preview_id)) else: @@ -353,6 +362,8 @@ def _build_record_response( name=record.name, hash=content.hash, loader_path=record.loader_path, + display_name=display_name, + file_path=logical_path, size=content.size_bytes, mime_type=record.mime_type, tags=tags, @@ -452,12 +463,8 @@ async def list_assets_route(request: web.Request) -> web.Response: GET request to list assets. """ if "metadata_filter" in request.query: - return web.json_response( - { - "error": "UNSUPPORTED_PARAM", - "message": "metadata_filter is no longer supported", - }, - status=400, + return _build_error_response( + 400, "UNSUPPORTED_PARAM", "metadata_filter is no longer supported" ) query_dict = get_query_dict(request) @@ -977,12 +984,8 @@ async def delete_asset_tags(request: web.Request) -> web.Response: async def get_tags_refine(request: web.Request) -> web.Response: """GET request to get tag histogram for filtered assets.""" if "metadata_filter" in request.query: - return web.json_response( - { - "error": "UNSUPPORTED_PARAM", - "message": "metadata_filter is no longer supported", - }, - status=400, + return _build_error_response( + 400, "UNSUPPORTED_PARAM", "metadata_filter is no longer supported" ) query_dict = get_query_dict(request) diff --git a/app/assets/api/schemas_out.py b/app/assets/api/schemas_out.py index 5cccd74de..849f5f94b 100644 --- a/app/assets/api/schemas_out.py +++ b/app/assets/api/schemas_out.py @@ -15,7 +15,14 @@ class Asset(BaseModel): default=None, 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, exclude=True) + display_name: str | None = Field( + default=None, + description="Human-facing label for the asset. Not unique.", + ) + file_path: str | None = Field( + default=None, + description='Relative path in global-namespace-root form (e.g. "models/checkpoints/flux.safetensors").', + ) size: int | None = None mime_type: str | None = None tags: list[str] = Field(default_factory=list) diff --git a/openapi.yaml b/openapi.yaml index 69e268340..8470b5dc9 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -7,6 +7,10 @@ components: description: Timestamp when the asset was created format: date-time type: string + display_name: + description: Human-facing label for the asset. Not unique. + nullable: true + type: string file_path: description: Relative path in global-namespace-root form (e.g. "models/checkpoints/flux.safetensors") nullable: true @@ -141,6 +145,10 @@ components: AssetUpdated: description: Response returned when an existing asset is successfully updated. properties: + display_name: + description: Human-facing label for the asset. Not unique. + nullable: true + type: string file_path: description: Relative path in global-namespace-root form (e.g. "models/checkpoints/flux.safetensors") nullable: true diff --git a/tests-unit/assets_test/services/test_asset_response_loader_path.py b/tests-unit/assets_test/services/test_asset_response_loader_path.py index 2ce433b67..0186a639c 100644 --- a/tests-unit/assets_test/services/test_asset_response_loader_path.py +++ b/tests-unit/assets_test/services/test_asset_response_loader_path.py @@ -70,6 +70,8 @@ def test_null_stored_loader_path_is_served_as_null(tmp_path: Path): resp = _build_asset_response(result, {}) assert resp.loader_path is None + assert resp.display_name == "checkpoints/bar.safetensors" + assert resp.file_path == "models/checkpoints/bar.safetensors" def test_all_path_fields_null_without_file_path(): @@ -79,3 +81,37 @@ def test_all_path_fields_null_without_file_path(): resp = _build_asset_response(result, {}) assert resp.loader_path is None + assert resp.display_name is None + assert resp.file_path is None + + +def test_display_name_and_file_path_are_serialized(tmp_path: Path): + """Serialisation guard: both display_name and file_path survive + model_dump(mode="json"). + + display_name previously carried exclude=True, which silently dropped it + from every serialised response; file_path was never wired through at all. + Asserting on the dumped payload (not just attribute access) is what would + have caught either regression, so this guards the JSON contract directly. + """ + models = tmp_path / "models" + ckpt = models / "checkpoints" + ckpt.mkdir(parents=True) + f = ckpt / "flux.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, {}) + dumped = resp.model_dump(mode="json") + + assert dumped["display_name"] == "checkpoints/flux.safetensors" + assert dumped["file_path"] == "models/checkpoints/flux.safetensors" diff --git a/tests-unit/assets_test/test_list_filter.py b/tests-unit/assets_test/test_list_filter.py index 1fdc77699..bbfbdcfc3 100644 --- a/tests-unit/assets_test/test_list_filter.py +++ b/tests-unit/assets_test/test_list_filter.py @@ -43,8 +43,11 @@ def test_record_list_rejects_metadata_filter(http, api_base): assert response.status_code == 400 assert response.json() == { - "error": "UNSUPPORTED_PARAM", - "message": "metadata_filter is no longer supported", + "error": { + "code": "UNSUPPORTED_PARAM", + "message": "metadata_filter is no longer supported", + "details": {}, + } } diff --git a/tests-unit/assets_test/test_tags_api.py b/tests-unit/assets_test/test_tags_api.py index 1dc96688a..7e587aeee 100644 --- a/tests-unit/assets_test/test_tags_api.py +++ b/tests-unit/assets_test/test_tags_api.py @@ -15,6 +15,9 @@ def test_tag_refine_rejects_metadata_filter(http, api_base): assert response.status_code == 400 assert response.json() == { - "error": "UNSUPPORTED_PARAM", - "message": "metadata_filter is no longer supported", + "error": { + "code": "UNSUPPORTED_PARAM", + "message": "metadata_filter is no longer supported", + "details": {}, + } }