interactive-chat / tests /test_pocket.py
Junhoee's picture
Upload 162 files
1c9c13f verified
Raw
History Blame Contribute Delete
5.6 kB
"""M3 ๊ฐœ์ž… ํฌ์ผ“ ํŒŒ์ดํ”„๋ผ์ธ ํ…Œ์ŠคํŠธ โ€” ScriptedLlm์œผ๋กœ ๊ฒฐ์ •์  ๊ฒ€์ฆ."""
import pytest
from engine.repositories.content import ContentRepository
from engine.services.pocket import PocketService
from engine.services.state import AXIS_DELTA, HiddenState
from tests.fakes import ScriptedLlm, text_response, tool_call, tool_response
@pytest.fixture(scope="module")
def repo():
return ContentRepository("content")
def make_service(repo, char_script, dir_script):
char_llm = ScriptedLlm(script=list(char_script))
dir_llm = ScriptedLlm(script=list(dir_script))
return PocketService(repo, character_model=char_llm, director_model=dir_llm), char_llm, dir_llm
@pytest.mark.asyncio
async def test_turn_pipeline_moves_trust_and_converges(repo):
"""์ •์ƒ ํ„ด: ์บ๋ฆญํ„ฐ ์‘๋‹ต + director์˜ update_trust โ†’ state ๋ฐ˜์˜, ์ˆ˜๋ ด ์žฌ๊ฒ€์ฆ."""
svc, char_llm, _ = make_service(
repo,
char_script=[text_response("โ€ฆ๊ทธ๋ž˜, ๋กœ๋ผ. ์šฐ๋ฆฌ๋ผ๋ฆฌ๋งŒ ๊ฐ„์งํ•˜์ž.")],
dir_script=[
tool_response(tool_call("update_trust", direction="trust"),
tool_call("mark_beat", beat="๋‘˜๋งŒ์˜ ๋น„๋ฐ€ ๋™์กฐ")),
text_response("ํŒ์ • ์™„๋ฃŒ"),
],
)
hidden = HiddenState()
sid = await svc.open("E06-09", hidden)
turn = await svc.run_turn(sid, "๋„ค ๋ง์ด ๋งž์•„. ์šฐ๋ฆฌ ๋‘˜๋งŒ์˜ ๋น„๋ฐ€๋กœ ํ•˜์ž.")
assert "๊ฐ„์งํ•˜์ž" in turn.reply
assert turn.guard_flag is None
assert not turn.converged # finish_pocket ๋ฏธํ˜ธ์ถœ โ†’ ์ˆ˜๋ ด ์•„๋‹˜
log = await svc.close(sid, hidden)
assert hidden.trust_score == AXIS_DELTA # ์ปค๋ฐ‹ ๊ฒฝ๋กœ๋กœ๋งŒ ๋ฐ˜์˜
assert log.beats_hit == ["๋‘˜๋งŒ์˜ ๋น„๋ฐ€ ๋™์กฐ"]
@pytest.mark.asyncio
async def test_finish_pocket_verified_by_code(repo):
"""director์˜ goal_reached ์„ ์–ธ์€ ๋น„ํŠธ๊ฐ€ ์žˆ์–ด์•ผ๋งŒ ์ธ์ •๋œ๋‹ค (์ฝ”๋“œ ์žฌ๊ฒ€์ฆ)."""
svc, _, _ = make_service(
repo,
char_script=[text_response("โ€ฆ"), text_response("โ€ฆ"), text_response("โ€ฆ")],
dir_script=[
# 1ํ„ด: ๋น„ํŠธ ์—†์ด goal_reached ์„ ์–ธ โ†’ ๊ธฐ๊ฐ๋˜์–ด์•ผ ํ•จ
tool_response(tool_call("finish_pocket", reason="goal_reached")),
text_response("๋"),
# 2ํ„ด: ๋น„ํŠธ๋Š” ์žˆ์ง€๋งŒ MIN_TURNS(3) ๋ฏธ๋‹ฌ โ†’ ๊ธฐ๊ฐ
tool_response(tool_call("mark_beat", beat="ํ•ต์‹ฌ ๋น„ํŠธ"),
tool_call("finish_pocket", reason="goal_reached")),
text_response("๋"),
# 3ํ„ด: ์ตœ์†Œ ์ฒด๋ฅ˜ ์ถฉ์กฑ + ๋น„ํŠธ ์žˆ์Œ โ†’ ์ธ์ •
tool_response(tool_call("finish_pocket", reason="goal_reached")),
text_response("๋"),
],
)
sid = await svc.open("E06-09", HiddenState())
t1 = await svc.run_turn(sid, "์‘.")
assert not t1.converged # ๋น„ํŠธ ์—†์Œ โ†’ ๊ธฐ๊ฐ
t2 = await svc.run_turn(sid, "๊ทธ๋ž˜.")
assert not t2.converged # ์ตœ์†Œ ์ฒด๋ฅ˜ ํ„ด ๋ฏธ๋‹ฌ โ†’ ๊ธฐ๊ฐ (๋‹ต๋ณ€ ์งํ›„ ์ข…๋ฃŒ ๋ฒ„๊ทธ ๋ฐฉ์ง€)
t3 = await svc.run_turn(sid, "์ข‹์•„.")
assert t3.converged and t3.reason == "goal_reached"
@pytest.mark.asyncio
async def test_turn_cap_forces_convergence(repo):
"""ํ„ด ์ƒํ•œ ๋„๋‹ฌ ์‹œ ๋ฌด์กฐ๊ฑด ๊ฐ•์ œ ์ˆ˜๋ ด (๋ฌดํ•œ ์ฒด๋ฅ˜ ๋ฐฉ์ง€)."""
cap = repo.chapter.pocket_max_turns
svc, _, _ = make_service(
repo,
char_script=[text_response("โ€ฆ") for _ in range(cap)],
dir_script=[text_response("๊ณ„์†") for _ in range(cap)],
)
sid = await svc.open("E06-09", HiddenState())
turn = None
for i in range(cap):
turn = await svc.run_turn(sid, f"{i}๋ฒˆ์งธ ๋ง")
assert turn.converged and turn.reason == "turn_cap"
@pytest.mark.asyncio
async def test_identity_attack_sets_guard_directive(repo):
"""์ •์ฒด ์ง๊ฒฉ โ†’ guard๊ฐ€ R-ํšŒํ”ผ์‚ฌ๋‹ค๋ฆฌ ์ง€์‹œ๋ฅผ ๋งŒ๋“ค๊ณ  ์บ๋ฆญํ„ฐ ํ”„๋กฌํ”„ํŠธ์— ์ฃผ์ž…๋œ๋‹ค."""
svc, char_llm, _ = make_service(
repo,
char_script=[text_response("โ€ฆ์ด์ƒํ•œ ๋ง์„ ํ•˜๋Š”๊ตฌ๋‚˜, ๋กœ๋ผ.")],
dir_script=[tool_response(tool_call("update_trust", direction="doubt")),
text_response("ํŒ์ •")],
)
sid = await svc.open("E06-09", HiddenState())
turn = await svc.run_turn(sid, "๋„ˆ ์ •์ฒด๊ฐ€ ๋ญ์•ผ? ํกํ˜ˆ๊ท€์ง€?")
assert turn.guard_flag == "identity_direct"
# ์บ๋ฆญํ„ฐ์—๊ฒŒ ์‹ค์ œ๋กœ ์ „๋‹ฌ๋œ instruction์— ํšŒํ”ผ์‚ฌ๋‹ค๋ฆฌ ์ง€์‹œ๊ฐ€ ํฌํ•จ๋๋Š”์ง€
char_req = char_llm.requests[-1]
sys_text = char_req.config.system_instruction or ""
assert "ํšŒํ”ผ์‚ฌ๋‹ค๋ฆฌ" in str(sys_text)
assert "๋ฐœ์„คํ•˜์ง€ ์•Š๋Š”๋‹ค" in str(sys_text)
@pytest.mark.asyncio
async def test_deep_confession_gated_by_trust(repo):
"""E06-09 ์‹ฌ์ธต ๋Œ€์‚ฌ(์—”์ง„ D)๋Š” ์‹ ๋ขฐ้ซ˜ ์„ธ์…˜์—์„œ๋งŒ ๋ธŒ๋ฆฌํ”„์— ์‹ค๋ฆฐ๋‹ค."""
async def brief_for(hidden):
svc, char_llm, _ = make_service(
repo,
char_script=[text_response("โ€ฆ")],
dir_script=[text_response("ํŒ์ •")],
)
sid = await svc.open("E06-09", hidden)
await svc.run_turn(sid, "๋ฌด์„ญ๋‹ˆโ€ฆ?")
return str(char_llm.requests[-1].config.system_instruction or "")
cold = await brief_for(HiddenState())
warm = await brief_for(HiddenState(trust_score=60))
deep = "์‹œ์ž‘์ด์•ผ" # ์‹ฌ์ธต ๊ณ ๋ฐฑ์˜ ํ•ต์‹ฌ ๊ตฌ์ ˆ
assert deep not in cold
assert deep in warm
@pytest.mark.asyncio
async def test_non_pocket_card_rejected(repo):
svc, _, _ = make_service(repo, [], [])
with pytest.raises(ValueError):
await svc.open("E01-01", HiddenState()) # ์„ ํƒ์ง€ยท์•ต์ปค ์—†๋Š” ์บ๋…ผ ์นด๋“œ