Lightweight containerized prosody tooling + moved auth scripts + xmpp reconnect/auth stabilization
This commit is contained in:
39
core/tests/test_adapter_boundary_rules.py
Normal file
39
core/tests/test_adapter_boundary_rules.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
|
||||
class AdapterBoundaryRulesTests(SimpleTestCase):
|
||||
def test_client_adapters_do_not_import_business_engines(self):
|
||||
clients_dir = Path(__file__).resolve().parents[1] / "clients"
|
||||
banned_prefixes = (
|
||||
"from core.commands",
|
||||
"import core.commands",
|
||||
"from core.tasks",
|
||||
"import core.tasks",
|
||||
"from core.assist",
|
||||
"import core.assist",
|
||||
"from core.translation",
|
||||
"import core.translation",
|
||||
)
|
||||
|
||||
violations = []
|
||||
for file_path in sorted(clients_dir.glob("*.py")):
|
||||
if file_path.name == "__init__.py":
|
||||
continue
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
for line_no, line in enumerate(content.splitlines(), start=1):
|
||||
stripped = line.strip()
|
||||
if any(stripped.startswith(prefix) for prefix in banned_prefixes):
|
||||
violations.append(f"{file_path.name}:{line_no}: {stripped}")
|
||||
|
||||
self.assertEqual(
|
||||
[],
|
||||
violations,
|
||||
msg=(
|
||||
"Adapter modules must stay translator-only and must not import "
|
||||
"business policy/task/assist/translation engines directly."
|
||||
),
|
||||
)
|
||||
@@ -71,3 +71,86 @@ class CodexCLITaskProviderTests(SimpleTestCase):
|
||||
self.assertTrue(result.ok)
|
||||
self.assertTrue(bool((result.payload or {}).get("requires_approval")))
|
||||
self.assertEqual("requires_approval", (result.payload or {}).get("parsed_status"))
|
||||
|
||||
@patch("core.tasks.providers.codex_cli.subprocess.run")
|
||||
def test_retries_with_positional_op_when_flag_unsupported(self, run_mock):
|
||||
run_mock.side_effect = [
|
||||
CompletedProcess(
|
||||
args=[],
|
||||
returncode=2,
|
||||
stdout="",
|
||||
stderr="error: unexpected argument '--op' found",
|
||||
),
|
||||
CompletedProcess(
|
||||
args=[],
|
||||
returncode=0,
|
||||
stdout='{"status":"ok","external_key":"cx-42"}',
|
||||
stderr="",
|
||||
),
|
||||
]
|
||||
result = self.provider.create_task({"command": "codex"}, {"task_id": "t1"})
|
||||
self.assertTrue(result.ok)
|
||||
self.assertEqual("cx-42", result.external_key)
|
||||
self.assertEqual(2, run_mock.call_count)
|
||||
first = run_mock.call_args_list[0].args[0]
|
||||
second = run_mock.call_args_list[1].args[0]
|
||||
self.assertIn("--op", first)
|
||||
self.assertNotIn("--op", second)
|
||||
self.assertEqual(["codex", "task-sync", "create"], second[:3])
|
||||
|
||||
@patch("core.tasks.providers.codex_cli.subprocess.run")
|
||||
def test_falls_back_to_builtin_approval_stub_when_no_task_sync_contract(self, run_mock):
|
||||
run_mock.side_effect = [
|
||||
CompletedProcess(
|
||||
args=[],
|
||||
returncode=2,
|
||||
stdout="",
|
||||
stderr="error: unexpected argument '--op' found",
|
||||
),
|
||||
CompletedProcess(
|
||||
args=[],
|
||||
returncode=2,
|
||||
stdout="",
|
||||
stderr="error: unrecognized subcommand 'create'\nUsage: codex [OPTIONS] [PROMPT]",
|
||||
),
|
||||
]
|
||||
result = self.provider.create_task(
|
||||
{"command": "codex"},
|
||||
{
|
||||
"task_id": "t1",
|
||||
"trigger_message_id": "m1",
|
||||
"mode": "default",
|
||||
},
|
||||
)
|
||||
self.assertTrue(result.ok)
|
||||
self.assertTrue(bool((result.payload or {}).get("requires_approval")))
|
||||
self.assertEqual("requires_approval", str((result.payload or {}).get("status") or ""))
|
||||
self.assertEqual("builtin_task_sync_stub", str((result.payload or {}).get("fallback_mode") or ""))
|
||||
|
||||
@patch("core.tasks.providers.codex_cli.subprocess.run")
|
||||
def test_builtin_stub_approval_response_returns_ok(self, run_mock):
|
||||
run_mock.side_effect = [
|
||||
CompletedProcess(
|
||||
args=[],
|
||||
returncode=2,
|
||||
stdout="",
|
||||
stderr="error: unexpected argument '--op' found",
|
||||
),
|
||||
CompletedProcess(
|
||||
args=[],
|
||||
returncode=2,
|
||||
stdout="",
|
||||
stderr="error: unexpected argument 'append_update' found\nUsage: codex [OPTIONS] [PROMPT]",
|
||||
),
|
||||
]
|
||||
result = self.provider.append_update(
|
||||
{"command": "codex"},
|
||||
{
|
||||
"task_id": "t1",
|
||||
"mode": "approval_response",
|
||||
"approval_key": "abc123",
|
||||
},
|
||||
)
|
||||
self.assertTrue(result.ok)
|
||||
self.assertFalse(bool((result.payload or {}).get("requires_approval")))
|
||||
self.assertEqual("ok", str((result.payload or {}).get("status") or ""))
|
||||
|
||||
@@ -123,10 +123,17 @@ class CodexCommandExecutionTests(TestCase):
|
||||
self.assertTrue(results[0].ok)
|
||||
run = CodexRun.objects.order_by("-created_at").first()
|
||||
self.assertIsNotNone(run)
|
||||
self.assertEqual("queued", run.status)
|
||||
self.assertEqual("waiting_approval", run.status)
|
||||
event = ExternalSyncEvent.objects.order_by("-created_at").first()
|
||||
self.assertEqual("pending", event.status)
|
||||
self.assertEqual("waiting_approval", event.status)
|
||||
self.assertEqual("default", str((event.payload or {}).get("provider_payload", {}).get("mode") or ""))
|
||||
self.assertTrue(
|
||||
CodexPermissionRequest.objects.filter(
|
||||
user=self.user,
|
||||
codex_run=run,
|
||||
status="pending",
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_plan_requires_reply_anchor(self):
|
||||
trigger = self._msg("#codex plan# #1")
|
||||
@@ -145,6 +152,14 @@ class CodexCommandExecutionTests(TestCase):
|
||||
self.assertEqual("reply_required_for_codex_plan", results[0].error)
|
||||
|
||||
def test_approve_command_queues_resume_event(self):
|
||||
waiting_event = ExternalSyncEvent.objects.create(
|
||||
user=self.user,
|
||||
task=self.task,
|
||||
provider="codex_cli",
|
||||
status="waiting_approval",
|
||||
payload={},
|
||||
error="",
|
||||
)
|
||||
run = CodexRun.objects.create(
|
||||
user=self.user,
|
||||
task=self.task,
|
||||
@@ -158,6 +173,7 @@ class CodexCommandExecutionTests(TestCase):
|
||||
req = CodexPermissionRequest.objects.create(
|
||||
user=self.user,
|
||||
codex_run=run,
|
||||
external_sync_event=waiting_event,
|
||||
approval_key="ak-123",
|
||||
summary="Need approval",
|
||||
requested_permissions={"items": ["write"]},
|
||||
@@ -186,8 +202,69 @@ class CodexCommandExecutionTests(TestCase):
|
||||
self.assertTrue(results[0].ok)
|
||||
req.refresh_from_db()
|
||||
run.refresh_from_db()
|
||||
waiting_event.refresh_from_db()
|
||||
self.assertEqual("approved", req.status)
|
||||
self.assertEqual("approved_waiting_resume", run.status)
|
||||
self.assertEqual("ok", waiting_event.status)
|
||||
self.assertTrue(
|
||||
ExternalSyncEvent.objects.filter(idempotency_key="codex_approval:ak-123:approved", status="pending").exists()
|
||||
)
|
||||
|
||||
def test_approve_pre_submit_request_queues_original_action(self):
|
||||
waiting_event = ExternalSyncEvent.objects.create(
|
||||
user=self.user,
|
||||
task=self.task,
|
||||
provider="codex_cli",
|
||||
status="waiting_approval",
|
||||
payload={},
|
||||
error="",
|
||||
)
|
||||
run = CodexRun.objects.create(
|
||||
user=self.user,
|
||||
task=self.task,
|
||||
project=self.project,
|
||||
source_service="web",
|
||||
source_channel="web-chan-1",
|
||||
status="waiting_approval",
|
||||
request_payload={"action": "append_update", "provider_payload": {"task_id": str(self.task.id)}},
|
||||
result_payload={},
|
||||
)
|
||||
CodexPermissionRequest.objects.create(
|
||||
user=self.user,
|
||||
codex_run=run,
|
||||
external_sync_event=waiting_event,
|
||||
approval_key="pre-ak-1",
|
||||
summary="pre submit",
|
||||
requested_permissions={"type": "pre_submit"},
|
||||
resume_payload={
|
||||
"gate_type": "pre_submit",
|
||||
"action": "append_update",
|
||||
"provider_payload": {"task_id": str(self.task.id), "mode": "default"},
|
||||
"idempotency_key": "codex_cmd:resume:1",
|
||||
},
|
||||
status="pending",
|
||||
)
|
||||
CommandChannelBinding.objects.get_or_create(
|
||||
profile=self.profile,
|
||||
direction="ingress",
|
||||
service="web",
|
||||
channel_identifier="approver-chan",
|
||||
defaults={"enabled": True},
|
||||
)
|
||||
trigger = self._msg(".codex approve pre-ak-1", source_chat_id="approver-chan")
|
||||
results = async_to_sync(process_inbound_message)(
|
||||
CommandContext(
|
||||
service="web",
|
||||
channel_identifier="approver-chan",
|
||||
message_id=str(trigger.id),
|
||||
user_id=self.user.id,
|
||||
message_text=str(trigger.text),
|
||||
payload={},
|
||||
)
|
||||
)
|
||||
self.assertEqual(1, len(results))
|
||||
self.assertTrue(results[0].ok)
|
||||
resume = ExternalSyncEvent.objects.filter(idempotency_key="codex_cmd:resume:1").first()
|
||||
self.assertIsNotNone(resume)
|
||||
self.assertEqual("pending", resume.status)
|
||||
self.assertEqual("append_update", str((resume.payload or {}).get("action") or ""))
|
||||
|
||||
@@ -121,3 +121,62 @@ class CodexWorkerPhase1Tests(TestCase):
|
||||
request = CodexPermissionRequest.objects.get(approval_key="ak-worker-1")
|
||||
self.assertEqual("pending", request.status)
|
||||
self.assertEqual(str(run.id), str(request.codex_run_id))
|
||||
|
||||
@patch("core.management.commands.codex_worker.get_provider")
|
||||
def test_approval_response_marks_original_waiting_event_ok(self, get_provider_mock):
|
||||
waiting_event = ExternalSyncEvent.objects.create(
|
||||
user=self.user,
|
||||
provider="codex_cli",
|
||||
status="waiting_approval",
|
||||
payload={"action": "append_update", "provider_payload": {"mode": "default"}},
|
||||
error="",
|
||||
)
|
||||
run = CodexRun.objects.create(
|
||||
user=self.user,
|
||||
project=self.project,
|
||||
source_service="web",
|
||||
source_channel="web-chan-1",
|
||||
status="approved_waiting_resume",
|
||||
request_payload={},
|
||||
result_payload={},
|
||||
)
|
||||
CodexPermissionRequest.objects.create(
|
||||
user=self.user,
|
||||
codex_run=run,
|
||||
external_sync_event=waiting_event,
|
||||
approval_key="ak-worker-ok",
|
||||
summary="needs permissions",
|
||||
requested_permissions={"items": ["write"]},
|
||||
resume_payload={"resume": True},
|
||||
status="approved",
|
||||
)
|
||||
resume_event = ExternalSyncEvent.objects.create(
|
||||
user=self.user,
|
||||
provider="codex_cli",
|
||||
status="pending",
|
||||
payload={
|
||||
"action": "append_update",
|
||||
"provider_payload": {
|
||||
"mode": "approval_response",
|
||||
"approval_key": "ak-worker-ok",
|
||||
"codex_run_id": str(run.id),
|
||||
},
|
||||
},
|
||||
error="",
|
||||
)
|
||||
|
||||
class _Provider:
|
||||
run_in_worker = True
|
||||
|
||||
def append_update(self, config, payload):
|
||||
return ProviderResult(ok=True, payload={"status": "ok", "summary": "resumed"})
|
||||
|
||||
create_task = mark_complete = link_task = append_update
|
||||
|
||||
get_provider_mock.return_value = _Provider()
|
||||
CodexWorkerCommand()._run_event(resume_event)
|
||||
|
||||
waiting_event.refresh_from_db()
|
||||
resume_event.refresh_from_db()
|
||||
self.assertEqual("ok", resume_event.status)
|
||||
self.assertEqual("ok", waiting_event.status)
|
||||
|
||||
@@ -53,3 +53,26 @@ class CommandRoutingVariantUITests(TestCase):
|
||||
row = self.profile.variant_policies.get(variant_key="bp_set")
|
||||
self.assertEqual("ai", row.generation_mode)
|
||||
self.assertTrue(row.send_status_to_egress)
|
||||
|
||||
def test_variant_policy_update_preserves_source_status_toggle(self):
|
||||
response = self.client.post(
|
||||
reverse("command_routing"),
|
||||
{
|
||||
"action": "variant_policy_update",
|
||||
"profile_id": str(self.profile.id),
|
||||
"variant_key": "bp_set",
|
||||
"enabled": "1",
|
||||
"generation_mode": "verbatim",
|
||||
# Intentionally omit send_status_to_source to set it False.
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
row = self.profile.variant_policies.get(variant_key="bp_set")
|
||||
self.assertFalse(row.send_status_to_source)
|
||||
|
||||
# Rendering the page should not overwrite user policy decisions.
|
||||
response = self.client.get(reverse("command_routing"))
|
||||
self.assertEqual(200, response.status_code)
|
||||
row.refresh_from_db()
|
||||
self.assertFalse(row.send_status_to_source)
|
||||
|
||||
@@ -187,6 +187,36 @@ class ComposeReactTests(TestCase):
|
||||
response.json(),
|
||||
)
|
||||
|
||||
@patch("core.views.compose.transport.send_reaction", new_callable=AsyncMock)
|
||||
def test_whatsapp_web_local_message_without_bridge_is_unresolvable(
|
||||
self, mocked_send_reaction
|
||||
):
|
||||
person, _, message = self._build_message(
|
||||
service="whatsapp",
|
||||
identifier="12345@s.whatsapp.net",
|
||||
source_message_id="1771234567000",
|
||||
)
|
||||
message.source_service = "web"
|
||||
message.save(update_fields=["source_service"])
|
||||
|
||||
response = self.client.post(
|
||||
reverse("compose_react"),
|
||||
{
|
||||
"service": "whatsapp",
|
||||
"identifier": "12345@s.whatsapp.net",
|
||||
"person": str(person.id),
|
||||
"message_id": str(message.id),
|
||||
"emoji": "😮",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual(
|
||||
{"ok": False, "error": "whatsapp_target_unresolvable"},
|
||||
response.json(),
|
||||
)
|
||||
mocked_send_reaction.assert_not_awaited()
|
||||
|
||||
def test_compose_page_renders_reaction_actions_for_signal(self):
|
||||
person, _, _ = self._build_message(
|
||||
service="signal",
|
||||
|
||||
62
core/tests/test_event_ledger.py
Normal file
62
core/tests/test_event_ledger.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from core.events.ledger import append_event_sync
|
||||
from core.models import ChatSession, ConversationEvent, Person, PersonIdentifier, User
|
||||
|
||||
|
||||
@override_settings(EVENT_LEDGER_DUAL_WRITE=True)
|
||||
class EventLedgerTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="ledger-user",
|
||||
email="ledger@example.com",
|
||||
password="x",
|
||||
)
|
||||
self.person = Person.objects.create(user=self.user, name="Ledger Person")
|
||||
self.identifier = PersonIdentifier.objects.create(
|
||||
user=self.user,
|
||||
person=self.person,
|
||||
service="signal",
|
||||
identifier="+15555550123",
|
||||
)
|
||||
self.session = ChatSession.objects.create(
|
||||
user=self.user,
|
||||
identifier=self.identifier,
|
||||
)
|
||||
|
||||
def test_append_event_creates_row(self):
|
||||
row = append_event_sync(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1234,
|
||||
event_type="message_created",
|
||||
direction="in",
|
||||
origin_transport="signal",
|
||||
origin_message_id="abc",
|
||||
payload={"text": "hello"},
|
||||
)
|
||||
self.assertIsNotNone(row)
|
||||
self.assertEqual(1, ConversationEvent.objects.count())
|
||||
|
||||
def test_append_event_is_idempotent_for_same_origin_and_type(self):
|
||||
append_event_sync(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1234,
|
||||
event_type="message_created",
|
||||
direction="in",
|
||||
origin_transport="signal",
|
||||
origin_message_id="dup-1",
|
||||
payload={"text": "hello"},
|
||||
)
|
||||
append_event_sync(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1235,
|
||||
event_type="message_created",
|
||||
direction="in",
|
||||
origin_transport="signal",
|
||||
origin_message_id="dup-1",
|
||||
payload={"text": "hello again"},
|
||||
)
|
||||
self.assertEqual(1, ConversationEvent.objects.count())
|
||||
47
core/tests/test_event_ledger_smoke_command.py
Normal file
47
core/tests/test_event_ledger_smoke_command.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from io import StringIO
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from core.events.ledger import append_event_sync
|
||||
from core.models import ChatSession, Person, PersonIdentifier, User
|
||||
|
||||
|
||||
@override_settings(EVENT_LEDGER_DUAL_WRITE=True)
|
||||
class EventLedgerSmokeCommandTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="ledger-smoke-user",
|
||||
email="ledger-smoke@example.com",
|
||||
password="pw",
|
||||
)
|
||||
person = Person.objects.create(user=self.user, name="Smoke Person")
|
||||
identifier = PersonIdentifier.objects.create(
|
||||
user=self.user,
|
||||
person=person,
|
||||
service="signal",
|
||||
identifier="+15550001111",
|
||||
)
|
||||
self.session = ChatSession.objects.create(user=self.user, identifier=identifier)
|
||||
|
||||
def test_smoke_command_reports_recent_rows(self):
|
||||
append_event_sync(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1770000000000,
|
||||
event_type="message_created",
|
||||
direction="in",
|
||||
origin_transport="signal",
|
||||
origin_message_id="abc",
|
||||
payload={"message_id": "m1"},
|
||||
)
|
||||
out = StringIO()
|
||||
call_command(
|
||||
"event_ledger_smoke",
|
||||
user_id=str(self.user.id),
|
||||
minutes=999999,
|
||||
stdout=out,
|
||||
)
|
||||
rendered = out.getvalue()
|
||||
self.assertIn("event-ledger-smoke", rendered)
|
||||
self.assertIn("event_type_counts=", rendered)
|
||||
132
core/tests/test_event_projection_shadow.py
Normal file
132
core/tests/test_event_projection_shadow.py
Normal file
@@ -0,0 +1,132 @@
|
||||
from io import StringIO
|
||||
import time
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from core.events.ledger import append_event_sync
|
||||
from core.events.projection import shadow_compare_session
|
||||
from core.models import ChatSession, Message, Person, PersonIdentifier, User
|
||||
|
||||
|
||||
@override_settings(EVENT_LEDGER_DUAL_WRITE=True)
|
||||
class EventProjectionShadowTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="projection-user",
|
||||
email="projection@example.com",
|
||||
password="x",
|
||||
)
|
||||
self.person = Person.objects.create(user=self.user, name="Projection Person")
|
||||
self.identifier = PersonIdentifier.objects.create(
|
||||
user=self.user,
|
||||
person=self.person,
|
||||
service="signal",
|
||||
identifier="+15555550333",
|
||||
)
|
||||
self.session = ChatSession.objects.create(user=self.user, identifier=self.identifier)
|
||||
|
||||
def test_shadow_compare_has_zero_mismatch_when_projection_matches(self):
|
||||
message = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1700000000000,
|
||||
sender_uuid="+15555550333",
|
||||
text="hello",
|
||||
delivered_ts=1700000000000,
|
||||
read_ts=1700000000500,
|
||||
receipt_payload={
|
||||
"reactions": [
|
||||
{
|
||||
"source_service": "signal",
|
||||
"actor": "user:1:signal",
|
||||
"emoji": "👍",
|
||||
"removed": False,
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
append_event_sync(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1700000000000,
|
||||
event_type="message_created",
|
||||
direction="in",
|
||||
origin_transport="signal",
|
||||
origin_message_id=str(message.id),
|
||||
payload={"message_id": str(message.id), "text": "hello"},
|
||||
)
|
||||
append_event_sync(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1700000000500,
|
||||
event_type="read_receipt",
|
||||
direction="system",
|
||||
origin_transport="signal",
|
||||
origin_message_id=str(message.id),
|
||||
payload={"message_id": str(message.id), "read_ts": 1700000000500},
|
||||
)
|
||||
append_event_sync(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1700000000600,
|
||||
event_type="reaction_added",
|
||||
direction="system",
|
||||
actor_identifier="user:1:signal",
|
||||
origin_transport="signal",
|
||||
origin_message_id=str(message.id),
|
||||
payload={
|
||||
"message_id": str(message.id),
|
||||
"emoji": "👍",
|
||||
"source_service": "signal",
|
||||
"actor": "user:1:signal",
|
||||
},
|
||||
)
|
||||
|
||||
compared = shadow_compare_session(self.session, detail_limit=10)
|
||||
self.assertEqual(0, compared["mismatch_total"])
|
||||
|
||||
def test_shadow_compare_detects_missing_projection_row(self):
|
||||
Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1700000000000,
|
||||
sender_uuid="+15555550333",
|
||||
text="no-event",
|
||||
)
|
||||
compared = shadow_compare_session(self.session, detail_limit=10)
|
||||
self.assertGreater(compared["counters"]["missing_in_projection"], 0)
|
||||
|
||||
def test_management_command_emits_summary(self):
|
||||
out = StringIO()
|
||||
call_command(
|
||||
"event_projection_shadow",
|
||||
user_id=str(self.user.id),
|
||||
limit_sessions=5,
|
||||
stdout=out,
|
||||
)
|
||||
rendered = out.getvalue()
|
||||
self.assertIn("shadow compare:", rendered)
|
||||
self.assertIn("cause_counts=", rendered)
|
||||
|
||||
def test_management_command_supports_service_and_recent_filters(self):
|
||||
Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=int(time.time() * 1000),
|
||||
sender_uuid="+15550000000",
|
||||
text="recent",
|
||||
source_service="signal",
|
||||
source_message_id="recent-1",
|
||||
)
|
||||
out = StringIO()
|
||||
call_command(
|
||||
"event_projection_shadow",
|
||||
user_id=str(self.user.id),
|
||||
service="signal",
|
||||
recent_minutes=120,
|
||||
limit_sessions=5,
|
||||
stdout=out,
|
||||
)
|
||||
rendered = out.getvalue()
|
||||
self.assertIn("shadow compare:", rendered)
|
||||
@@ -8,6 +8,7 @@ from core.commands.engine import _matches_trigger, process_inbound_message
|
||||
from core.messaging.reply_sync import extract_reply_ref, resolve_reply_target
|
||||
from core.views.compose import _command_options_for_channel
|
||||
from core.models import (
|
||||
ChatTaskSource,
|
||||
ChatSession,
|
||||
CommandAction,
|
||||
CommandChannelBinding,
|
||||
@@ -362,6 +363,14 @@ class Phase1CommandEngineTests(TestCase):
|
||||
).exists()
|
||||
self.assertTrue(ingress_exists)
|
||||
self.assertTrue(egress_exists)
|
||||
self.assertTrue(
|
||||
ChatTaskSource.objects.filter(
|
||||
user=self.user,
|
||||
service="signal",
|
||||
channel_identifier="+15550000002",
|
||||
enabled=True,
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_first_user_bp_command_auto_setup_is_idempotent(self):
|
||||
CommandProfile.objects.filter(user=self.user, slug="bp").delete()
|
||||
|
||||
126
core/tests/test_reaction_normalization.py
Normal file
126
core/tests/test_reaction_normalization.py
Normal file
@@ -0,0 +1,126 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.test import TestCase
|
||||
|
||||
from core.messaging import history
|
||||
from core.models import ChatSession, Message, Person, PersonIdentifier, User
|
||||
from core.views.compose import _serialize_message
|
||||
|
||||
|
||||
class ReactionNormalizationTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="react-normalize",
|
||||
email="react-normalize@example.com",
|
||||
password="pw",
|
||||
)
|
||||
self.person = Person.objects.create(user=self.user, name="Reaction Person")
|
||||
self.identifier = PersonIdentifier.objects.create(
|
||||
user=self.user,
|
||||
person=self.person,
|
||||
service="signal",
|
||||
identifier="+15551239999",
|
||||
)
|
||||
self.session = ChatSession.objects.create(
|
||||
user=self.user,
|
||||
identifier=self.identifier,
|
||||
)
|
||||
|
||||
def test_apply_reaction_prefers_exact_source_timestamp_match(self):
|
||||
near_message = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1700000000100,
|
||||
sender_uuid="author-near",
|
||||
text="near",
|
||||
source_service="signal",
|
||||
source_message_id="1700000000100",
|
||||
)
|
||||
exact_message = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1700000000000,
|
||||
sender_uuid="author-exact",
|
||||
text="exact",
|
||||
source_service="signal",
|
||||
source_message_id="1700000000000",
|
||||
)
|
||||
|
||||
updated = async_to_sync(history.apply_reaction)(
|
||||
self.user,
|
||||
self.identifier,
|
||||
target_ts=1700000000000,
|
||||
emoji="❤️",
|
||||
source_service="signal",
|
||||
actor="reactor-1",
|
||||
target_author="author-exact",
|
||||
remove=False,
|
||||
payload={"origin": "test"},
|
||||
)
|
||||
|
||||
self.assertEqual(str(exact_message.id), str(updated.id))
|
||||
exact_message.refresh_from_db()
|
||||
near_message.refresh_from_db()
|
||||
self.assertEqual(1, len((exact_message.receipt_payload or {}).get("reactions") or []))
|
||||
self.assertEqual(
|
||||
"exact_source_message_id_ts",
|
||||
str((exact_message.receipt_payload or {}).get("reaction_last_match_strategy") or ""),
|
||||
)
|
||||
self.assertEqual(0, len((near_message.receipt_payload or {}).get("reactions") or []))
|
||||
|
||||
def test_remove_without_emoji_is_audited_not_active(self):
|
||||
message = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1700000001000,
|
||||
sender_uuid="author-1",
|
||||
text="msg",
|
||||
source_service="signal",
|
||||
source_message_id="1700000001000",
|
||||
)
|
||||
|
||||
async_to_sync(history.apply_reaction)(
|
||||
self.user,
|
||||
self.identifier,
|
||||
target_ts=1700000001000,
|
||||
emoji="",
|
||||
source_service="whatsapp",
|
||||
actor="actor-1",
|
||||
remove=True,
|
||||
payload={"origin": "test"},
|
||||
)
|
||||
|
||||
message.refresh_from_db()
|
||||
payload = dict(message.receipt_payload or {})
|
||||
self.assertEqual([], list(payload.get("reactions") or []))
|
||||
self.assertEqual(1, len(list(payload.get("reaction_events") or [])))
|
||||
|
||||
def test_emoji_only_reply_text_is_not_reaction(self):
|
||||
anchor = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1700000002000,
|
||||
sender_uuid="author-1",
|
||||
text="anchor",
|
||||
source_service="signal",
|
||||
source_message_id="1700000002000",
|
||||
)
|
||||
heart_reply = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
ts=1700000003000,
|
||||
sender_uuid="author-2",
|
||||
text="❤️",
|
||||
source_service="signal",
|
||||
source_message_id="1700000003000",
|
||||
reply_to=anchor,
|
||||
reply_source_service="signal",
|
||||
reply_source_message_id="1700000002000",
|
||||
receipt_payload={},
|
||||
)
|
||||
|
||||
serialized = _serialize_message(heart_reply)
|
||||
self.assertEqual("❤️", serialized["text"])
|
||||
self.assertEqual([], list(serialized.get("reactions") or []))
|
||||
self.assertEqual(str(anchor.id), serialized["reply_to_id"])
|
||||
@@ -211,6 +211,119 @@ class TaskSettingsViewActionsTests(TestCase):
|
||||
)
|
||||
|
||||
|
||||
@override_settings(TASK_DERIVATION_USE_AI=False)
|
||||
class TaskAutoBootstrapTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user("task-auto-user", "task-auto@example.com", "x")
|
||||
self.person = Person.objects.create(user=self.user, name="Bootstrap Chat")
|
||||
self.identifier = PersonIdentifier.objects.create(
|
||||
user=self.user,
|
||||
person=self.person,
|
||||
service="whatsapp",
|
||||
identifier="120363402761690215@g.us",
|
||||
)
|
||||
self.session = ChatSession.objects.create(user=self.user, identifier=self.identifier)
|
||||
|
||||
def test_task_message_auto_creates_project_and_source(self):
|
||||
msg = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="peer",
|
||||
text="task: ship alpha",
|
||||
ts=1000,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215@g.us",
|
||||
)
|
||||
with patch("core.tasks.engine.send_message_raw", new=AsyncMock()):
|
||||
async_to_sync(process_inbound_task_intelligence)(msg)
|
||||
source = ChatTaskSource.objects.filter(
|
||||
user=self.user,
|
||||
service="whatsapp",
|
||||
channel_identifier="120363402761690215@g.us",
|
||||
enabled=True,
|
||||
).first()
|
||||
self.assertIsNotNone(source)
|
||||
self.assertTrue(TaskProject.objects.filter(user=self.user, id=source.project_id).exists())
|
||||
self.assertEqual(1, DerivedTask.objects.filter(user=self.user).count())
|
||||
|
||||
|
||||
class TaskProjectDeleteGuardTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user("task-delete-user", "task-delete@example.com", "x")
|
||||
self.client.force_login(self.user)
|
||||
self.project = TaskProject.objects.create(user=self.user, name="Delete Me")
|
||||
self.source = ChatTaskSource.objects.create(
|
||||
user=self.user,
|
||||
service="signal",
|
||||
channel_identifier="+15550000001",
|
||||
project=self.project,
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
def test_project_delete_requires_exact_confirmation(self):
|
||||
response = self.client.post(
|
||||
reverse("tasks_hub"),
|
||||
{
|
||||
"action": "project_delete",
|
||||
"project_id": str(self.project.id),
|
||||
"confirm_name": "wrong",
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertTrue(TaskProject.objects.filter(id=self.project.id, user=self.user).exists())
|
||||
|
||||
def test_project_delete_reseeds_default_mapping(self):
|
||||
response = self.client.post(
|
||||
reverse("tasks_hub"),
|
||||
{
|
||||
"action": "project_delete",
|
||||
"project_id": str(self.project.id),
|
||||
"confirm_name": "Delete Me",
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertFalse(TaskProject.objects.filter(id=self.project.id, user=self.user).exists())
|
||||
self.assertTrue(
|
||||
ChatTaskSource.objects.filter(
|
||||
user=self.user,
|
||||
service="signal",
|
||||
channel_identifier="+15550000001",
|
||||
enabled=True,
|
||||
).exists()
|
||||
)
|
||||
|
||||
|
||||
class TaskHubEmptyProjectVisibilityTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user("task-hub-user", "task-hub@example.com", "x")
|
||||
self.client.force_login(self.user)
|
||||
self.empty = TaskProject.objects.create(user=self.user, name="Empty")
|
||||
self.used = TaskProject.objects.create(user=self.user, name="Used")
|
||||
DerivedTask.objects.create(
|
||||
user=self.user,
|
||||
project=self.used,
|
||||
title="Visible Task",
|
||||
source_service="web",
|
||||
source_channel="web-1",
|
||||
reference_code="1",
|
||||
status_snapshot="open",
|
||||
)
|
||||
|
||||
def test_tasks_hub_hides_empty_projects_by_default(self):
|
||||
response = self.client.get(reverse("tasks_hub"))
|
||||
self.assertEqual(200, response.status_code)
|
||||
projects = list(response.context["projects"])
|
||||
self.assertEqual(["Used"], [str(row.name) for row in projects])
|
||||
|
||||
def test_tasks_hub_can_show_empty_projects(self):
|
||||
response = self.client.get(reverse("tasks_hub"), {"show_empty": "1"})
|
||||
self.assertEqual(200, response.status_code)
|
||||
names = sorted(str(row.name) for row in response.context["projects"])
|
||||
self.assertEqual(["Empty", "Used"], names)
|
||||
|
||||
|
||||
class TaskSettingsExternalChatLinkScopeTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user("task-link-user", "task-link@example.com", "x")
|
||||
@@ -331,10 +444,29 @@ class CodexSettingsAndSubmitTests(TestCase):
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertTrue(CodexRun.objects.filter(user=self.user, task=self.task).exists())
|
||||
self.assertTrue(ExternalSyncEvent.objects.filter(user=self.user, task=self.task, provider="codex_cli").exists())
|
||||
run = CodexRun.objects.filter(user=self.user, task=self.task).order_by("-created_at").first()
|
||||
self.assertIsNotNone(run)
|
||||
self.assertEqual("waiting_approval", str(getattr(run, "status", "")))
|
||||
event = ExternalSyncEvent.objects.filter(user=self.user, task=self.task, provider="codex_cli").order_by("-created_at").first()
|
||||
self.assertIsNotNone(event)
|
||||
self.assertEqual("waiting_approval", str(getattr(event, "status", "")))
|
||||
self.assertTrue(
|
||||
CodexPermissionRequest.objects.filter(
|
||||
user=self.user,
|
||||
codex_run=run,
|
||||
status="pending",
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_codex_settings_page_and_approval_action(self):
|
||||
waiting_event = ExternalSyncEvent.objects.create(
|
||||
user=self.user,
|
||||
task=self.task,
|
||||
provider="codex_cli",
|
||||
status="waiting_approval",
|
||||
payload={},
|
||||
error="",
|
||||
)
|
||||
run = CodexRun.objects.create(
|
||||
user=self.user,
|
||||
task=self.task,
|
||||
@@ -348,6 +480,7 @@ class CodexSettingsAndSubmitTests(TestCase):
|
||||
req = CodexPermissionRequest.objects.create(
|
||||
user=self.user,
|
||||
codex_run=run,
|
||||
external_sync_event=waiting_event,
|
||||
approval_key="approve-me",
|
||||
summary="need approval",
|
||||
requested_permissions={"items": ["write"]},
|
||||
@@ -365,5 +498,7 @@ class CodexSettingsAndSubmitTests(TestCase):
|
||||
self.assertEqual(200, response.status_code)
|
||||
req.refresh_from_db()
|
||||
run.refresh_from_db()
|
||||
waiting_event.refresh_from_db()
|
||||
self.assertEqual("approved", req.status)
|
||||
self.assertEqual("approved_waiting_resume", run.status)
|
||||
self.assertEqual("ok", waiting_event.status)
|
||||
|
||||
21
core/tests/test_tracing_helpers.py
Normal file
21
core/tests/test_tracing_helpers.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from core.observability.tracing import ensure_trace_id
|
||||
|
||||
|
||||
class TracingHelpersTests(SimpleTestCase):
|
||||
def test_ensure_trace_id_prefers_explicit_value(self):
|
||||
self.assertEqual(
|
||||
"abc123",
|
||||
ensure_trace_id("abc123", {"trace_id": "payload"}),
|
||||
)
|
||||
|
||||
def test_ensure_trace_id_uses_payload_value(self):
|
||||
self.assertEqual(
|
||||
"payload-value",
|
||||
ensure_trace_id("", {"trace_id": "payload-value"}),
|
||||
)
|
||||
|
||||
def test_ensure_trace_id_generates_when_missing(self):
|
||||
trace_id = ensure_trace_id("", {})
|
||||
self.assertEqual(32, len(trace_id))
|
||||
17
core/tests/test_transport_capabilities.py
Normal file
17
core/tests/test_transport_capabilities.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from core.transports.capabilities import capability_snapshot, supports, unsupported_reason
|
||||
|
||||
|
||||
class TransportCapabilitiesTests(SimpleTestCase):
|
||||
def test_signal_reactions_supported(self):
|
||||
self.assertTrue(supports("signal", "reactions"))
|
||||
|
||||
def test_instagram_reactions_not_supported(self):
|
||||
self.assertFalse(supports("instagram", "reactions"))
|
||||
self.assertIn("instagram does not support reactions", unsupported_reason("instagram", "reactions"))
|
||||
|
||||
def test_snapshot_has_schema_version(self):
|
||||
snapshot = capability_snapshot()
|
||||
self.assertIn("schema_version", snapshot)
|
||||
self.assertIn("services", snapshot)
|
||||
Reference in New Issue
Block a user