255 lines
9.8 KiB
Python
255 lines
9.8 KiB
Python
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))
|