from __future__ import annotations import datetime from unittest.mock import AsyncMock, patch from asgiref.sync import async_to_sync from django.test import TestCase, override_settings from core.models import ( ChatSession, ChatTaskSource, DerivedTask, DerivedTaskEvent, Message, Person, PersonIdentifier, TaskProject, User, ) from core.tasks.engine import ( _parse_assignee, _parse_due_date, process_inbound_task_intelligence, ) class DueDateParsingTests(TestCase): def test_parses_due_iso_date(self): result = _parse_due_date("due 2026-04-15") self.assertEqual(datetime.date(2026, 4, 15), result) def test_parses_by_iso_date(self): result = _parse_due_date("please finish by 2026-04-15") self.assertEqual(datetime.date(2026, 4, 15), result) def test_returns_none_for_no_date(self): self.assertIsNone(_parse_due_date("just a task description")) def test_parses_due_today(self): result = _parse_due_date("due today") self.assertEqual(datetime.date.today(), result) def test_parses_due_tomorrow(self): result = _parse_due_date("by tomorrow") self.assertEqual(datetime.date.today() + datetime.timedelta(days=1), result) def test_parses_weekday_name(self): result = _parse_due_date("by friday") self.assertIsNotNone(result) self.assertEqual(4, result.weekday()) # Friday = 4 def test_case_insensitive(self): result = _parse_due_date("Due Today") self.assertEqual(datetime.date.today(), result) class AssigneeParsingTests(TestCase): def test_parses_at_mention(self): result = _parse_assignee("@alice please review this") self.assertEqual("alice", result) def test_parses_assign_to(self): result = _parse_assignee("assign to bob") self.assertEqual("bob", result) def test_parses_for_person(self): result = _parse_assignee("for charlie to fix by friday") self.assertEqual("charlie", result) def test_returns_empty_string_when_no_assignee(self): result = _parse_assignee("no one mentioned") self.assertEqual("", result) def test_prefers_at_mention(self): result = _parse_assignee("@dave assign to someone") self.assertEqual("dave", result) @override_settings(TASK_DERIVATION_USE_AI=False) class TaskEnginePlan09Tests(TestCase): def setUp(self): self.user = User.objects.create_user("plan09-user", "plan09@example.com", "x") self.person = Person.objects.create(user=self.user, name="Plan09 Person") self.identifier = PersonIdentifier.objects.create( user=self.user, person=self.person, service="signal", identifier="+15559001234", ) self.session = ChatSession.objects.create( user=self.user, identifier=self.identifier ) self.project = TaskProject.objects.create(user=self.user, name="Plan09 Project") ChatTaskSource.objects.create( user=self.user, service="signal", channel_identifier="+15559001234", project=self.project, enabled=True, ) def _msg(self, text: str, ts: int = 1000): return Message.objects.create( user=self.user, session=self.session, sender_uuid="peer", text=text, ts=ts, source_service="signal", source_chat_id="+15559001234", ) def test_due_date_stored_in_model_field(self): m = self._msg("task: update SSL cert by 2026-04-15") async_to_sync(process_inbound_task_intelligence)(m) task = DerivedTask.objects.get(origin_message=m) self.assertEqual(datetime.date(2026, 4, 15), task.due_date) def test_assignee_stored_in_model_field(self): m = self._msg("task: review PR @alice") async_to_sync(process_inbound_task_intelligence)(m) task = DerivedTask.objects.get(origin_message=m) self.assertEqual("alice", task.assignee_identifier) def test_task_without_due_date_has_null_due_date(self): m = self._msg("task: plain task no date") async_to_sync(process_inbound_task_intelligence)(m) task = DerivedTask.objects.get(origin_message=m) self.assertIsNone(task.due_date) @patch("core.tasks.engine.send_message_raw", new_callable=AsyncMock) def test_dot_task_list_command(self, mocked_send): seed = self._msg("task: fix the database issue", ts=1001) async_to_sync(process_inbound_task_intelligence)(seed) cmd = self._msg(".task list", ts=1002) async_to_sync(process_inbound_task_intelligence)(cmd) payloads = [ str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list ] self.assertTrue(any("open tasks" in row.lower() for row in payloads)) @patch("core.tasks.engine.send_message_raw", new_callable=AsyncMock) def test_dot_task_show_displays_task_detail(self, mocked_send): seed = self._msg("task: deploy new version", ts=1003) async_to_sync(process_inbound_task_intelligence)(seed) task = DerivedTask.objects.get(origin_message=seed) cmd = self._msg(f".task show #{task.reference_code}", ts=1004) async_to_sync(process_inbound_task_intelligence)(cmd) payloads = [ str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list ] self.assertTrue(any("deploy new version" in row.lower() for row in payloads)) self.assertTrue(any(str(task.reference_code) in row for row in payloads)) @patch("core.tasks.engine.send_message_raw", new_callable=AsyncMock) def test_dot_task_complete_marks_task_done(self, mocked_send): seed = self._msg("task: restart services", ts=1005) async_to_sync(process_inbound_task_intelligence)(seed) task = DerivedTask.objects.get(origin_message=seed) cmd = self._msg(f".task complete #{task.reference_code}", ts=1006) async_to_sync(process_inbound_task_intelligence)(cmd) task.refresh_from_db() self.assertEqual("completed", task.status_snapshot) self.assertTrue( DerivedTaskEvent.objects.filter( task=task, event_type="completion_marked" ).exists() ) payloads = [ str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list ] self.assertTrue(any("completed" in row.lower() for row in payloads)) def test_dot_task_complete_creates_audit_event(self): seed = self._msg("task: patch kernel", ts=1007) async_to_sync(process_inbound_task_intelligence)(seed) task = DerivedTask.objects.get(origin_message=seed) with patch("core.tasks.engine.send_message_raw", new_callable=AsyncMock): cmd = self._msg(f".task complete #{task.reference_code}", ts=1008) async_to_sync(process_inbound_task_intelligence)(cmd) event = DerivedTaskEvent.objects.filter( task=task, event_type="completion_marked" ).first() self.assertIsNotNone(event) self.assertIn("command", str(event.payload or {}).lower()) @override_settings(TASK_DERIVATION_USE_AI=False) class TaskEngineMemoryContextTests(TestCase): def setUp(self): self.user = User.objects.create_user("mem-ctx-user", "mem-ctx@example.com", "x") self.person = Person.objects.create(user=self.user, name="Mem Ctx Person") self.identifier = PersonIdentifier.objects.create( user=self.user, person=self.person, service="whatsapp", identifier="447700900001@s.whatsapp.net", ) self.session = ChatSession.objects.create( user=self.user, identifier=self.identifier ) self.project = TaskProject.objects.create(user=self.user, name="Mem Project") ChatTaskSource.objects.create( user=self.user, service="whatsapp", channel_identifier="447700900001@s.whatsapp.net", project=self.project, enabled=True, ) def _msg(self, text: str, ts: int = 2000): return Message.objects.create( user=self.user, session=self.session, sender_uuid="peer", text=text, ts=ts, source_service="whatsapp", source_chat_id="447700900001@s.whatsapp.net", ) def test_task_creation_invokes_memory_retrieval(self): m = self._msg("task: deploy production release") with patch( "core.tasks.engine.retrieve_memories_for_prompt", return_value=[] ) as mock_retrieve: async_to_sync(process_inbound_task_intelligence)(m) self.assertTrue(mock_retrieve.called) def test_memory_context_included_in_sync_event_payload(self): from core.models import CodexRun m = self._msg("task: fix authentication bug", ts=2001) fake_memory = [ { "id": "mem-1", "memory_kind": "fact", "content": {"text": "prefers short summaries"}, } ] with patch( "core.tasks.engine.retrieve_memories_for_prompt", return_value=fake_memory ): async_to_sync(process_inbound_task_intelligence)(m) task = DerivedTask.objects.filter(origin_message=m).first() self.assertIsNotNone(task) run = CodexRun.objects.filter(task=task).order_by("-created_at").first() self.assertIsNotNone(run, "Expected CodexRun created for task") provider_payload = (run.request_payload or {}).get("provider_payload") or {} memory_context = provider_payload.get("memory_context") self.assertIsNotNone( memory_context, "Expected memory_context in CodexRun provider payload" ) self.assertEqual(1, len(memory_context))