diff --git a/app/assets/api/schemas_in.py b/app/assets/api/schemas_in.py index 6f045d32f..b2e8eb34b 100644 --- a/app/assets/api/schemas_in.py +++ b/app/assets/api/schemas_in.py @@ -54,16 +54,11 @@ class ListAssetsQuery(BaseModel): exclude_tags: list[str] = Field(default_factory=list) name_contains: str | None = None - # Filter to assets whose content hash matches exactly. Param name is `hash` - # per the projected openapi.yaml listAssets contract (the response-body field - # is `asset_hash`; the query param is `hash`). + # Filter by exact content hash (emitted as `asset_hash` in responses) hash: str | None = None - # Declared for cloud/core contract parity. In core, reads are owner-scoped - # (owner_id == "") and there is no separate shared/public pool for this flag - # to include or exclude, so it is inert here and intentionally not threaded - # into the query. Accepted (not rejected) so the FE needs no isCloud branch; - # cloud enforces the flag in its own service layer. + # Accepted for API compatibility; has no effect, as there is no shared + # asset pool to include or exclude include_public: bool = True # Accept either a JSON string (query param) or a dict @@ -101,12 +96,7 @@ class ListAssetsQuery(BaseModel): @field_validator("hash", mode="before") @classmethod def _normalize_hash(cls, v): - # Normalize for an exact match against stored hashes (which are - # lowercase `blake3:`). Liberal in what we accept — no pattern - # enforcement; a non-matching value simply yields an empty page. - # An explicitly-supplied-but-empty value (`?hash=`) stays `""` so it - # is treated as an exact-match miss (empty page), not silently dropped - # to "no filter" — omit the param entirely to disable the filter. + # Stored hashes are lowercase `blake3:`; no pattern is enforced if isinstance(v, str): return v.strip().lower() return v diff --git a/app/assets/database/queries/asset_reference.py b/app/assets/database/queries/asset_reference.py index 734777377..f3e978f28 100644 --- a/app/assets/database/queries/asset_reference.py +++ b/app/assets/database/queries/asset_reference.py @@ -294,8 +294,7 @@ def list_references_page( escaped, esc = escape_sql_like_string(name_contains) base = base.where(AssetReference.name.ilike(f"%{escaped}%", escape=esc)) - # `is not None` (not truthiness): an explicit empty hash is an exact-match - # miss (empty page), while an omitted hash (None) disables the filter. + # Not truthiness: an empty hash matches nothing, an omitted one filters nothing if asset_hash is not None: base = base.where(Asset.hash == asset_hash) diff --git a/tests-unit/assets_test/test_list_filter.py b/tests-unit/assets_test/test_list_filter.py index c5e870137..dab0339b3 100644 --- a/tests-unit/assets_test/test_list_filter.py +++ b/tests-unit/assets_test/test_list_filter.py @@ -308,7 +308,7 @@ def test_list_assets_invalid_query_rejected(http: requests.Session, api_base: st def test_list_assets_display_name_emitted(http, api_base, asset_factory, make_asset_bytes): """`display_name` is emitted for every populated asset in list responses, - derived from the storage path (category prefix + hash-based stored filename).""" + derived from the storage path.""" scope = f"lf-dispname-{uuid.uuid4().hex[:6]}" tags = ["models", "model_type:checkpoints", "unit-tests", scope] asset_factory("dn_a.safetensors", tags, {}, make_asset_bytes("dn_a", 700)) @@ -371,8 +371,7 @@ def test_list_assets_hash_filter_no_match(http, api_base, asset_factory, make_as def test_list_assets_hash_filter_normalizes_case_and_whitespace( http, api_base, asset_factory, make_asset_bytes ): - """`hash` is trimmed and lowercased before matching, so an upper-cased, - space-padded value still matches the stored lowercase hash.""" + """An upper-cased, space-padded `hash` still matches the stored hash.""" scope = f"lf-hashnorm-{uuid.uuid4().hex[:6]}" tags = ["models", "model_type:checkpoints", "unit-tests", scope] a = asset_factory("hnorm_a.safetensors", tags, {}, make_asset_bytes("hnorm_a", 1024)) @@ -396,8 +395,8 @@ def test_list_assets_hash_filter_normalizes_case_and_whitespace( def test_list_assets_hash_filter_empty_returns_empty_page( http, api_base, asset_factory, make_asset_bytes ): - """An explicitly-supplied but empty `hash` (`?hash=`) is an exact-match miss - and returns an empty page, rather than silently disabling the filter.""" + """An empty `hash` matches nothing and returns an empty page, rather than + disabling the filter.""" scope = f"lf-hashempty-{uuid.uuid4().hex[:6]}" tags = ["models", "model_type:checkpoints", "unit-tests", scope] asset_factory("he_a.safetensors", tags, {}, make_asset_bytes("he_a", 800)) @@ -414,8 +413,7 @@ def test_list_assets_hash_filter_empty_returns_empty_page( def test_list_assets_include_public_accepted(http, api_base, asset_factory, make_asset_bytes): - """`include_public` is accepted for contract parity; core results are always - the caller's own assets regardless of its value (the param is inert).""" + """`include_public` is accepted and does not change which assets come back.""" scope = f"lf-incpub-{uuid.uuid4().hex[:6]}" tags = ["models", "model_type:checkpoints", "unit-tests", scope] a = asset_factory("ip_a.safetensors", tags, {}, make_asset_bytes("ip_a", 900)) @@ -429,7 +427,7 @@ def test_list_assets_include_public_accepted(http, api_base, asset_factory, make body = r.json() assert r.status_code == 200, body names = [x["name"] for x in body["assets"]] - assert a["name"] in names, f"caller's own asset must be returned (include_public={value})" + assert a["name"] in names, f"asset must be returned (include_public={value})" def test_list_assets_name_contains_literal_underscore(