from __future__ import annotations from unittest.mock import patch from django.test import TestCase from django.urls import reverse from core.models import ContactAvailabilitySettings, ChatSession, ConversationEvent, Person, PersonIdentifier, User class AvailabilitySettingsPageTests(TestCase): def setUp(self): self.user = User.objects.create_user("avail-user", "avail@example.com", "x") self.client.force_login(self.user) def test_get_page_renders(self): response = self.client.get(reverse("behavioral_signals_settings")) self.assertEqual(200, response.status_code) self.assertContains(response, "Behavioral Signals") def test_post_updates_settings(self): response = self.client.post( reverse("behavioral_signals_settings"), { "enabled": "1", "show_in_chat": "1", "show_in_groups": "0", "inference_enabled": "1", "retention_days": "120", "fade_threshold_seconds": "300", }, ) self.assertEqual(200, response.status_code) row = ContactAvailabilitySettings.objects.get(user=self.user) self.assertTrue(row.enabled) self.assertTrue(row.show_in_chat) self.assertFalse(row.show_in_groups) self.assertTrue(row.inference_enabled) self.assertEqual(120, row.retention_days) self.assertEqual(300, row.fade_threshold_seconds) @patch("core.views.availability.get_behavioral_availability_stats") def test_behavioral_manticore_stats_are_in_context(self, mocked_stats): person = Person.objects.create(user=self.user, name="Alice") mocked_stats.return_value = [ { "person_id": str(person.id), "transport": "whatsapp", "total_events": 9, "presence_events": 2, "read_events": 3, "typing_events": 2, "message_events": 1, "abandoned_events": 1, "last_event_ts": 5555, } ] response = self.client.get(reverse("behavioral_signals_settings")) self.assertEqual(200, response.status_code) stats = list(response.context["behavioral_stats"]) self.assertEqual(1, len(stats)) self.assertEqual("Alice", stats[0]["person_name"]) self.assertEqual(1, stats[0]["abandoned_events"]) self.assertEqual("manticore", response.context["behavioral_stats_source"]) self.assertContains(response, "Behavioral Event Statistics") self.assertNotIn("contact_stats", response.context) self.assertNotIn("parity_rows", response.context) @patch("core.views.availability.get_behavioral_availability_stats") def test_behavioral_stats_fallback_to_conversation_event_shadow(self, mocked_stats): person = Person.objects.create(user=self.user, name="Alice") identifier = PersonIdentifier.objects.create( user=self.user, person=person, service="signal", identifier="+15551230000", ) session = ChatSession.objects.create(user=self.user, identifier=identifier) ConversationEvent.objects.create( user=self.user, session=session, ts=1234, event_type="presence_available", direction="system", origin_transport="signal", ) mocked_stats.return_value = [] response = self.client.get(reverse("behavioral_signals_settings")) self.assertEqual(200, response.status_code) stats = list(response.context["behavioral_stats"]) self.assertEqual(1, len(stats)) self.assertEqual("Alice", stats[0]["person_name"]) self.assertEqual(1, int(stats[0]["presence_events"])) self.assertEqual( "conversation_event_shadow", response.context["behavioral_stats_source"] ) def test_legacy_availability_url_redirects(self): response = self.client.get(reverse("availability_settings")) self.assertEqual(302, response.status_code) self.assertIn( reverse("behavioral_signals_settings"), str(response.headers.get("Location") or ""), )