"""M4 전체 관통 — 읽기 루프 → 개입 포켓(채팅) → 복귀 → 엔딩까지 한 번에.""" import json import pytest from engine.plugins.logging import TurnLoggingPlugin from engine.repositories.content import ContentRepository from engine.repositories.playlog import PlaylogRepository from engine.services.pocket import PocketService from engine.services.state import AXIS_DELTA from engine.services.story import StoryEngine from tests.fakes import ScriptedLlm, text_response, tool_call, tool_response @pytest.fixture(scope="module") def repo(): return ContentRepository("content") @pytest.mark.asyncio async def test_full_playthrough_with_pocket(repo, tmp_path): """E06-09까지 읽고 → 포켓 채팅 2턴(수렴) → 상태 커밋 → 이어서 엔딩 도달.""" playlog = PlaylogRepository(tmp_path / "playlog.jsonl") svc = PocketService( repo, plugins=[TurnLoggingPlugin(playlog)], character_model=ScriptedLlm(script=[ text_response("…그래. 너와 나 사이의 일이야, 로라."), text_response("착하지. 우리끼리만 간직하자."), text_response("…이제 됐어. 아무 말도 필요 없어."), ]), director_model=ScriptedLlm(script=[ tool_response(tool_call("update_trust", direction="trust")), text_response("계속"), tool_response(tool_call("update_trust", direction="trust"), tool_call("mark_beat", beat="둘만의 비밀 동조")), text_response("계속"), tool_response(tool_call("finish_pocket", reason="goal_reached")), text_response("수렴"), ]), ) # 1) 읽기 루프: E06-09까지 신뢰 성향으로 진행 engine = StoryEngine(repo) step = engine.start() while engine.current_id != "E06-09": if step.choices: idx = next((i for i, c in enumerate(step.choices) if c.axis == "trust"), next((i for i, c in enumerate(step.choices) if c.axis in (None, "neutral")), 0)) _, step = engine.choose(idx) else: step = engine.advance() trust_before = engine.state.trust_score # 2) 개입 포켓: 자유채팅 3턴 → director 수렴 (최소 체류 3턴) sid = await svc.open("E06-09", engine.state) t1 = await svc.run_turn(sid, "…우리 둘만의 일이라는 거지?") assert not t1.converged and "로라" in t1.reply t2 = await svc.run_turn(sid, "알았어. 아무에게도 말하지 않을게.") assert not t2.converged # 최소 체류 턴 미달 t3 = await svc.run_turn(sid, "…응. 약속할게.") assert t3.converged and t3.reason == "goal_reached" # 3) 복귀: 숨은 상태 커밋 (채팅 2턴 = trust +2×AXIS_DELTA) log = await svc.close(sid, engine.state) assert engine.state.trust_score == trust_before + 2 * AXIS_DELTA assert log.beats_hit == ["둘만의 비밀 동조"] # 4) 이어서 읽기 → 엔딩 (신뢰 성향 유지 → 영원) while step.ending is None: if step.choices: idx = next((i for i, c in enumerate(step.choices) if c.axis == "trust"), next((i for i, c in enumerate(step.choices) if c.axis in (None, "neutral")), 0)) _, step = engine.choose(idx) else: step = engine.advance() assert step.ending.id == "ending_eternal" assert "E06-10" in engine.visited # 도달점은 채팅과 무관하게 통과 # 5) 턴 로그: 유저 입력/이벤트/턴 요약이 기록됨 (연구 데이터) records = [json.loads(l) for l in (tmp_path / "playlog.jsonl").read_text().splitlines()] kinds = {r["kind"] for r in records} assert {"user_input", "event", "turn_summary"} <= kinds tool_events = [r for r in records if r.get("tool_calls")] assert any(c["tool"] == "finish_pocket" for r in tool_events for c in r["tool_calls"])