Files
C.E.L_Slide_test2/tests/phase_z2_ai_fallback/test_cache.py
kyeongmin c864fe0479 feat(#61): IMP-33 AI fallback scaffolding (u1~u11, flag default OFF)
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>
2026-05-21 12:46:49 +09:00

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)