세션 계약과 메모리 경계 보강

This commit is contained in:
Yun Chan 2026-06-28 23:52:18 +09:00
parent 391639c1de
commit 2bb052f624
12 changed files with 836 additions and 116 deletions

View file

@ -45,6 +45,90 @@ class SessionMemoryPureTest(unittest.TestCase):
self.assertIn("[ORG]", digest)
self.assertNotIn("김서연", digest)
def test_session_digest_input_keeps_only_client_visible_masked_turns(self) -> None:
digest_input = memory.build_session_digest_input(
session_id="00000000-0000-0000-0000-00000000feed",
case_id="00000000-0000-0000-0000-00000000ca5e",
session_no=4,
masked_turns=[
{
"speaker": "client",
"text": "raw 김서연",
"text_masked": "저는 [NAME]입니다.",
"turn_id": "00000000-0000-0000-0000-000000000201",
"visible_to": ["client", "evaluator"],
},
{
"speaker": "client",
"text": "평가자 전용 raw",
"text_masked": "평가자 전용 masked",
"visible_to": ["evaluator"],
},
{
"speaker": "system",
"text": "시스템 메모",
"visible_to": ["client"],
},
],
open_threads=[" 가족 이야기 이어가기 ", ""],
)
self.assertEqual(digest_input.session_no, 4)
self.assertEqual(len(digest_input.masked_turns), 1)
self.assertEqual(digest_input.masked_turns[0].speaker, "client")
self.assertEqual(digest_input.masked_turns[0].text, "저는 [NAME]입니다.")
self.assertEqual(
digest_input.masked_turns[0].turn_id,
"00000000-0000-0000-0000-000000000201",
)
self.assertEqual(digest_input.open_threads, ("가족 이야기 이어가기",))
self.assertNotIn("김서연", " ".join(turn.text for turn in digest_input.masked_turns))
def test_compression_messages_use_digest_contract_without_end_state(self) -> None:
digest_input = memory.build_session_digest_input(
session_id="00000000-0000-0000-0000-00000000feed",
case_id="00000000-0000-0000-0000-00000000ca5e",
session_no=5,
masked_turns=[
{"speaker": "client", "text": "저는 [NAME]입니다.", "visible_to": ["client"]},
{"speaker": "counselor", "text": "그때 마음을 더 말해볼까요?", "visible_to": ["client"]},
],
open_threads=["다음 회기에 가족 이야기를 이어가기"],
)
messages = memory.build_compression_messages(memory.CompressionJob(digest_input=digest_input))
prompt = "\n".join(message["content"] for message in messages)
self.assertIn("저는 [NAME]입니다.", prompt)
self.assertIn("다음 회기에 가족 이야기를 이어가기", prompt)
self.assertNotIn("김서연", prompt)
self.assertNotIn("종료 상태", prompt)
self.assertNotIn("rapport_credit", prompt)
self.assertNotIn("evaluation", prompt)
def test_fallback_digest_result_uses_shared_digest_contract(self) -> None:
digest_input = memory.build_session_digest_input(
session_id="00000000-0000-0000-0000-00000000feed",
case_id="00000000-0000-0000-0000-00000000ca5e",
session_no=6,
masked_turns=[
{"speaker": "client", "text": "저는 [NAME]입니다.", "visible_to": ["client"]},
],
open_threads=["가족 이야기"],
)
result = memory.build_fallback_digest_result(
digest_input,
end_state={"stage": "탐색", "effective_openness": 0.4, "rapport_credit": 0.3},
)
self.assertEqual(result.source, "fallback")
self.assertEqual(result.session_id, "00000000-0000-0000-0000-00000000feed")
self.assertEqual(result.case_id, "00000000-0000-0000-0000-00000000ca5e")
self.assertEqual(result.session_no, 6)
self.assertEqual(result.open_threads, ("가족 이야기",))
self.assertIn("S6:", result.digest)
self.assertIn("[NAME]", result.digest)
self.assertNotIn("김서연", result.digest)
def test_extract_pinned_fact_candidates_is_conservative_and_masked(self) -> None:
facts = memory.extract_pinned_fact_candidates(
[
@ -363,13 +447,35 @@ class SessionMemoryPersistenceTest(unittest.IsolatedAsyncioTestCase):
persisted = await session_persistence.end_session(sess, carry)
self.assertTrue(persisted)
summary_writes = [
args for query, args in conn.executed if "INSERT INTO app.session_summary" in query
]
self.assertEqual(len(summary_writes), 1)
(
summary_session_id,
summary_case_id,
summary_session_no,
summary_end_state,
summary_rapport_delta,
summary_digest,
summary_open_threads,
) = summary_writes[0]
self.assertEqual(summary_session_id, sess.session_id)
self.assertEqual(summary_case_id, case_id)
self.assertEqual(summary_session_no, 2)
self.assertEqual(summary_end_state, carry.end_state)
self.assertEqual(summary_rapport_delta, 0.21)
self.assertTrue(summary_digest.startswith("S2:"))
self.assertIn("[NAME]", summary_digest)
self.assertNotIn("김서연", summary_digest)
self.assertEqual(summary_open_threads, [])
case_updates = [
args for query, args in conn.executed if "UPDATE app.case_profile" in query
]
self.assertEqual(len(case_updates), 1)
_, _, case_digest, trajectory, alliance_level = case_updates[0]
self.assertIn("S1: 이전 회기", case_digest)
self.assertIn("S2:", case_digest)
self.assertIn(summary_digest, case_digest)
self.assertIn("[NAME]", case_digest)
self.assertNotIn("김서연", case_digest)
self.assertEqual(trajectory[-1]["session_no"], 2)