"""IMP-46 u1 — Frame cache signature builder tests. Verifies: * Determinism — identical inputs yield the same SHA256 digest. * Axis-change sensitivity — every one of the 8 declared axes mutates the digest when changed in isolation. * Public surface — only the 8 declared axes are accepted (no sample/section identifier leakage). * char_count bucket boundaries (0-50, 51-150, 151-400, 401-1000, 1001+). * source_shape enum equivalence (string and SourceShape inputs match). * schema_version is part of the hashed payload (digest stable for fixture). """ from __future__ import annotations import inspect import pytest from src.phase_z2_ai_fallback.signature import ( CHAR_COUNT_BUCKET_LABELS, SCHEMA_VERSION, SourceShape, bucket_char_count, build_signature, ) def _base_kwargs() -> dict: return dict( frame_id="frame_03", v4_label="light_edit", cardinality=3, source_shape=SourceShape.BULLET, h3_count=2, char_count_bucket="51-150", layout_preset="sidebar-right", zone_position="top", ) def test_schema_version_is_one() -> None: assert SCHEMA_VERSION == 1 def test_bucket_labels_match_spec() -> None: assert CHAR_COUNT_BUCKET_LABELS == ( "0-50", "51-150", "151-400", "401-1000", "1001+", ) def test_signature_is_deterministic() -> None: a = build_signature(**_base_kwargs()) b = build_signature(**_base_kwargs()) assert a == b assert len(a) == 64 @pytest.mark.parametrize( "axis, new_value", [ ("frame_id", "frame_04"), ("v4_label", "restructure"), ("cardinality", 5), ("source_shape", SourceShape.PARAGRAPH), ("h3_count", 3), ("char_count_bucket", "151-400"), ("layout_preset", "two-column"), ("zone_position", "bottom_l"), ], ) def test_signature_changes_for_each_axis(axis: str, new_value: object) -> None: base = build_signature(**_base_kwargs()) kwargs = _base_kwargs() kwargs[axis] = new_value assert build_signature(**kwargs) != base def test_signature_accepts_string_source_shape() -> None: enum_sig = build_signature(**_base_kwargs()) kwargs = _base_kwargs() kwargs["source_shape"] = "bullet" assert build_signature(**kwargs) == enum_sig def test_signature_rejects_unknown_source_shape() -> None: kwargs = _base_kwargs() kwargs["source_shape"] = "nonsense" with pytest.raises(ValueError): build_signature(**kwargs) def test_signature_rejects_unknown_char_count_bucket() -> None: kwargs = _base_kwargs() kwargs["char_count_bucket"] = "999-1234" with pytest.raises(ValueError): build_signature(**kwargs) def test_signature_handles_none_cardinality() -> None: kwargs = _base_kwargs() kwargs["cardinality"] = None sig = build_signature(**kwargs) assert len(sig) == 64 kwargs2 = _base_kwargs() kwargs2["cardinality"] = 0 assert build_signature(**kwargs2) != sig def test_signature_surface_only_8_declared_axes() -> None: params = set(inspect.signature(build_signature).parameters) expected = { "frame_id", "v4_label", "cardinality", "source_shape", "h3_count", "char_count_bucket", "layout_preset", "zone_position", } assert params == expected def test_bucket_boundaries() -> None: assert bucket_char_count(0) == "0-50" assert bucket_char_count(50) == "0-50" assert bucket_char_count(51) == "51-150" assert bucket_char_count(150) == "51-150" assert bucket_char_count(151) == "151-400" assert bucket_char_count(400) == "151-400" assert bucket_char_count(401) == "401-1000" assert bucket_char_count(1000) == "401-1000" assert bucket_char_count(1001) == "1001+" assert bucket_char_count(10_000) == "1001+" def test_bucket_rejects_negative() -> None: with pytest.raises(ValueError): bucket_char_count(-1) def test_bucket_rejects_non_int() -> None: with pytest.raises(TypeError): bucket_char_count(3.14) # type: ignore[arg-type] with pytest.raises(TypeError): bucket_char_count(True) # type: ignore[arg-type] def test_signature_stable_known_fixture() -> None: """Lock the digest for a known fixture so a silent payload-shape change (e.g. a new axis sneaks in, or schema_version drifts) breaks this test. """ sig = build_signature( frame_id="frame_03", v4_label="light_edit", cardinality=3, source_shape=SourceShape.BULLET, h3_count=2, char_count_bucket="51-150", layout_preset="sidebar-right", zone_position="top", ) import hashlib import json expected_payload = { "schema_version": 1, "frame_id": "frame_03", "v4_label": "light_edit", "cardinality": 3, "source_shape": "bullet", "h3_count": 2, "char_count_bucket": "51-150", "layout_preset": "sidebar-right", "zone_position": "top", } expected = hashlib.sha256( json.dumps(expected_payload, sort_keys=True, ensure_ascii=False).encode("utf-8") ).hexdigest() assert sig == expected