Improve tasks and backdate insights

This commit is contained in:
2026-03-03 17:21:06 +00:00
parent 9c14e51b43
commit 2898d9e832
18 changed files with 1617 additions and 264 deletions

View File

@@ -3,7 +3,12 @@ from __future__ import annotations
from django.test import TestCase
from django.urls import reverse
from core.models import ContactAvailabilitySettings, User
from core.models import (
ContactAvailabilityEvent,
ContactAvailabilitySettings,
Person,
User,
)
class AvailabilitySettingsPageTests(TestCase):
@@ -36,3 +41,36 @@ class AvailabilitySettingsPageTests(TestCase):
self.assertTrue(row.inference_enabled)
self.assertEqual(120, row.retention_days)
self.assertEqual(300, row.fade_threshold_seconds)
def test_contact_event_stats_are_aggregated(self):
person = Person.objects.create(user=self.user, name="Alice")
ContactAvailabilityEvent.objects.create(
user=self.user,
person=person,
service="whatsapp",
source_kind="message_in",
availability_state="available",
confidence=0.9,
ts=1000,
payload={},
)
ContactAvailabilityEvent.objects.create(
user=self.user,
person=person,
service="whatsapp",
source_kind="inferred_timeout",
availability_state="fading",
confidence=0.5,
ts=2000,
payload={},
)
response = self.client.get(reverse("availability_settings"))
self.assertEqual(200, response.status_code)
stats = list(response.context["contact_stats"])
self.assertEqual(1, len(stats))
self.assertEqual("Alice", stats[0]["person__name"])
self.assertEqual(2, stats[0]["total_events"])
self.assertEqual(1, stats[0]["available_events"])
self.assertEqual(1, stats[0]["fading_events"])
self.assertEqual(1, stats[0]["message_activity_events"])
self.assertEqual(1, stats[0]["inferred_timeout_events"])

View File

@@ -0,0 +1,131 @@
from __future__ import annotations
from django.core.management import call_command
from django.test import TestCase
from core.models import (
ChatSession,
Message,
Person,
PersonIdentifier,
User,
WorkspaceMetricSnapshot,
)
from core.views.workspace import _conversation_for_person
class ReconcileWorkspaceMetricHistoryCommandTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
"reconcile-user",
"reconcile@example.com",
"x",
)
self.person = Person.objects.create(user=self.user, name="Reconcile Person")
self.identifier = PersonIdentifier.objects.create(
user=self.user,
person=self.person,
service="whatsapp",
identifier="15551230000@s.whatsapp.net",
)
self.session = ChatSession.objects.create(user=self.user, identifier=self.identifier)
base_ts = 1_700_000_000_000
for idx in range(10):
inbound = idx % 2 == 0
Message.objects.create(
user=self.user,
session=self.session,
ts=base_ts + (idx * 60_000),
text=f"m{idx}",
source_service="whatsapp",
source_chat_id="120363402761690215@g.us",
sender_uuid=f"actor-{idx}@s.whatsapp.net",
custom_author="OTHER" if inbound else "USER",
)
def test_reconcile_builds_checkpoints_and_no_reset_is_idempotent(self):
call_command(
"reconcile_workspace_metric_history",
"--person-id",
str(self.person.id),
"--service",
"whatsapp",
"--days",
"36500",
"--step-messages",
"2",
)
conversation = _conversation_for_person(self.user, self.person)
first_count = WorkspaceMetricSnapshot.objects.filter(
conversation=conversation
).count()
self.assertGreaterEqual(first_count, 5)
call_command(
"reconcile_workspace_metric_history",
"--person-id",
str(self.person.id),
"--service",
"whatsapp",
"--days",
"36500",
"--step-messages",
"2",
"--no-reset",
)
second_count = WorkspaceMetricSnapshot.objects.filter(
conversation=conversation
).count()
self.assertEqual(first_count, second_count)
latest = conversation.metric_snapshots.first()
self.assertEqual(5, int(latest.inbound_messages or 0))
self.assertEqual(5, int(latest.outbound_messages or 0))
def test_no_reset_does_not_duplicate_when_messages_share_timestamp(self):
Message.objects.all().delete()
base_ts = 1_700_100_000_000
for idx in range(8):
inbound = idx % 2 == 0
Message.objects.create(
user=self.user,
session=self.session,
ts=base_ts + ((idx // 2) * 60_000),
text=f"d{idx}",
source_service="whatsapp",
source_chat_id="120363402761690215@g.us",
sender_uuid=f"dup-{idx}@s.whatsapp.net",
custom_author="OTHER" if inbound else "USER",
)
call_command(
"reconcile_workspace_metric_history",
"--person-id",
str(self.person.id),
"--service",
"whatsapp",
"--days",
"36500",
"--step-messages",
"2",
)
conversation = _conversation_for_person(self.user, self.person)
first_count = WorkspaceMetricSnapshot.objects.filter(
conversation=conversation
).count()
call_command(
"reconcile_workspace_metric_history",
"--person-id",
str(self.person.id),
"--service",
"whatsapp",
"--days",
"36500",
"--step-messages",
"2",
"--no-reset",
)
second_count = WorkspaceMetricSnapshot.objects.filter(
conversation=conversation
).count()
self.assertEqual(first_count, second_count)

View File

@@ -0,0 +1,113 @@
from __future__ import annotations
from django.test import TestCase
from django.urls import reverse
from core.models import ChatTaskSource, 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)
def test_tasks_hub_requires_group_scope_for_project_create(self):
create_response = self.client.post(
reverse("tasks_hub"),
{
"action": "project_create",
"name": "Ops",
},
follow=True,
)
self.assertEqual(200, create_response.status_code)
self.assertFalse(TaskProject.objects.filter(user=self.user, name="Ops").exists())
def test_tasks_hub_can_create_scoped_project_and_delete(self):
create_response = self.client.post(
reverse("tasks_hub"),
{
"action": "project_create",
"name": "Ops",
"service": "whatsapp",
"channel_identifier": "120363402761690215@g.us",
},
follow=True,
)
self.assertEqual(200, create_response.status_code)
project = TaskProject.objects.get(user=self.user, name="Ops")
self.assertIsNotNone(project)
self.assertTrue(
ChatTaskSource.objects.filter(
user=self.user,
service="whatsapp",
channel_identifier="120363402761690215@g.us",
project=project,
).exists()
)
delete_response = self.client.post(
reverse("tasks_hub"),
{
"action": "project_delete",
"project_id": str(project.id),
},
follow=True,
)
self.assertEqual(200, delete_response.status_code)
self.assertFalse(TaskProject.objects.filter(user=self.user, name="Ops").exists())
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_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()
)

