573 lines
24 KiB
Python
573 lines
24 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from . import session_persistence
|
|
from .routes import sessions
|
|
from .services import memory, rag, state_machine
|
|
from .services.persona import P1
|
|
from .store import InProcSession, TurnRecord
|
|
|
|
|
|
class SessionMemoryPureTest(unittest.TestCase):
|
|
def test_case_digest_merge_is_idempotent_by_session_no(self) -> None:
|
|
digest = memory.merge_case_digest(
|
|
existing_digest="S1: 이전 회기 요약\nS2: 오래된 요약",
|
|
session_no=2,
|
|
session_digest="S2: 새 요약",
|
|
)
|
|
|
|
self.assertEqual(digest, "S1: 이전 회기 요약\nS2: 새 요약")
|
|
|
|
def test_rapport_trajectory_merge_replaces_same_session(self) -> None:
|
|
merged = memory.merge_rapport_trajectory(
|
|
[{"session_no": 1, "end_rapport": 0.2}, {"session_no": 2, "end_rapport": 0.3}],
|
|
{"session_no": 2, "end_rapport": 0.7, "end_openness": 0.5},
|
|
)
|
|
|
|
self.assertEqual(len(merged), 2)
|
|
self.assertEqual(merged[-1]["session_no"], 2)
|
|
self.assertEqual(merged[-1]["end_rapport"], 0.7)
|
|
|
|
def test_fallback_session_digest_uses_masked_client_visible_text(self) -> None:
|
|
digest = memory.build_fallback_session_digest(
|
|
session_no=3,
|
|
masked_turns=[
|
|
{"speaker": "counselor", "text": "그때 마음이 어땠나요?"},
|
|
{"speaker": "client", "text": "저는 [NAME]이고 [ORG]에 다녀요."},
|
|
],
|
|
end_state={"stage": "탐색", "effective_openness": 0.42, "rapport_credit": 0.31},
|
|
)
|
|
|
|
self.assertIn("S3:", digest)
|
|
self.assertIn("[NAME]", digest)
|
|
self.assertIn("[ORG]", digest)
|
|
self.assertNotIn("김서연", digest)
|
|
|
|
def test_extract_pinned_fact_candidates_is_conservative_and_masked(self) -> None:
|
|
facts = memory.extract_pinned_fact_candidates(
|
|
[
|
|
{"speaker": "counselor", "text": "이름을 말해줄 수 있나요?"},
|
|
{
|
|
"speaker": "client",
|
|
"text": "저는 [NAME]이고 [ORG]에 다녀요. 동생과 자주 싸워요. 주 1회 상담 약속은 지키고 싶어요.",
|
|
"turn_id": "00000000-0000-0000-0000-000000000201",
|
|
},
|
|
]
|
|
)
|
|
|
|
by_key = {fact.key: fact for fact in facts}
|
|
self.assertEqual(by_key["identity:name"].value, "[NAME]")
|
|
self.assertEqual(by_key["identity:org"].value, "[ORG]")
|
|
self.assertEqual(by_key["agreement:counseling"].fact_type, "agreement")
|
|
self.assertNotIn("relationship:sibling", by_key)
|
|
self.assertNotIn("김서연", " ".join(fact.value for fact in facts))
|
|
|
|
def test_extract_pinned_fact_candidates_skips_inferred_or_transient_content(self) -> None:
|
|
facts = memory.extract_pinned_fact_candidates(
|
|
[
|
|
{"speaker": "client", "text": "오늘은 그냥 기분이 좀 나빴어요."},
|
|
{"speaker": "client", "text": "동생과 자주 싸워요."},
|
|
{"speaker": "client", "text": "죽고 싶다는 생각이 스쳐갔어요."},
|
|
]
|
|
)
|
|
|
|
self.assertEqual(facts, [])
|
|
|
|
def test_extract_pinned_fact_candidates_marks_explicit_agreement_withdrawal_only(self) -> None:
|
|
facts = memory.extract_pinned_fact_candidates(
|
|
[
|
|
{
|
|
"speaker": "client",
|
|
"text": "상담 약속은 이제 못 지키겠어요. 회기는 그만하고 싶어요.",
|
|
"turn_id": "00000000-0000-0000-0000-000000000203",
|
|
},
|
|
{"speaker": "client", "text": "동생과 자주 싸워요."},
|
|
]
|
|
)
|
|
|
|
by_key = {fact.key: fact for fact in facts}
|
|
self.assertEqual(list(by_key), ["agreement:counseling"])
|
|
self.assertEqual(by_key["agreement:counseling"].status, "contradicted")
|
|
self.assertEqual(by_key["agreement:counseling"].fact_type, "agreement")
|
|
self.assertIn("못 지키겠어요", by_key["agreement:counseling"].value)
|
|
self.assertEqual(
|
|
by_key["agreement:counseling"].source_turn_id,
|
|
"00000000-0000-0000-0000-000000000203",
|
|
)
|
|
|
|
def test_episodic_turn_inputs_use_masked_client_visible_client_turns_only(self) -> None:
|
|
inputs = rag.episodic_turn_inputs_from_records(
|
|
session_id="00000000-0000-0000-0000-00000000feed",
|
|
case_id="00000000-0000-0000-0000-00000000ca5e",
|
|
turns=[
|
|
TurnRecord(
|
|
turn_seq=1,
|
|
speaker="client",
|
|
stage="라포",
|
|
text="저는 김서연이고 한신대학교에 다녀요.",
|
|
text_masked="저는 [NAME]이고 [ORG]에 다녀요.",
|
|
turn_id="00000000-0000-0000-0000-000000000201",
|
|
),
|
|
TurnRecord(
|
|
turn_seq=2,
|
|
speaker="counselor",
|
|
stage="라포",
|
|
text="상담자 발화",
|
|
text_masked="상담자 발화",
|
|
turn_id="00000000-0000-0000-0000-000000000202",
|
|
),
|
|
TurnRecord(
|
|
turn_seq=3,
|
|
speaker="client",
|
|
stage="라포",
|
|
text="평가자만 볼 발화",
|
|
text_masked="평가자만 볼 발화",
|
|
turn_id="00000000-0000-0000-0000-000000000203",
|
|
visible_to=("evaluator",),
|
|
),
|
|
TurnRecord(
|
|
turn_seq=4,
|
|
speaker="client",
|
|
stage="라포",
|
|
text="DB turn id 없음",
|
|
text_masked="DB turn id 없음",
|
|
),
|
|
],
|
|
)
|
|
|
|
self.assertEqual(len(inputs), 1)
|
|
self.assertEqual(inputs[0].turn_id, "00000000-0000-0000-0000-000000000201")
|
|
self.assertEqual(inputs[0].seq, 1)
|
|
self.assertEqual(inputs[0].text_masked, "저는 [NAME]이고 [ORG]에 다녀요.")
|
|
self.assertNotIn("김서연", inputs[0].text_masked)
|
|
self.assertNotIn("한신대학교", inputs[0].text_masked)
|
|
|
|
|
|
class SessionMemoryPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|
async def test_write_persona_turn_embeddings_is_masked_and_idempotent(self) -> None:
|
|
class FakeConn:
|
|
def __init__(self) -> None:
|
|
self.executed: list[tuple[str, tuple[object, ...]]] = []
|
|
|
|
async def execute(self, query: str, *args: object) -> str:
|
|
self.executed.append((query, args))
|
|
return "INSERT 0 1"
|
|
|
|
conn = FakeConn()
|
|
turn = rag.EpisodicTurnInput(
|
|
turn_id="00000000-0000-0000-0000-000000000201",
|
|
case_id="00000000-0000-0000-0000-00000000ca5e",
|
|
session_id="00000000-0000-0000-0000-00000000feed",
|
|
seq=7,
|
|
text_masked="저는 [NAME]이고 [ORG]에 다녀요.",
|
|
)
|
|
captured_texts: list[str] = []
|
|
|
|
def fake_embed_query(text: str) -> rag.EmbeddedQuery:
|
|
captured_texts.append(text)
|
|
return rag.EmbeddedQuery(dense=[0.1] * rag.EMBED_DIM, sparse={"42": 0.7})
|
|
|
|
with patch.object(rag, "embed_query", fake_embed_query):
|
|
result = await rag.write_persona_turn_embeddings(conn, turns=[turn])
|
|
|
|
self.assertEqual(result.inserted, 1)
|
|
self.assertEqual(captured_texts, ["저는 [NAME]이고 [ORG]에 다녀요."])
|
|
self.assertEqual(len(conn.executed), 1)
|
|
query, args = conn.executed[0]
|
|
self.assertIn("INSERT INTO app.turn_embedding", query)
|
|
self.assertIn("ON CONFLICT (turn_id) DO NOTHING", query)
|
|
self.assertEqual(args[0], turn.turn_id)
|
|
self.assertEqual(args[1], turn.case_id)
|
|
self.assertEqual(args[2], turn.session_id)
|
|
self.assertEqual(args[3], 7)
|
|
self.assertIn("[0.1,0.1", str(args[4]))
|
|
self.assertEqual(args[5], '{"42": 0.7}')
|
|
self.assertNotIn("김서연", " ".join(str(arg) for arg in args))
|
|
|
|
async def test_end_persisted_session_schedules_episodic_embedding_writer(self) -> None:
|
|
scheduled: list[object] = []
|
|
|
|
def fake_create_task(coro):
|
|
scheduled.append(coro)
|
|
coro.close()
|
|
return None
|
|
|
|
sess = InProcSession(
|
|
session_id="00000000-0000-0000-0000-00000000feed",
|
|
case_id="00000000-0000-0000-0000-00000000ca5e",
|
|
learner_id="00000000-0000-0000-0000-000000000101",
|
|
persona_code=P1.code,
|
|
theory_mode="humanistic",
|
|
persona=P1,
|
|
state=state_machine.SessionState(),
|
|
session_no=1,
|
|
)
|
|
carry = memory.CarryOver(
|
|
end_state={},
|
|
rapport_delta=0.0,
|
|
compression_job=None,
|
|
)
|
|
|
|
with patch.object(session_persistence, "end_session", return_value=True), patch.object(
|
|
sessions.asyncio,
|
|
"create_task",
|
|
fake_create_task,
|
|
):
|
|
await sessions._end_persisted_session(sess, carry)
|
|
|
|
self.assertTrue(sess.ended)
|
|
self.assertEqual(len(scheduled), 1)
|
|
|
|
async def test_seed_recall_loads_case_digest_and_client_visible_pinned_facts(self) -> None:
|
|
test_case = self
|
|
|
|
class FakeConn:
|
|
def __init__(self) -> None:
|
|
self.fetch_queries: list[tuple[str, tuple[object, ...]]] = []
|
|
|
|
async def fetchrow(self, query: str, *args: object):
|
|
self.fetch_queries.append((query, args))
|
|
if "FROM app.case_profile" in query:
|
|
return {"case_digest": "S1: 케이스 큰그림"}
|
|
if "FROM app.session_summary" in query:
|
|
return {
|
|
"digest": "직전 회기 요약",
|
|
"open_threads": ["가족 이야기를 이어가기"],
|
|
"end_state": {"rapport_credit": 0.5},
|
|
}
|
|
return None
|
|
|
|
async def fetch(self, query: str, *args: object):
|
|
self.fetch_queries.append((query, args))
|
|
test_case.assertIn("FROM app.pinned_fact", query)
|
|
test_case.assertIn("status IN ('stable', 'evolving', 'locked')", query)
|
|
test_case.assertIn("$2 = ANY(visible_to)", query)
|
|
return [{"value": "동생과의 갈등"}, {"value": "주 1회 상담 약속"}]
|
|
|
|
class FakeAcquire:
|
|
def __init__(self, conn: FakeConn) -> None:
|
|
self.conn = conn
|
|
|
|
async def __aenter__(self) -> FakeConn:
|
|
return self.conn
|
|
|
|
async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
|
|
return None
|
|
|
|
conn = FakeConn()
|
|
with patch.object(sessions.db, "get_pool", return_value=object()), patch.object(
|
|
sessions.db,
|
|
"acquire",
|
|
return_value=FakeAcquire(conn),
|
|
):
|
|
recall = await sessions._build_seed_recall(
|
|
case_id="00000000-0000-0000-0000-00000000ca5e"
|
|
)
|
|
|
|
self.assertIn("[케이스 큰그림]", recall.recall_summary or "")
|
|
self.assertIn("S1: 케이스 큰그림", recall.recall_summary or "")
|
|
self.assertIn("직전 회기 요약", recall.recall_summary or "")
|
|
self.assertEqual(recall.pinned_facts, ["동생과의 갈등", "주 1회 상담 약속"])
|
|
self.assertEqual(recall.carry, {"rapport_credit": 0.5})
|
|
|
|
async def test_end_session_updates_session_summary_and_case_profile(self) -> None:
|
|
test_case = self
|
|
|
|
class FakeConn:
|
|
def __init__(self) -> None:
|
|
self.executed: list[tuple[str, tuple[object, ...]]] = []
|
|
|
|
async def execute(self, query: str, *args: object) -> str:
|
|
self.executed.append((query, args))
|
|
return "OK"
|
|
|
|
async def fetchrow(self, query: str, *args: object):
|
|
self.executed.append((query, args))
|
|
if "INSERT INTO app.pinned_fact" in query:
|
|
return {
|
|
"id": f"00000000-0000-0000-0000-00000000fa{len(self.executed):02d}",
|
|
"old_value": None,
|
|
"new_value": args[2],
|
|
}
|
|
test_case.assertIn("FROM app.case_profile", query)
|
|
return {
|
|
"case_digest": "S1: 이전 회기",
|
|
"rapport_trajectory": [{"session_no": 1, "end_rapport": 0.2}],
|
|
"alliance_level": 0.2,
|
|
}
|
|
|
|
class FakeAcquire:
|
|
def __init__(self, conn: FakeConn) -> None:
|
|
self.conn = conn
|
|
|
|
async def __aenter__(self) -> FakeConn:
|
|
return self.conn
|
|
|
|
async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
|
|
return None
|
|
|
|
learner_id = "00000000-0000-0000-0000-000000000101"
|
|
case_id = "00000000-0000-0000-0000-00000000ca5e"
|
|
state = state_machine.SessionState(
|
|
stage=state_machine.Stage.EXPLORE,
|
|
turn_seq=2,
|
|
effective_openness=0.42,
|
|
rapport_credit=0.31,
|
|
resistance=0.5,
|
|
)
|
|
sess = InProcSession(
|
|
session_id="00000000-0000-0000-0000-00000000feed",
|
|
case_id=case_id,
|
|
learner_id=learner_id,
|
|
persona_code=P1.code,
|
|
theory_mode="humanistic",
|
|
persona=P1,
|
|
state=state,
|
|
session_no=2,
|
|
prev_rapport_credit=0.1,
|
|
turns=[
|
|
TurnRecord(
|
|
turn_seq=1,
|
|
speaker="counselor",
|
|
stage="라포",
|
|
text="실명 질문",
|
|
text_masked="이름을 말해줄 수 있나요?",
|
|
),
|
|
TurnRecord(
|
|
turn_seq=1,
|
|
speaker="client",
|
|
stage="라포",
|
|
text="저는 김서연이고 한신대학교에 다녀요. 동생과 자주 싸워요. 주 1회 상담 약속은 지키고 싶어요.",
|
|
text_masked="저는 [NAME]이고 [ORG]에 다녀요. 동생과 자주 싸워요. 주 1회 상담 약속은 지키고 싶어요.",
|
|
turn_id="00000000-0000-0000-0000-000000000201",
|
|
),
|
|
],
|
|
)
|
|
carry = memory.make_carry_over(
|
|
state=state,
|
|
session_id=sess.session_id,
|
|
case_id=sess.case_id,
|
|
session_no=sess.session_no,
|
|
masked_turns=sess.masked_turns(),
|
|
prev_rapport_credit=sess.prev_rapport_credit,
|
|
)
|
|
|
|
conn = FakeConn()
|
|
with patch.object(session_persistence, "get_pool", return_value=object()), patch.object(
|
|
session_persistence,
|
|
"acquire",
|
|
return_value=FakeAcquire(conn),
|
|
):
|
|
persisted = await session_persistence.end_session(sess, carry)
|
|
|
|
self.assertTrue(persisted)
|
|
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("[NAME]", case_digest)
|
|
self.assertNotIn("김서연", case_digest)
|
|
self.assertEqual(trajectory[-1]["session_no"], 2)
|
|
self.assertEqual(trajectory[-1]["end_rapport"], 0.31)
|
|
self.assertGreater(alliance_level, 0.2)
|
|
pinned_writes = [
|
|
args
|
|
for query, args in conn.executed
|
|
if "INSERT INTO app.pinned_fact (" in query
|
|
]
|
|
self.assertEqual(len(pinned_writes), 3)
|
|
by_key = {args[1]: args for args in pinned_writes}
|
|
self.assertEqual(by_key["identity:name"][2], "[NAME]")
|
|
self.assertEqual(by_key["identity:org"][2], "[ORG]")
|
|
self.assertEqual(by_key["agreement:counseling"][3], "agreement")
|
|
self.assertNotIn("relationship:sibling", by_key)
|
|
self.assertEqual(by_key["identity:name"][5], "00000000-0000-0000-0000-000000000201")
|
|
self.assertEqual(by_key["identity:name"][7], 2)
|
|
self.assertEqual(by_key["identity:name"][8], ["client", "evaluator"])
|
|
self.assertNotIn("김서연", " ".join(str(args[2]) for args in pinned_writes))
|
|
history_writes = [
|
|
args for query, args in conn.executed if "INSERT INTO app.pinned_fact_history" in query
|
|
]
|
|
self.assertEqual(len(history_writes), 3)
|
|
self.assertEqual(history_writes[0][1], case_id)
|
|
self.assertIsNone(history_writes[0][2])
|
|
self.assertEqual(history_writes[0][3], "[NAME]")
|
|
self.assertEqual(history_writes[0][4], "progression")
|
|
self.assertEqual(history_writes[0][5], 2)
|
|
self.assertEqual(
|
|
history_writes[0][6],
|
|
"00000000-0000-0000-0000-000000000201",
|
|
)
|
|
self.assertNotIn("김서연", " ".join(str(args[3]) for args in history_writes))
|
|
|
|
async def test_pinned_fact_history_skips_same_value_refresh(self) -> None:
|
|
class FakeConn:
|
|
def __init__(self) -> None:
|
|
self.executed: list[tuple[str, tuple[object, ...]]] = []
|
|
|
|
async def fetchrow(self, query: str, *args: object):
|
|
self.executed.append((query, args))
|
|
return {
|
|
"id": "00000000-0000-0000-0000-00000000fa11",
|
|
"old_value": args[2],
|
|
"new_value": args[2],
|
|
}
|
|
|
|
async def execute(self, query: str, *args: object) -> str:
|
|
self.executed.append((query, args))
|
|
return "OK"
|
|
|
|
conn = FakeConn()
|
|
sess = InProcSession(
|
|
session_id="00000000-0000-0000-0000-00000000feed",
|
|
case_id="00000000-0000-0000-0000-00000000ca5e",
|
|
learner_id="00000000-0000-0000-0000-000000000101",
|
|
persona_code=P1.code,
|
|
theory_mode="humanistic",
|
|
persona=P1,
|
|
state=state_machine.SessionState(),
|
|
session_no=3,
|
|
turns=[
|
|
TurnRecord(
|
|
turn_seq=1,
|
|
speaker="client",
|
|
stage="라포",
|
|
text="저는 김서연입니다.",
|
|
text_masked="저는 [NAME]입니다.",
|
|
turn_id="00000000-0000-0000-0000-000000000201",
|
|
)
|
|
],
|
|
)
|
|
|
|
await session_persistence._upsert_pinned_fact_candidates(conn, sess)
|
|
|
|
pinned_writes = [
|
|
args
|
|
for query, args in conn.executed
|
|
if "INSERT INTO app.pinned_fact (" in query
|
|
]
|
|
history_writes = [
|
|
args for query, args in conn.executed if "INSERT INTO app.pinned_fact_history" in query
|
|
]
|
|
self.assertEqual(len(pinned_writes), 1)
|
|
self.assertEqual(history_writes, [])
|
|
|
|
async def test_pinned_fact_history_records_value_change_as_clarification(self) -> None:
|
|
class FakeConn:
|
|
def __init__(self) -> None:
|
|
self.executed: list[tuple[str, tuple[object, ...]]] = []
|
|
|
|
async def fetchrow(self, query: str, *args: object):
|
|
self.executed.append((query, args))
|
|
return {
|
|
"id": "00000000-0000-0000-0000-00000000fa22",
|
|
"old_value": "예전에는 격주 상담 약속을 말함.",
|
|
"new_value": args[2],
|
|
}
|
|
|
|
async def execute(self, query: str, *args: object) -> str:
|
|
self.executed.append((query, args))
|
|
return "OK"
|
|
|
|
conn = FakeConn()
|
|
sess = InProcSession(
|
|
session_id="00000000-0000-0000-0000-00000000feed",
|
|
case_id="00000000-0000-0000-0000-00000000ca5e",
|
|
learner_id="00000000-0000-0000-0000-000000000101",
|
|
persona_code=P1.code,
|
|
theory_mode="humanistic",
|
|
persona=P1,
|
|
state=state_machine.SessionState(),
|
|
session_no=4,
|
|
turns=[
|
|
TurnRecord(
|
|
turn_seq=1,
|
|
speaker="client",
|
|
stage="라포",
|
|
text="앞으로는 주 1회 상담 약속을 지키고 싶어요.",
|
|
text_masked="앞으로는 주 1회 상담 약속을 지키고 싶어요.",
|
|
turn_id="00000000-0000-0000-0000-000000000202",
|
|
)
|
|
],
|
|
)
|
|
|
|
await session_persistence._upsert_pinned_fact_candidates(conn, sess)
|
|
|
|
history_writes = [
|
|
args for query, args in conn.executed if "INSERT INTO app.pinned_fact_history" in query
|
|
]
|
|
self.assertEqual(len(history_writes), 1)
|
|
self.assertEqual(history_writes[0][2], "예전에는 격주 상담 약속을 말함.")
|
|
self.assertIn("주 1회 상담 약속", str(history_writes[0][3]))
|
|
self.assertEqual(history_writes[0][4], "clarification")
|
|
self.assertEqual(history_writes[0][5], 4)
|
|
|
|
async def test_pinned_fact_history_records_explicit_agreement_withdrawal_as_contradiction(self) -> None:
|
|
test_case = self
|
|
|
|
class FakeConn:
|
|
def __init__(self) -> None:
|
|
self.executed: list[tuple[str, tuple[object, ...]]] = []
|
|
|
|
async def fetchrow(self, query: str, *args: object):
|
|
self.executed.append((query, args))
|
|
test_case.assertIn("UPDATE app.pinned_fact", query)
|
|
test_case.assertIn("status = 'contradicted'", query)
|
|
return {
|
|
"id": "00000000-0000-0000-0000-00000000fa33",
|
|
"old_value": "주 1회 상담 약속은 지키고 싶어요.",
|
|
"new_value": args[2],
|
|
}
|
|
|
|
async def execute(self, query: str, *args: object) -> str:
|
|
self.executed.append((query, args))
|
|
return "OK"
|
|
|
|
conn = FakeConn()
|
|
sess = InProcSession(
|
|
session_id="00000000-0000-0000-0000-00000000feed",
|
|
case_id="00000000-0000-0000-0000-00000000ca5e",
|
|
learner_id="00000000-0000-0000-0000-000000000101",
|
|
persona_code=P1.code,
|
|
theory_mode="humanistic",
|
|
persona=P1,
|
|
state=state_machine.SessionState(),
|
|
session_no=5,
|
|
turns=[
|
|
TurnRecord(
|
|
turn_seq=1,
|
|
speaker="client",
|
|
stage="라포",
|
|
text="상담 약속은 이제 못 지키겠어요. 회기는 그만하고 싶어요.",
|
|
text_masked="상담 약속은 이제 못 지키겠어요. 회기는 그만하고 싶어요.",
|
|
turn_id="00000000-0000-0000-0000-000000000203",
|
|
)
|
|
],
|
|
)
|
|
|
|
await session_persistence._upsert_pinned_fact_candidates(conn, sess)
|
|
|
|
contradiction_updates = [
|
|
args for query, args in conn.executed if "UPDATE app.pinned_fact" in query
|
|
]
|
|
self.assertEqual(len(contradiction_updates), 1)
|
|
self.assertEqual(contradiction_updates[0][1], "agreement:counseling")
|
|
self.assertIn("그만하고 싶어요", str(contradiction_updates[0][2]))
|
|
self.assertEqual(contradiction_updates[0][7], ["evaluator"])
|
|
history_writes = [
|
|
args for query, args in conn.executed if "INSERT INTO app.pinned_fact_history" in query
|
|
]
|
|
self.assertEqual(len(history_writes), 1)
|
|
self.assertEqual(history_writes[0][2], "주 1회 상담 약속은 지키고 싶어요.")
|
|
self.assertIn("못 지키겠어요", str(history_writes[0][3]))
|
|
self.assertEqual(history_writes[0][4], "contradiction")
|
|
self.assertEqual(history_writes[0][5], 5)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|