File size: 5,598 Bytes
1c9c13f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
"""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())  # ์„ ํƒ์ง€ยท์•ต์ปค ์—†๋Š” ์บ๋…ผ ์นด๋“œ