Lightweight containerized prosody tooling + moved auth scripts + xmpp reconnect/auth stabilization

This commit is contained in:
2026-03-05 02:18:12 +00:00
parent 0718a06c19
commit 2140c5facf
69 changed files with 3767 additions and 144 deletions

View 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."
),
)

View File

@@ -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 ""))

View File

@@ -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 ""))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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",

View 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())

View 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)

View 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)

View File

@@ -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()

View 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"])

View File

@@ -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)

View 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))

View 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)