vignette/apps/api/app/test_state_machine_resistance.py
2026-08-28 19:04:17 +09:00

152 lines
5.8 KiB
Python

"""Regression tests for the deterministic resistance engine."""
from __future__ import annotations
import unittest
from .services import state_machine
from .services.persona import P1
EMPATHIC_UTTERANCES = [
"얼마나 힘들었는지 마음이 느껴져요. 어떤 순간이 제일 버거웠나요?",
"그런 마음을 꺼내는 것 자체가 쉽지 않았을 것 같아요. 더 말해줘도 괜찮아요.",
"잠도 잘 못 자고 학교도 버거웠다면 하루가 길게 느껴졌겠어요.",
"지금은 해결책보다 그 마음을 천천히 이해하는 게 먼저인 것 같아요.",
"그 시간을 버텨온 마음을 함께 살펴보고 싶어요. 무엇부터 이야기해볼까요?",
]
ADVICE_JUMP_UTTERANCES = [
"그냥 학교는 가야 해요. 노력하면 하면 돼요. 왜 안 하죠?",
"그건 잘못 생각하는 거예요. 원래 다 힘들어요.",
"당연히 엄마 말을 들어야죠. 하지 마세요.",
"내 생각엔 그냥 계획표를 만들면 돼요.",
"그러니까 더 노력해야 해요. 왜 안 바꾸나요?",
]
def _initial_p1_state() -> state_machine.SessionState:
return state_machine.init_state(
params=P1.openness_params(),
)
def _run_curve(utterances: list[str]) -> list[state_machine.SessionState]:
state = _initial_p1_state()
curve: list[state_machine.SessionState] = []
for utterance in utterances:
signal = state_machine.estimate_rapport_signal(utterance)
state = state_machine.evolve(
state,
rapport_signal=signal,
unlock_rate=P1.unlock_rate(),
decay_floor=P1.decay_floor(),
)
curve.append(state)
return curve
class ResistanceEngineTest(unittest.TestCase):
def test_empathy_opens_p1_while_advice_jump_closes_it(self) -> None:
empathy_curve = _run_curve(EMPATHIC_UTTERANCES)
advice_curve = _run_curve(ADVICE_JUMP_UTTERANCES)
empathy_final = empathy_curve[-1]
advice_final = advice_curve[-1]
self.assertGreater(empathy_final.rapport_credit, advice_final.rapport_credit)
self.assertLess(empathy_final.resistance, advice_final.resistance)
self.assertGreater(empathy_final.effective_openness, advice_final.effective_openness)
self.assertEqual(empathy_final.stage, state_machine.Stage.EXPLORE)
self.assertEqual(advice_final.stage, state_machine.Stage.RAPPORT)
self.assertGreater(empathy_final.effective_openness, 0.1)
self.assertEqual(advice_final.effective_openness, 0.0)
def test_advice_jump_never_advances_stage_after_five_turns(self) -> None:
advice_curve = _run_curve(ADVICE_JUMP_UTTERANCES)
self.assertTrue(all(state.stage is state_machine.Stage.RAPPORT for state in advice_curve))
self.assertTrue(all(state.rapport_credit == 0 for state in advice_curve))
self.assertGreaterEqual(advice_curve[-1].resistance, 0.95)
def test_evolve_clamps_existing_ideation_state_over_cap(self) -> None:
state = state_machine.SessionState(ideation_stage=5)
self.assertEqual(state.ideation_stage, state_machine.IDEATION_STAGE_CAP)
# 이전 버전에서 만들어진 비정상 객체가 메모리에 남은 상황도 전이 경계에서
# 다시 정규화되는지 확인한다.
state.ideation_stage = 5
evolved = state_machine.evolve(
state,
rapport_signal=0.0,
unlock_rate=P1.unlock_rate(),
decay_floor=P1.decay_floor(),
)
self.assertEqual(evolved.ideation_stage, state_machine.IDEATION_STAGE_CAP)
self.assertEqual(evolved.snapshot()["ideation_stage"], state_machine.IDEATION_STAGE_CAP)
def test_evolve_clamps_observed_ideation_over_cap(self) -> None:
state = state_machine.SessionState(ideation_stage=1)
evolved = state_machine.evolve(
state,
rapport_signal=0.0,
unlock_rate=P1.unlock_rate(),
decay_floor=P1.decay_floor(),
ideation_observed=5,
)
self.assertEqual(evolved.ideation_stage, state_machine.IDEATION_STAGE_CAP)
def test_init_state_clamps_baseline_and_carry_over_cap(self) -> None:
baseline_over_cap = state_machine.init_state(
params=state_machine.OpennessParams(
base_resistance=0.65,
unlock_rate=0.25,
decay_floor=0.2,
ideation_baseline=5,
)
)
carry_over_cap = state_machine.init_state(
params=P1.openness_params(),
carry={"ideation_stage": 5},
)
self.assertEqual(baseline_over_cap.ideation_stage, state_machine.IDEATION_STAGE_CAP)
self.assertEqual(carry_over_cap.ideation_stage, state_machine.IDEATION_STAGE_CAP)
def test_valid_ideation_values_preserve_monotonic_behavior(self) -> None:
state = state_machine.SessionState(ideation_stage=2)
raised = state_machine.evolve(
state,
rapport_signal=0.0,
unlock_rate=P1.unlock_rate(),
decay_floor=P1.decay_floor(),
ideation_observed=3,
)
unchanged = state_machine.evolve(
state,
rapport_signal=0.0,
unlock_rate=P1.unlock_rate(),
decay_floor=P1.decay_floor(),
ideation_observed=1,
)
initialized = state_machine.init_state(
params=state_machine.OpennessParams(
base_resistance=0.65,
unlock_rate=0.25,
decay_floor=0.2,
ideation_baseline=2,
),
carry={"ideation_stage": 1},
)
self.assertEqual(raised.ideation_stage, 3)
self.assertEqual(unchanged.ideation_stage, 2)
self.assertEqual(initialized.ideation_stage, 2)
if __name__ == "__main__":
unittest.main()