from __future__ import annotations from django.test import TestCase from core.models import ( ContactAvailabilityEvent, ContactAvailabilitySettings, ContactAvailabilitySpan, Person, PersonIdentifier, User, ) from core.presence.engine import ( AvailabilitySignal, ensure_fading_state, record_inferred_signal, record_native_signal, ) from core.presence.inference import now_ms class PresenceEngineTests(TestCase): def setUp(self): self.user = User.objects.create_user( "presence-user", "presence@example.com", "x" ) self.person = Person.objects.create(user=self.user, name="Presence Person") self.identifier = PersonIdentifier.objects.create( user=self.user, person=self.person, service="signal", identifier="+15550001111", ) ContactAvailabilitySettings.objects.update_or_create( user=self.user, defaults={ "enabled": True, "show_in_chat": True, "show_in_groups": True, "inference_enabled": True, "retention_days": 90, "fade_threshold_seconds": 1, }, ) def test_read_receipt_signal_creates_available_event(self): ts = now_ms() event = record_native_signal( AvailabilitySignal( user=self.user, person=self.person, person_identifier=self.identifier, service="signal", source_kind="read_receipt", availability_state="available", confidence=0.95, ts=ts, payload={"origin": "test"}, ) ) self.assertIsNotNone(event) self.assertEqual( 1, ContactAvailabilityEvent.objects.filter(user=self.user).count() ) self.assertEqual("available", event.availability_state) def test_inactivity_transitions_to_fading(self): base_ts = now_ms() record_inferred_signal( AvailabilitySignal( user=self.user, person=self.person, person_identifier=self.identifier, service="signal", source_kind="read_receipt", availability_state="available", confidence=0.95, ts=base_ts, ) ) fade_event = ensure_fading_state( user=self.user, person=self.person, person_identifier=self.identifier, service="signal", at_ts=base_ts + 10_000, ) self.assertIsNotNone(fade_event) self.assertEqual("fading", fade_event.availability_state) def test_explicit_unavailable_blocks_fade_inference(self): base_ts = now_ms() record_native_signal( AvailabilitySignal( user=self.user, person=self.person, person_identifier=self.identifier, service="xmpp", source_kind="native_presence", availability_state="unavailable", confidence=1.0, ts=base_ts, ) ) fade_event = ensure_fading_state( user=self.user, person=self.person, person_identifier=self.identifier, service="xmpp", at_ts=base_ts + 60_000, ) self.assertIsNone(fade_event) self.assertEqual( 1, ContactAvailabilityEvent.objects.filter(user=self.user).count() ) def test_adjacent_same_state_events_extend_single_span(self): ts0 = now_ms() record_native_signal( AvailabilitySignal( user=self.user, person=self.person, person_identifier=self.identifier, service="signal", source_kind="typing_start", availability_state="available", confidence=0.9, ts=ts0, ) ) record_native_signal( AvailabilitySignal( user=self.user, person=self.person, person_identifier=self.identifier, service="signal", source_kind="message_in", availability_state="available", confidence=0.8, ts=ts0 + 5_000, ) ) spans = list( ContactAvailabilitySpan.objects.filter(user=self.user).order_by("start_ts") ) self.assertEqual(1, len(spans)) self.assertEqual(ts0, spans[0].start_ts) self.assertEqual(ts0 + 5_000, spans[0].end_ts)