from __future__ import annotations from unittest.mock import AsyncMock, patch from django.test import TestCase from django.urls import reverse from core.models import ( ChatSession, ChatTaskSource, DerivedTask, DerivedTaskEvent, Message, Person, PersonIdentifier, TaskEpic, TaskProject, User, ) class TasksPagesManagementTests(TestCase): def setUp(self): self.user = User.objects.create_user( "tasks-pages-user", "tasks-pages@example.com", "x" ) self.client.force_login(self.user) self.person = Person.objects.create(user=self.user, name="Scope Person") self.pid_whatsapp = PersonIdentifier.objects.create( user=self.user, person=self.person, service="whatsapp", identifier="120363402761690215@g.us", ) self.pid_signal = PersonIdentifier.objects.create( user=self.user, person=self.person, service="signal", identifier="+15551230000", ) def test_tasks_hub_can_create_project_name_only(self): response = self.client.post( reverse("tasks_hub"), {"action": "project_create", "name": "Ops"}, follow=True, ) self.assertEqual(200, response.status_code) project = TaskProject.objects.get(user=self.user, name="Ops") self.assertIsNotNone(project) self.assertFalse( ChatTaskSource.objects.filter(user=self.user, project=project).exists() ) def test_tasks_hub_can_map_identifier_to_selected_project(self): project = TaskProject.objects.create(user=self.user, name="Mapped") response = self.client.post( reverse("tasks_hub"), { "action": "project_map_identifier", "project_id": str(project.id), "person_identifier_id": str(self.pid_signal.id), "person": str(self.person.id), "service": "whatsapp", "identifier": "120363402761690215@g.us", }, follow=True, ) self.assertEqual(200, response.status_code) self.assertTrue( ChatTaskSource.objects.filter( user=self.user, project=project, service="signal", channel_identifier="+15551230000", enabled=True, ).exists() ) def test_tasks_hub_settings_link_preserves_scope_context(self): response = self.client.get( f"{reverse('tasks_hub')}?person={self.person.id}&service=signal&identifier=147e75bd-91b7-4014-b9e5-12a44b978f7b" ) self.assertEqual(200, response.status_code) self.assertContains( response, f"{reverse('tasks_settings')}?person={self.person.id}&service=signal&identifier=147e75bd-91b7-4014-b9e5-12a44b978f7b", ) def test_project_page_can_create_and_delete_epic(self): project = TaskProject.objects.create(user=self.user, name="Roadmap") create_response = self.client.post( reverse("tasks_project", kwargs={"project_id": str(project.id)}), { "action": "epic_create", "name": "Phase 1", }, follow=True, ) self.assertEqual(200, create_response.status_code) epic = TaskEpic.objects.get(project=project, name="Phase 1") self.assertIsNotNone(epic) delete_response = self.client.post( reverse("tasks_project", kwargs={"project_id": str(project.id)}), { "action": "epic_delete", "epic_id": str(epic.id), }, follow=True, ) self.assertEqual(200, delete_response.status_code) self.assertFalse( TaskEpic.objects.filter(project=project, name="Phase 1").exists() ) def test_project_page_can_assign_and_clear_task_epic(self): project = TaskProject.objects.create(user=self.user, name="Roadmap") epic = TaskEpic.objects.create(project=project, name="Sprint A") session = ChatSession.objects.create(user=self.user, identifier=self.pid_signal) origin = Message.objects.create( user=self.user, session=session, ts=1_700_000_000_100, text="task: assign epic", sender_uuid="+15551230000", custom_author="OTHER", source_service="signal", source_chat_id="+15551230000", ) task = DerivedTask.objects.create( user=self.user, project=project, title="Assign me", source_service="signal", source_channel="+15551230000", origin_message=origin, reference_code="9", status_snapshot="open", ) assign_response = self.client.post( reverse("tasks_project", kwargs={"project_id": str(project.id)}), { "action": "task_set_epic", "task_id": str(task.id), "epic_id": str(epic.id), }, follow=True, ) self.assertEqual(200, assign_response.status_code) task.refresh_from_db() self.assertEqual(epic.id, task.epic_id) clear_response = self.client.post( reverse("tasks_project", kwargs={"project_id": str(project.id)}), { "action": "task_set_epic", "task_id": str(task.id), "epic_id": "", }, follow=True, ) self.assertEqual(200, clear_response.status_code) task.refresh_from_db() self.assertIsNone(task.epic_id) @patch("core.views.tasks.send_message_raw", new_callable=AsyncMock) def test_project_epic_create_announces_to_project_chats(self, mocked_send): project = TaskProject.objects.create(user=self.user, name="Roadmap") ChatTaskSource.objects.create( user=self.user, service="whatsapp", channel_identifier="120363402761690215@g.us", project=project, enabled=True, ) response = self.client.post( reverse("tasks_project", kwargs={"project_id": str(project.id)}), { "action": "epic_create", "name": "Phase 2", }, follow=True, ) self.assertEqual(200, response.status_code) self.assertTrue( TaskEpic.objects.filter(project=project, name="Phase 2").exists() ) self.assertTrue(mocked_send.await_count >= 1) payloads = [ str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list ] self.assertTrue(any("whatsapp usage" in row.lower() for row in payloads)) self.assertTrue(any("add task to epic" in row.lower() for row in payloads)) def test_group_page_create_and_map_project(self): response = self.client.post( reverse( "tasks_group", kwargs={ "service": "whatsapp", "identifier": "120363402761690215@g.us", }, ), { "action": "group_project_create", "project_name": "Group Project", "epic_name": "Sprint A", }, follow=True, ) self.assertEqual(200, response.status_code) project = TaskProject.objects.get(user=self.user, name="Group Project") epic = TaskEpic.objects.get(project=project, name="Sprint A") self.assertTrue( ChatTaskSource.objects.filter( user=self.user, service="whatsapp", channel_identifier="120363402761690215@g.us", project=project, epic=epic, enabled=True, ).exists() ) def test_tasks_hub_shows_human_creator_label(self): project = TaskProject.objects.create(user=self.user, name="Creator Test") session = ChatSession.objects.create(user=self.user, identifier=self.pid_signal) origin = Message.objects.create( user=self.user, session=session, ts=1_700_000_000_000, text="task: write docs", sender_uuid="+15551230000", custom_author="OTHER", source_service="signal", source_chat_id="+15551230000", ) DerivedTask.objects.create( user=self.user, project=project, title="Write docs", source_service="signal", source_channel="+15551230000", origin_message=origin, reference_code="1", status_snapshot="open", ) response = self.client.get(reverse("tasks_hub")) self.assertEqual(200, response.status_code) self.assertContains(response, "Scope Person") def test_project_page_creator_column_links_to_compose(self): project = TaskProject.objects.create(user=self.user, name="Creator Link Test") session = ChatSession.objects.create(user=self.user, identifier=self.pid_signal) origin = Message.objects.create( user=self.user, session=session, ts=1_700_000_000_111, text="task: creator link", sender_uuid="+15551230000", custom_author="OTHER", source_service="signal", source_chat_id="+15551230000", ) DerivedTask.objects.create( user=self.user, project=project, title="Creator link task", source_service="signal", source_channel="+15551230000", origin_message=origin, reference_code="2", status_snapshot="open", ) response = self.client.get( reverse("tasks_project", kwargs={"project_id": str(project.id)}) ) self.assertEqual(200, response.status_code) self.assertContains( response, f'{reverse("compose_page")}?service=signal&identifier=%2B15551230000&person={self.person.id}', ) def test_task_detail_renders_payload_summary_and_json(self): project = TaskProject.objects.create(user=self.user, name="Payload Test") session = ChatSession.objects.create(user=self.user, identifier=self.pid_signal) origin = Message.objects.create( user=self.user, session=session, ts=1_700_000_000_000, text="origin", sender_uuid="+15551230000", custom_author="OTHER", source_service="signal", source_chat_id="+15551230000", ) task = DerivedTask.objects.create( user=self.user, project=project, title="Payload detail", source_service="signal", source_channel="+15551230000", origin_message=origin, reference_code="42", status_snapshot="open", ) DerivedTaskEvent.objects.create( task=task, event_type="reaction_captured", payload={"source": "signal", "emoji": "❤️", "reason": "heart_reaction"}, ) response = self.client.get( reverse("tasks_task", kwargs={"task_id": str(task.id)}) ) self.assertEqual(200, response.status_code) self.assertContains(response, "View payload JSON") self.assertContains(response, "source: signal", html=True) self.assertContains(response, ""emoji": "❤️"")