Frame-aware AI fallback module scaffolded under src/phase_z2_ai_fallback/ with master flag ai_fallback_enabled=False; normal-path AI call count remains 0. AI output constrained to builder_options_patch / partial_overrides / slot_mapping_proposal; MDX / frame_id / raw HTML / raw CSS mutations rejected at schema layer. IMP-46 cache gate (cache.py) raises AiFallbackCacheGateError unless visual_check_passed AND user_approved. Step 12 wires AI repair after IMP-30 provisional payload only; Step 17 stays blocked behind IMP-34 / IMP-35 prerequisites. AST isolation guard forbids fallback package from importing Phase Q / Kei / pipeline runtime symbols. Docs IMP-17 / IMP-31 bound to runtime module surface via 11-row structural test pin (test_docs_sync.py) so drift fails CI. Tests: 116 fallback / 161 phase_z2 regression / 526 scoped full sweep all passing. Existing pre-IMP-33 fixture issue in scripts/test_phase_t_* remains untouched (out of scope). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
91 lines
2.6 KiB
Python
91 lines
2.6 KiB
Python
"""IMP-33 u6 — AI fallback cache gate tests.
|
|
|
|
Verifies the IMP-46 gate contract:
|
|
* ``read_proposal`` is a stub (returns None until IMP-46).
|
|
* ``save_proposal`` enforces both gates before any write attempt.
|
|
* Storage itself raises NotImplementedError (IMP-46 marker).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from src.phase_z2_ai_fallback.cache import (
|
|
AiFallbackCacheGateError,
|
|
read_proposal,
|
|
save_proposal,
|
|
)
|
|
from src.phase_z2_ai_fallback.schema import AiFallbackProposal, ProposalKind
|
|
|
|
|
|
def _proposal() -> AiFallbackProposal:
|
|
return AiFallbackProposal(
|
|
proposal_kind=ProposalKind.BUILDER_OPTIONS_PATCH,
|
|
payload={"item_parser": "bullet_v2"},
|
|
rationale="u6-test",
|
|
)
|
|
|
|
|
|
def test_read_proposal_returns_none_for_any_key():
|
|
assert read_proposal("frame=foo|cardinality=3") is None
|
|
|
|
|
|
def test_read_proposal_rejects_empty_key():
|
|
with pytest.raises(ValueError):
|
|
read_proposal("")
|
|
|
|
|
|
def test_save_rejects_when_visual_check_failed():
|
|
with pytest.raises(AiFallbackCacheGateError) as exc:
|
|
save_proposal(
|
|
"k", _proposal(), visual_check_passed=False, user_approved=True
|
|
)
|
|
assert "visual_check_passed" in str(exc.value)
|
|
|
|
|
|
def test_save_rejects_when_user_not_approved():
|
|
with pytest.raises(AiFallbackCacheGateError) as exc:
|
|
save_proposal(
|
|
"k", _proposal(), visual_check_passed=True, user_approved=False
|
|
)
|
|
assert "user_approved" in str(exc.value)
|
|
|
|
|
|
def test_save_rejects_when_both_gates_false():
|
|
with pytest.raises(AiFallbackCacheGateError):
|
|
save_proposal(
|
|
"k", _proposal(), visual_check_passed=False, user_approved=False
|
|
)
|
|
|
|
|
|
def test_save_raises_not_implemented_when_both_gates_pass():
|
|
with pytest.raises(NotImplementedError) as exc:
|
|
save_proposal(
|
|
"k", _proposal(), visual_check_passed=True, user_approved=True
|
|
)
|
|
assert "IMP-46" in str(exc.value)
|
|
|
|
|
|
def test_save_rejects_empty_key():
|
|
with pytest.raises(ValueError):
|
|
save_proposal(
|
|
"", _proposal(), visual_check_passed=True, user_approved=True
|
|
)
|
|
|
|
|
|
def test_save_rejects_non_proposal_object():
|
|
with pytest.raises(TypeError):
|
|
save_proposal(
|
|
"k",
|
|
{"proposal_kind": "builder_options_patch"}, # type: ignore[arg-type]
|
|
visual_check_passed=True,
|
|
user_approved=True,
|
|
)
|
|
|
|
|
|
def test_gate_error_is_not_notimplementederror():
|
|
with pytest.raises(AiFallbackCacheGateError):
|
|
save_proposal(
|
|
"k", _proposal(), visual_check_passed=False, user_approved=True
|
|
)
|
|
assert not issubclass(AiFallbackCacheGateError, NotImplementedError)
|