Begin adding AI memory
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from io import StringIO
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import CommandError
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from core.events.ledger import append_event_sync
|
||||
@@ -45,3 +46,46 @@ class EventLedgerSmokeCommandTests(TestCase):
|
||||
rendered = out.getvalue()
|
||||
self.assertIn("event-ledger-smoke", rendered)
|
||||
self.assertIn("event_type_counts=", rendered)
|
||||
|
||||
def test_smoke_command_validates_required_types(self):
|
||||
append_event_sync(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1770000000001,
|
||||
event_type="message_created",
|
||||
direction="in",
|
||||
origin_transport="signal",
|
||||
origin_message_id="m-required",
|
||||
payload={"message_id": "m-required"},
|
||||
)
|
||||
out = StringIO()
|
||||
call_command(
|
||||
"event_ledger_smoke",
|
||||
user_id=str(self.user.id),
|
||||
minutes=999999,
|
||||
require_types="message_created",
|
||||
stdout=out,
|
||||
)
|
||||
rendered = out.getvalue()
|
||||
self.assertIn("required_types=", rendered)
|
||||
self.assertIn("missing_required_types=[]", rendered)
|
||||
|
||||
def test_smoke_command_errors_when_required_type_missing(self):
|
||||
with self.assertRaises(CommandError):
|
||||
call_command(
|
||||
"event_ledger_smoke",
|
||||
user_id=str(self.user.id),
|
||||
minutes=999999,
|
||||
require_types="reaction_added",
|
||||
stdout=StringIO(),
|
||||
)
|
||||
|
||||
def test_smoke_command_errors_when_empty_and_fail_if_empty(self):
|
||||
with self.assertRaises(CommandError):
|
||||
call_command(
|
||||
"event_ledger_smoke",
|
||||
user_id=str(self.user.id),
|
||||
minutes=1,
|
||||
fail_if_empty=True,
|
||||
stdout=StringIO(),
|
||||
)
|
||||
|
||||
@@ -136,3 +136,24 @@ class EventProjectionShadowTests(TestCase):
|
||||
)
|
||||
rendered = out.getvalue()
|
||||
self.assertIn("shadow compare:", rendered)
|
||||
|
||||
def test_management_command_supports_recent_only_switch(self):
|
||||
Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=int(time.time() * 1000),
|
||||
sender_uuid="+15550000001",
|
||||
text="recent-only",
|
||||
source_service="signal",
|
||||
source_message_id="recent-only-1",
|
||||
)
|
||||
out = StringIO()
|
||||
call_command(
|
||||
"event_projection_shadow",
|
||||
user_id=str(self.user.id),
|
||||
recent_only=True,
|
||||
limit_sessions=5,
|
||||
stdout=out,
|
||||
)
|
||||
rendered = out.getvalue()
|
||||
self.assertIn("shadow compare:", rendered)
|
||||
|
||||
62
core/tests/test_memory_search_commands.py
Normal file
62
core/tests/test_memory_search_commands.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from io import StringIO
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from core.models import AIRequest, MemoryItem, User, WorkspaceConversation
|
||||
|
||||
|
||||
@override_settings(MEMORY_SEARCH_BACKEND="django")
|
||||
class MemorySearchCommandTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="memory-search-user",
|
||||
email="memory-search@example.com",
|
||||
password="pw",
|
||||
)
|
||||
self.conversation = WorkspaceConversation.objects.create(
|
||||
user=self.user,
|
||||
platform_type="signal",
|
||||
title="Memory Search Scope",
|
||||
platform_thread_id="mem-scope-1",
|
||||
)
|
||||
request = AIRequest.objects.create(
|
||||
user=self.user,
|
||||
conversation=self.conversation,
|
||||
window_spec={},
|
||||
operation="memory_propose",
|
||||
)
|
||||
self.item = MemoryItem.objects.create(
|
||||
user=self.user,
|
||||
conversation=self.conversation,
|
||||
memory_kind="fact",
|
||||
status="active",
|
||||
content={"text": "Prefers concise updates with action items."},
|
||||
source_request=request,
|
||||
)
|
||||
|
||||
def test_reindex_command_emits_summary(self):
|
||||
out = StringIO()
|
||||
call_command(
|
||||
"memory_search_reindex",
|
||||
user_id=str(self.user.id),
|
||||
statuses="active",
|
||||
limit=100,
|
||||
stdout=out,
|
||||
)
|
||||
rendered = out.getvalue()
|
||||
self.assertIn("memory-search-reindex", rendered)
|
||||
self.assertIn("indexed=", rendered)
|
||||
|
||||
def test_query_command_returns_hit(self):
|
||||
out = StringIO()
|
||||
call_command(
|
||||
"memory_search_query",
|
||||
user_id=str(self.user.id),
|
||||
query="concise updates",
|
||||
statuses="active",
|
||||
stdout=out,
|
||||
)
|
||||
rendered = out.getvalue()
|
||||
self.assertIn("memory-search-query", rendered)
|
||||
self.assertIn(str(self.item.id), rendered)
|
||||
150
core/tests/test_system_diagnostics_api.py
Normal file
150
core/tests/test_system_diagnostics_api.py
Normal file
@@ -0,0 +1,150 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from core.models import (
|
||||
AIRequest,
|
||||
ChatSession,
|
||||
ConversationEvent,
|
||||
MemoryItem,
|
||||
Person,
|
||||
PersonIdentifier,
|
||||
User,
|
||||
WorkspaceConversation,
|
||||
)
|
||||
|
||||
|
||||
class SystemDiagnosticsAPITests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_superuser(
|
||||
username="sys-diag-admin",
|
||||
email="sys-diag@example.com",
|
||||
password="pw",
|
||||
)
|
||||
person = Person.objects.create(user=self.user, name="System Diagnostics Person")
|
||||
identifier = PersonIdentifier.objects.create(
|
||||
user=self.user,
|
||||
person=person,
|
||||
service="signal",
|
||||
identifier="+15554443333",
|
||||
)
|
||||
self.session = ChatSession.objects.create(user=self.user, identifier=identifier)
|
||||
self.workspace_conversation = WorkspaceConversation.objects.create(
|
||||
user=self.user,
|
||||
platform_type="signal",
|
||||
title="Diag Memory Scope",
|
||||
platform_thread_id=str(self.session.id),
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_event_ledger_smoke_api_returns_counts_and_missing_required(self):
|
||||
ConversationEvent.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1700000000000,
|
||||
event_type="message_created",
|
||||
direction="in",
|
||||
origin_transport="signal",
|
||||
payload={"message_id": "m1"},
|
||||
raw_payload={},
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse("system_event_ledger_smoke"),
|
||||
{
|
||||
"minutes": "999999",
|
||||
"service": "signal",
|
||||
"require_types": "message_created,reaction_added",
|
||||
},
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
payload = response.json()
|
||||
self.assertTrue(payload.get("ok"))
|
||||
self.assertEqual("signal", payload.get("service"))
|
||||
self.assertIn("event_type_counts", payload)
|
||||
self.assertIn("missing_required_types", payload)
|
||||
self.assertIn("reaction_added", payload.get("missing_required_types") or [])
|
||||
|
||||
def test_trace_diagnostics_includes_projection_shadow_links(self):
|
||||
trace_id = "trace-system-diag-1"
|
||||
event = ConversationEvent.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1700000001000,
|
||||
event_type="message_created",
|
||||
direction="in",
|
||||
origin_transport="signal",
|
||||
trace_id=trace_id,
|
||||
payload={"message_id": "m2"},
|
||||
raw_payload={},
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse("system_trace_diagnostics"),
|
||||
{"trace_id": trace_id},
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
payload = response.json()
|
||||
self.assertTrue(payload.get("ok"))
|
||||
self.assertEqual(1, payload.get("count"))
|
||||
self.assertIn(str(self.session.id), payload.get("related_session_ids") or [])
|
||||
urls = payload.get("projection_shadow_urls") or []
|
||||
self.assertTrue(urls)
|
||||
self.assertIn(str(self.session.id), str(urls[0]))
|
||||
events = payload.get("events") or []
|
||||
self.assertEqual(str(event.id), str(events[0].get("id")))
|
||||
self.assertIn(
|
||||
str(self.session.id),
|
||||
str(events[0].get("projection_shadow_url") or ""),
|
||||
)
|
||||
|
||||
def test_memory_search_status_and_query_api(self):
|
||||
request = AIRequest.objects.create(
|
||||
user=self.user,
|
||||
conversation=self.workspace_conversation,
|
||||
window_spec={},
|
||||
operation="memory_propose",
|
||||
)
|
||||
memory = MemoryItem.objects.create(
|
||||
user=self.user,
|
||||
conversation=self.workspace_conversation,
|
||||
memory_kind="fact",
|
||||
status="active",
|
||||
content={"text": "User prefers concise status updates on WhatsApp."},
|
||||
source_request=request,
|
||||
)
|
||||
status_response = self.client.get(reverse("system_memory_search_status"))
|
||||
self.assertEqual(200, status_response.status_code)
|
||||
status_payload = status_response.json()
|
||||
self.assertTrue(status_payload.get("ok"))
|
||||
self.assertIn("status", status_payload)
|
||||
|
||||
query_response = self.client.get(
|
||||
reverse("system_memory_search_query"),
|
||||
{"q": "concise status updates"},
|
||||
)
|
||||
self.assertEqual(200, query_response.status_code)
|
||||
query_payload = query_response.json()
|
||||
self.assertTrue(query_payload.get("ok"))
|
||||
self.assertGreaterEqual(int(query_payload.get("count") or 0), 1)
|
||||
first_hit = (query_payload.get("hits") or [{}])[0]
|
||||
self.assertEqual(str(memory.id), str(first_hit.get("memory_id") or ""))
|
||||
|
||||
def test_system_settings_page_renders_searchable_datalists(self):
|
||||
ConversationEvent.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1700000002000,
|
||||
event_type="reaction_added",
|
||||
direction="system",
|
||||
origin_transport="signal",
|
||||
trace_id="trace-system-diag-2",
|
||||
payload={"message_id": "m3"},
|
||||
raw_payload={},
|
||||
)
|
||||
response = self.client.get(reverse("system_settings"))
|
||||
self.assertEqual(200, response.status_code)
|
||||
content = response.content.decode("utf-8")
|
||||
self.assertIn('datalist id="diagnostics-session-options"', content)
|
||||
self.assertIn('datalist id="diagnostics-trace-options"', content)
|
||||
self.assertIn('datalist id="diagnostics-service-options"', content)
|
||||
self.assertIn('datalist id="diagnostics-event-type-options"', content)
|
||||
self.assertIn(str(self.session.id), content)
|
||||
self.assertIn("trace-system-diag-2", content)
|
||||
52
core/tests/test_system_projection_shadow_api.py
Normal file
52
core/tests/test_system_projection_shadow_api.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from core.models import ChatSession, Message, Person, PersonIdentifier, User
|
||||
|
||||
|
||||
class SystemProjectionShadowAPITests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_superuser(
|
||||
username="sys-shadow-admin",
|
||||
email="sys-shadow@example.com",
|
||||
password="pw",
|
||||
)
|
||||
person = Person.objects.create(user=self.user, name="System Shadow Person")
|
||||
identifier = PersonIdentifier.objects.create(
|
||||
user=self.user,
|
||||
person=person,
|
||||
service="signal",
|
||||
identifier="+15553332222",
|
||||
)
|
||||
self.session = ChatSession.objects.create(user=self.user, identifier=identifier)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_projection_shadow_requires_session_id(self):
|
||||
response = self.client.get(reverse("system_projection_shadow"))
|
||||
self.assertEqual(400, response.status_code)
|
||||
payload = response.json()
|
||||
self.assertFalse(payload.get("ok"))
|
||||
self.assertEqual("session_id_required", payload.get("error"))
|
||||
|
||||
def test_projection_shadow_includes_cause_summary_and_samples(self):
|
||||
Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1700000000000,
|
||||
sender_uuid="+15553332222",
|
||||
text="row-without-event",
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse("system_projection_shadow"),
|
||||
{"session_id": str(self.session.id), "detail_limit": 10},
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
payload = response.json()
|
||||
self.assertTrue(payload.get("ok"))
|
||||
self.assertIn("cause_summary", payload)
|
||||
self.assertIn("cause_samples", payload)
|
||||
cause_summary = dict(payload.get("cause_summary") or {})
|
||||
cause_samples = dict(payload.get("cause_samples") or {})
|
||||
self.assertIn("missing_event_write", cause_summary)
|
||||
self.assertIn("missing_event_write", cause_samples)
|
||||
self.assertGreaterEqual(int(cause_summary.get("missing_event_write") or 0), 1)
|
||||
Reference in New Issue
Block a user