- u1~u9: AI fallback infrastructure (router/prompts/schema/validator) + Step 12 hook - u10: e2e reject chain (writes final.html with AI-repaired slot, full coverage) - u11: frontend wiring deferred to follow-up commit (split from IMP-41 hunks) - u12: coverage_invariant guard - u13: cache save gate (visual_check PASS + user_approved/auto_cache) — Codex #22 verified Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
185 lines
5.2 KiB
Python
185 lines
5.2 KiB
Python
"""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
|