"""Alliance Pulse HTTP boundary tests.""" from __future__ import annotations import unittest from unittest.mock import AsyncMock, patch from uuid import UUID from fastapi import HTTPException from .contracts.measurement import AllianceScores from .deps import Principal, Role from .routes import measurements SESSION_ID = UUID("00000000-0000-0000-0000-00000000a111") PULSE_ID = UUID("00000000-0000-0000-0000-00000000a222") def _principal(role: Role) -> Principal: return Principal( user_id="00000000-0000-0000-0000-00000000a333", role=role, cohort_ids=["e2e-hanshin"], ) class AllianceMeasurementRouteTest(unittest.IsolatedAsyncioTestCase): async def test_create_locks_before_scheduling_agents(self) -> None: order: list[str] = [] async def lock(**_kwargs: object) -> measurements.alliance_measurement.LockedPulseResult: order.append("locked") return measurements.alliance_measurement.LockedPulseResult( pulse_id=PULSE_ID, idempotent_replay=False, ) def schedule(pulse_id: UUID) -> None: self.assertEqual(pulse_id, PULSE_ID) order.append("scheduled") with ( patch.object(measurements.alliance_measurement, "create_locked_pulse", lock), patch.object(measurements.alliance_measurement, "schedule_alliance_agents", schedule), ): response = await measurements.create_alliance_pulse( SESSION_ID, measurements.AlliancePulseCreateRequest( checkpoint="post", scores=AllianceScores(goal=0.4, task=0.6, bond=0.8), ), _principal(Role.LEARNER), ) self.assertEqual(response.pulse_id, PULSE_ID) self.assertFalse(response.idempotent_replay) self.assertEqual(order, ["locked", "scheduled"]) async def test_identical_replay_returns_same_id_without_rescheduling(self) -> None: schedule = unittest.mock.Mock() with ( patch.object( measurements.alliance_measurement, "create_locked_pulse", AsyncMock( return_value=measurements.alliance_measurement.LockedPulseResult( pulse_id=PULSE_ID, idempotent_replay=True, ) ), ), patch.object(measurements.alliance_measurement, "schedule_alliance_agents", schedule), ): response = await measurements.create_alliance_pulse( SESSION_ID, measurements.AlliancePulseCreateRequest( checkpoint="post", scores=AllianceScores(goal=0.4, task=0.6, bond=0.8), ), _principal(Role.LEARNER), ) self.assertEqual(response.pulse_id, PULSE_ID) self.assertTrue(response.idempotent_replay) schedule.assert_not_called() async def test_create_conflict_is_409_and_does_not_schedule(self) -> None: schedule = unittest.mock.Mock() with ( patch.object( measurements.alliance_measurement, "create_locked_pulse", AsyncMock( side_effect=measurements.alliance_measurement.AlliancePulseConflictError( "post alliance pulse is already locked" ) ), ), patch.object(measurements.alliance_measurement, "schedule_alliance_agents", schedule), ): with self.assertRaises(HTTPException) as raised: await measurements.create_alliance_pulse( SESSION_ID, measurements.AlliancePulseCreateRequest( checkpoint="post", scores=AllianceScores(goal=0.4, task=0.6, bond=0.8), ), _principal(Role.LEARNER), ) self.assertEqual(raised.exception.status_code, 409) schedule.assert_not_called() async def test_list_preserves_three_dimensions_and_perspective(self) -> None: service_items = [ { "pulse_id": str(PULSE_ID), "checkpoint": "post", "status": "ready", "learner_locked_at": "2026-08-06T10:00:00+00:00", "revealed_at": "2026-08-06T10:00:01+00:00", "error_code": None, "self_scores": {"goal": 0.4, "task": 0.6, "bond": 0.8}, "measurements": [ { "measurement_id": f"00000000-0000-0000-0000-00000000a22{index}", "dimension": dimension, "perspective": "learner_self_report", "source_kind": "learner_reported", "value": value, "confidence": None, "status": "ready", "error_code": None, "rationale": None, "evidence": [], "created_at": "2026-08-06T10:00:00+00:00", } for index, (dimension, value) in enumerate( (("goal", 0.4), ("task", 0.6), ("bond", 0.8)), start=3, ) ], } ] with patch.object( measurements.alliance_measurement, "list_alliance_pulses", AsyncMock(return_value=service_items), ): response = await measurements.get_alliance_pulses( SESSION_ID, _principal(Role.LEARNER), ) self.assertEqual([item.dimension for item in response.items[0].measurements], ["goal", "task", "bond"]) self.assertTrue( all( item.perspective == "learner_self_report" for item in response.items[0].measurements ) ) async def test_supervisor_state_error_is_422(self) -> None: with patch.object( measurements.alliance_measurement, "add_supervisor_rating", AsyncMock( side_effect=measurements.alliance_measurement.AlliancePulseStateError( "supervisor rating requires transcript evidence" ) ), ): with self.assertRaises(HTTPException) as raised: await measurements.create_supervisor_alliance_rating( SESSION_ID, PULSE_ID, measurements.SupervisorAllianceRatingRequest( scores=AllianceScores(goal=0.5, task=0.5, bond=0.5), evidence_turn_ids=( UUID("00000000-0000-0000-0000-00000000a444"), ), note="근거 장면 확인", ), _principal(Role.TEACHER), ) self.assertEqual(raised.exception.status_code, 422) if __name__ == "__main__": unittest.main()