import unittest from . import turn_runtime from .services import guardrail, live_coach from .services import state_machine class LiveCoachPromptPrivacyTest(unittest.TestCase): def tearDown(self) -> None: guardrail.set_ko_pii_recognizer(None) def test_messages_remask_recent_turns_and_evaluation_payload(self) -> None: class FakeKoRecognizer: def analyze(self, text: str): spans = [] for entity_type, value in ( ("NAME", "보라별"), ("ORG", "미래학교상담연구랩"), ): start = text.find(value) if start >= 0: spans.append(guardrail.PiiEntitySpan(entity_type, start, start + len(value))) return spans guardrail.set_ko_pii_recognizer(FakeKoRecognizer()) item = live_coach.LiveCoachInput( session_id="privacy-live-coach", turn_seq=3, stage="exploration", effective_openness=0.42, theory_mode="humanistic", persona_code="P1", persona_name="서연", learner_text="별명은 보라별이고 기관은 미래학교상담연구랩입니다.", client_reply="전화는 010-1234-5678입니다.", recent_turns=[ { "speaker": "counselor", "text": "보라별이 미래학교상담연구랩에서 힘들다고 했죠.", }, { "speaker": "client", "text": "010-1234-5678로 연락하지 말아주세요.", }, ], evaluation={ "appropriateness": "warn", "appropriateness_note": "보라별 연락처 010-1234-5678을 되물었다.", "nested": {"rationale": "기관 미래학교상담연구랩 언급"}, }, ) messages = live_coach._messages(item, []) user_prompt = messages[1].content for raw in ("보라별", "미래학교상담연구랩", "010-1234-5678"): self.assertNotIn(raw, user_prompt) for masked in ("[NAME]", "[ORG]", "[PHONE]"): self.assertIn(masked, user_prompt) class LiveCoachQuotaRechargeTest(unittest.TestCase): def test_recharge_requires_positive_score_and_client_change(self) -> None: before = state_machine.SessionState( stage=state_machine.Stage.RAPPORT, turn_seq=2, effective_openness=0.15, ) after = state_machine.SessionState( stage=state_machine.Stage.RAPPORT, turn_seq=3, effective_openness=0.19, ) should_recharge, reason = turn_runtime.should_recharge_live_coach_credit( {"appropriateness": "pos", "rapport_signal": 0.45}, before, after, ) self.assertTrue(should_recharge) self.assertIn("개방도", reason) # 성과 없음(개방도 하락) + 페이싱 주기 아님 → 미충전 should_recharge, _ = turn_runtime.should_recharge_live_coach_credit( {"appropriateness": "pos", "rapport_signal": 0.45}, before, state_machine.SessionState( stage=state_machine.Stage.RAPPORT, turn_seq=3, effective_openness=0.14, ), ) self.assertFalse(should_recharge) def test_recharge_allows_neutral_with_strong_rapport_and_gain(self) -> None: before = state_machine.SessionState( stage=state_machine.Stage.RAPPORT, turn_seq=2, effective_openness=0.15, ) after = state_machine.SessionState( stage=state_machine.Stage.RAPPORT, turn_seq=3, effective_openness=0.19, ) should_recharge, reason = turn_runtime.should_recharge_live_coach_credit( {"appropriateness": "neutral", "rapport_signal": 0.8}, before, after, ) self.assertTrue(should_recharge) self.assertIn("라포", reason) def test_recharge_warn_turn_does_not_recharge_off_cycle(self) -> None: before = state_machine.SessionState( stage=state_machine.Stage.RAPPORT, turn_seq=2, effective_openness=0.15, ) after = state_machine.SessionState( stage=state_machine.Stage.RAPPORT, turn_seq=3, effective_openness=0.19, ) should_recharge, _ = turn_runtime.should_recharge_live_coach_credit( {"appropriateness": "warn", "rapport_signal": 0.1}, before, after, ) self.assertFalse(should_recharge) def test_pacing_recharge_every_n_turns_even_without_evaluation(self) -> None: before = state_machine.SessionState( stage=state_machine.Stage.RAPPORT, turn_seq=5, effective_openness=0.15, ) after = state_machine.SessionState( stage=state_machine.Stage.RAPPORT, turn_seq=turn_runtime._LIVE_COACH_PACING_RECHARGE_EVERY_TURNS, effective_openness=0.14, ) should_recharge, reason = turn_runtime.should_recharge_live_coach_credit( None, before, after, ) self.assertTrue(should_recharge) self.assertIn("턴", reason) if __name__ == "__main__": unittest.main()