"""세션 영속화 회귀 — 스냅샷/복원 + SQLite 저장소 + 라우터 재시작 복원. 핵심 회귀: 복원은 _enter()를 거치지 않는다 — 쇠약도/신뢰축이 재적용되면 결함. """ import pytest from fastapi import HTTPException from engine import router from engine.repositories.content import ContentRepository from engine.repositories.play_session import PlaySessionRepository from engine.services.story import StoryEngine @pytest.fixture(scope="module") def repo(): return ContentRepository("content") def play_until(repo, stop_id: str | None, policy: str = "trust") -> StoryEngine: """stop_id 카드 도달(또는 None이면 엔딩)까지 진행.""" engine = StoryEngine(repo) step = engine.start() while step.ending is None and engine.current_id != stop_id: 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 # ── 스냅샷/복원 (엔진) ────────────────────────────────────── def test_snapshot_restore_roundtrip(repo): engine = play_until(repo, "E04-01") snap = engine.snapshot() restored = StoryEngine.restore(repo, snap) assert restored.snapshot() == snap # 복원이 상태를 조금도 바꾸지 않는다 assert restored.current_id == engine.current_id assert restored.visited == engine.visited step = restored.current_step() assert step.card.id == engine.current_id assert step.ending is None def test_restore_does_not_reapply_enter_side_effects(repo): """핵심 회귀 — 복원 시 쇠약도·신뢰축이 재적용되면 안 된다 (_enter 우회 검증).""" engine = play_until(repo, None) # 엔딩까지 = 쇠약도·축 이벤트 다수 누적 assert engine.state.frailty > 0 and engine.state.trust_score > 0 snap = engine.snapshot() restored = StoryEngine.restore(repo, snap) assert restored.state.frailty == engine.state.frailty assert restored.state.trust_score == engine.state.trust_score assert restored.state.doubt_score == engine.state.doubt_score assert restored.state.axis_log == engine.state.axis_log # 여러 번 복원해도 동일 (재적용이 있으면 여기서 어긋난다) again = StoryEngine.restore(repo, restored.snapshot()) assert again.snapshot() == snap def test_ending_session_restores_to_ending(repo): engine = play_until(repo, None, policy="trust") assert engine.ending is not None restored = StoryEngine.restore(repo, engine.snapshot()) step = restored.current_step() assert step.ending is not None and step.ending.id == engine.ending.id # ── SQLite 저장소 ─────────────────────────────────────────── def test_play_session_repo_save_load(tmp_path, repo): db = PlaySessionRepository(tmp_path / "sessions.db") engine = play_until(repo, "E02-01") db.save("sid1", engine.snapshot(), repo.content_version) loaded = db.load("sid1", repo.content_version) assert loaded == engine.snapshot() assert db.load("없는sid", repo.content_version) is None def test_content_version_mismatch_invalidates(tmp_path, repo): db = PlaySessionRepository(tmp_path / "sessions.db") db.save("sid1", {"story": {}, "hidden": {}}, "구버전해시") assert db.load("sid1", repo.content_version) is None # 콘텐츠 갱신 → 무효화 def test_purge_removes_stale_sessions(tmp_path): db = PlaySessionRepository(tmp_path / "sessions.db") db.save("sid1", {"story": {}, "hidden": {}}, "v") assert db.purge(days=30) == 0 # 방금 저장 — 남는다 assert db.purge(days=0) == 1 # 즉시 만료 취급 — 지워진다 # ── 라우터 (재시작 모사) ──────────────────────────────────── @pytest.mark.asyncio async def test_router_restores_after_restart(tmp_path, monkeypatch): monkeypatch.setattr(router, "_playdb", PlaySessionRepository(tmp_path / "s.db")) monkeypatch.setattr(router, "_sessions", {}) data = await router.create_session() sid = data["session_id"] step = data["step"] while not step["choices"] and not step["ending"]: # 첫 선택지까지 읽기 step = (await router.advance(sid))["step"] step = (await router.choose(sid, router.ChooseBody(index=0)))["step"] current_card = step["card"]["id"] router._sessions.clear() # 서버 재시작 모사 (인메모리 캐시 소실) got = await router.get_session(sid) assert got["step"]["card"]["id"] == current_card assert router._sessions[sid].pocket_sid is None # 포켓은 비영속 — 닫힌 채 복원 @pytest.mark.asyncio async def test_router_unknown_sid_404(tmp_path, monkeypatch): monkeypatch.setattr(router, "_playdb", PlaySessionRepository(tmp_path / "s.db")) monkeypatch.setattr(router, "_sessions", {}) with pytest.raises(HTTPException) as exc: await router.get_session("없는세션") assert exc.value.status_code == 404