"""M2 결정적 리딩 루프 회귀 테스트 — LLM 없이 전체 서사가 코드만으로 돈다.""" import pytest from engine.repositories.content import ContentError, ContentRepository from engine.services.state import HiddenState from engine.services.story import StoryEngine, eval_condition @pytest.fixture(scope="module") def repo(): return ContentRepository("content") def play(repo, policy: str) -> StoryEngine: engine = StoryEngine(repo) step = engine.start() while step.ending is None: if step.choices: idx = next((i for i, c in enumerate(step.choices) if c.axis == policy), next((i for i, c in enumerate(step.choices) if c.axis in (None, "neutral")), 0)) _, step = engine.choose(idx) else: step = engine.advance() return engine # ── 엔딩 3종 도달 (수렴 회귀의 핵심) ───────────────────────── def test_trust_policy_reaches_eternal(repo): e = play(repo, "trust") assert e.ending.id == "ending_eternal" def test_doubt_policy_reaches_reckoning(repo): e = play(repo, "doubt") assert e.ending.id == "ending_reckoning" assert e.ending.canonical # 처단이 원작 캐논 def test_neutral_policy_reaches_release(repo): e = play(repo, "neutral") assert e.ending.id == "ending_release" # ── 캐논 보장 ──────────────────────────────────────────────── def test_reach_point_visited_on_every_policy(repo): """E06-10(두 점 발견)은 어떤 성향이든 반드시 도달 (헌법 0번).""" for policy in ("trust", "doubt", "neutral"): e = play(repo, policy) assert "E06-10" in e.visited, policy assert e.state.frailty >= 1 # 쇠약도는 신뢰와 무관하게 진행 def test_awareness_established_at_entry(repo): e = StoryEngine(repo) e.start() assert e.state.awareness_established # 엔진 A: 도입 1회 확립 def test_trust_path_commits_hidden_branch(repo): """E06-09 동조(신뢰) → 도달점 통과 후 E06-10C(숨김) 분기가 예약된다.""" e = play(repo, "trust") assert "E06-10C" in e.visited assert "E06-10B" not in e.visited # ── 숨은 상태 규칙 ─────────────────────────────────────────── def test_frailty_is_monotonic(): s = HiddenState() s.advance_frailty() with pytest.raises(ValueError): s.advance_frailty(-1) def test_trust_levels(): s = HiddenState() assert s.trust_level == "neutral" s.trust_score = 40 assert s.trust_level == "trust_high" and s.deep_unlock s.doubt_score = 80 assert s.trust_level == "doubt_high" def test_condition_parser_rejects_arbitrary_code(): s = HiddenState() assert eval_condition("default", s) assert not eval_condition("doubt_score >= 30", s) s.doubt_score = 30 assert eval_condition("doubt_score >= 30", s) with pytest.raises(ValueError): eval_condition("__import__('os')", s) # ── 스포일러 필터 ──────────────────────────────────────────── def test_spoiler_filter_blocks_future_cards(repo): with pytest.raises(ContentError): repo.get_card("E06-10", current_id="E01-01") assert repo.get_card("E01-01", current_id="E06-10").id == "E01-01" def test_deep_gate_hidden_until_trust_high(repo): """E06-09 심층 고백(엔진 D)은 신뢰高에서만 노출.""" card = repo.get_card("E06-09") gated = [b for b in card.narration if b.gate == "trust_high"] assert gated, "E06-09에 심층 게이트 블록이 있어야 함" cold = StoryEngine(repo) cold.current_id = "E06-09" assert all(b.gate == "all" for b in cold.visible_narration(card)) warm = StoryEngine(repo, HiddenState(trust_score=60)) assert any(b.gate == "trust_high" for b in warm.visible_narration(card))