View File

@@ -10,6 +10,7 @@ from core.models import (
ChatSession,
ChatTaskSource,
DerivedTask,
ExternalChatLink,
Message,
Person,
PersonIdentifier,
@@ -203,3 +204,67 @@ class TaskSettingsViewActionsTests(TestCase):
self.assertFalse(
ChatTaskSource.objects.filter(id=self.source.id, user=self.user).exists()
)
class TaskSettingsExternalChatLinkScopeTests(TestCase):
def setUp(self):
self.user = User.objects.create_user("task-link-user", "task-link@example.com", "x")
self.client.force_login(self.user)
self.group_person = Person.objects.create(user=self.user, name="Scoped Group")
self.group_identifier = PersonIdentifier.objects.create(
user=self.user,
person=self.group_person,
service="whatsapp",
identifier="120363402761690215@g.us",
)
self.group_identifier_bare = PersonIdentifier.objects.create(
user=self.user,
person=self.group_person,
service="whatsapp",
identifier="120363402761690215",
)
self.other_person = Person.objects.create(user=self.user, name="Other Group")
self.other_identifier = PersonIdentifier.objects.create(
user=self.user,
person=self.other_person,
service="whatsapp",
identifier="120399999999999999@g.us",
)
def test_scoped_settings_limits_contact_identifier_options(self):
response = self.client.get(
reverse("tasks_settings"),
{
"service": "whatsapp",
"identifier": "120363402761690215@g.us",
},
)
self.assertEqual(200, response.status_code)
options = list(response.context["external_link_person_identifiers"])
self.assertTrue(any(row.id == self.group_identifier.id for row in options))
self.assertTrue(any(row.id == self.group_identifier_bare.id for row in options))
self.assertFalse(any(row.id == self.other_identifier.id for row in options))
self.assertTrue(bool(response.context["external_link_scoped"]))
def test_scoped_upsert_rejects_out_of_scope_identifier(self):
response = self.client.post(
reverse("tasks_settings"),
{
"action": "external_chat_link_upsert",
"provider": "codex_cli",
"person_identifier_id": str(self.other_identifier.id),
"external_chat_id": "codex-chat-abc",
"enabled": "1",
"prefill_service": "whatsapp",
"prefill_identifier": "120363402761690215@g.us",
},
follow=True,
)
self.assertEqual(200, response.status_code)
self.assertFalse(
ExternalChatLink.objects.filter(
user=self.user,
provider="codex_cli",
external_chat_id="codex-chat-abc",
).exists()
)