Reimplement compose and add tiling windows

This commit is contained in:
2026-03-12 22:03:30 +00:00
parent 79766d279d
commit 6ceff63b71
126 changed files with 5111 additions and 10796 deletions

View File

@@ -1,183 +0,0 @@
from __future__ import annotations
from subprocess import CompletedProcess, TimeoutExpired
from unittest.mock import patch
from django.test import SimpleTestCase
from core.tasks.providers.claude_cli import ClaudeCLITaskProvider
class ClaudeCLITaskProviderTests(SimpleTestCase):
def setUp(self):
self.provider = ClaudeCLITaskProvider()
@patch("core.tasks.providers.claude_cli.subprocess.run")
def test_healthcheck_success(self, run_mock):
run_mock.return_value = CompletedProcess(
args=["claude", "--version"],
returncode=0,
stdout="claude 1.0.0\n",
stderr="",
)
result = self.provider.healthcheck({"command": "claude", "timeout_seconds": 5})
self.assertTrue(result.ok)
self.assertIn("claude", str(result.payload.get("stdout") or ""))
@patch("core.tasks.providers.claude_cli.subprocess.run")
def test_create_task_builds_task_sync_command(self, run_mock):
run_mock.return_value = CompletedProcess(
args=[],
returncode=0,
stdout='{"external_key":"cl-123"}',
stderr="",
)
result = self.provider.create_task(
{
"command": "claude",
"workspace_root": "/tmp/work",
"default_profile": "default",
"timeout_seconds": 30,
},
{
"task_id": "t1",
"title": "hello",
"reference_code": "42",
},
)
self.assertTrue(result.ok)
self.assertEqual("cl-123", result.external_key)
args = run_mock.call_args.args[0]
self.assertEqual(["claude", "task-sync", "--op", "create"], args[:4])
self.assertIn("--workspace", args)
self.assertIn("--payload-json", args)
@patch("core.tasks.providers.claude_cli.subprocess.run")
def test_timeout_maps_to_failed_result(self, run_mock):
run_mock.side_effect = TimeoutExpired(cmd=["claude"], timeout=10)
result = self.provider.append_update(
{"command": "claude", "timeout_seconds": 10}, {"task_id": "t1"}
)
self.assertFalse(result.ok)
self.assertIn("timeout", result.error)
@patch("core.tasks.providers.claude_cli.subprocess.run")
def test_requires_approval_parsed_from_stdout(self, run_mock):
run_mock.return_value = CompletedProcess(
args=[],
returncode=0,
stdout='{"status":"requires_approval","approval_key":"ak-1","permission_request":{"requested_permissions":["write"]}}',
stderr="",
)
result = self.provider.append_update({"command": "claude"}, {"task_id": "t1"})
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.claude_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":"cl-42"}',
stderr="",
),
]
result = self.provider.create_task({"command": "claude"}, {"task_id": "t1"})
self.assertTrue(result.ok)
self.assertEqual("cl-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(["claude", "task-sync", "create"], second[:3])
@patch("core.tasks.providers.claude_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: claude [OPTIONS] [PROMPT]",
),
]
result = self.provider.create_task(
{"command": "claude"},
{
"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.claude_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: claude [OPTIONS] [PROMPT]",
),
]
result = self.provider.append_update(
{"command": "claude"},
{
"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 ""))
def test_provider_name_and_run_in_worker(self):
self.assertEqual("claude_cli", self.provider.name)
self.assertTrue(self.provider.run_in_worker)
@patch("core.tasks.providers.claude_cli.subprocess.run")
def test_healthcheck_failure(self, run_mock):
run_mock.return_value = CompletedProcess(
args=["claude", "--version"],
returncode=1,
stdout="",
stderr="command not found: claude",
)
result = self.provider.healthcheck({"command": "claude"})
self.assertFalse(result.ok)
self.assertIn("command not found", result.error)

View File

@@ -1,285 +0,0 @@
from __future__ import annotations
from asgiref.sync import async_to_sync
from django.test import TestCase
from core.commands.base import CommandContext
from core.commands.engine import process_inbound_message
from core.commands.handlers.claude import parse_claude_command
from core.models import (
ChatSession,
CodexPermissionRequest,
CodexRun,
CommandChannelBinding,
CommandProfile,
DerivedTask,
ExternalSyncEvent,
Message,
Person,
PersonIdentifier,
TaskProject,
TaskProviderConfig,
User,
)
class ClaudeCommandParserTests(TestCase):
def test_parse_variants(self):
self.assertEqual("default", parse_claude_command("#claude# run this").command)
self.assertEqual("plan", parse_claude_command("#claude plan# run this").command)
self.assertEqual("status", parse_claude_command("#claude status#").command)
parsed = parse_claude_command("#claude approve abc123#")
self.assertEqual("approve", parsed.command)
self.assertEqual("abc123", parsed.approval_key)
self.assertEqual("default", parse_claude_command(".claude run this").command)
self.assertEqual("plan", parse_claude_command(".CLAUDE plan run this").command)
self.assertEqual("status", parse_claude_command(".claude status").command)
parsed_dot = parse_claude_command(".claude approve abc123")
self.assertEqual("approve", parsed_dot.command)
self.assertEqual("abc123", parsed_dot.approval_key)
def test_no_match_returns_none_command(self):
self.assertIsNone(parse_claude_command("hello world").command)
self.assertIsNone(parse_claude_command(".codex do this").command)
class ClaudeCommandExecutionTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
"claude-cmd-user", "claude-cmd@example.com", "x"
)
self.person = Person.objects.create(user=self.user, name="Claude Cmd")
self.identifier = PersonIdentifier.objects.create(
user=self.user,
person=self.person,
service="web",
identifier="web-chan-1",
)
self.session = ChatSession.objects.create(
user=self.user, identifier=self.identifier
)
self.project = TaskProject.objects.create(user=self.user, name="Project A")
self.task = DerivedTask.objects.create(
user=self.user,
project=self.project,
epic=None,
title="Task A",
source_service="web",
source_channel="web-chan-1",
reference_code="1",
status_snapshot="open",
)
self.profile = CommandProfile.objects.create(
user=self.user,
slug="claude",
name="Claude",
enabled=True,
trigger_token="#claude#",
reply_required=False,
exact_match_only=False,
)
CommandChannelBinding.objects.create(
profile=self.profile,
direction="ingress",
service="web",
channel_identifier="web-chan-1",
enabled=True,
)
TaskProviderConfig.objects.create(
user=self.user,
provider="claude_cli",
enabled=True,
settings={
"command": "claude",
"workspace_root": "",
"default_profile": "",
"timeout_seconds": 60,
"chat_link_mode": "task-sync",
"instance_label": "default",
"approver_mode": "channel",
"approver_service": "web",
"approver_identifier": "approver-chan",
},
)
def _msg(self, text: str, *, source_chat_id: str = "web-chan-1", reply_to=None):
return Message.objects.create(
user=self.user,
session=self.session,
sender_uuid="",
text=text,
ts=1000 + Message.objects.filter(user=self.user).count(),
source_service="web",
source_chat_id=source_chat_id,
reply_to=reply_to,
message_meta={},
)
def test_default_submission_creates_run_and_event(self):
trigger = self._msg("#claude# please update #1")
results = async_to_sync(process_inbound_message)(
CommandContext(
service="web",
channel_identifier="web-chan-1",
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)
run = CodexRun.objects.order_by("-created_at").first()
self.assertIsNotNone(run)
self.assertEqual("waiting_approval", run.status)
event = ExternalSyncEvent.objects.order_by("-created_at").first()
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()
)
# The approval notification must reference ".claude approve" not ".codex approve"
req = CodexPermissionRequest.objects.get(codex_run=run, status="pending")
approval_key = str(req.approval_key or "")
# The approval_key should exist
self.assertTrue(bool(approval_key))
def test_plan_requires_reply_anchor(self):
trigger = self._msg("#claude plan# #1")
results = async_to_sync(process_inbound_message)(
CommandContext(
service="web",
channel_identifier="web-chan-1",
message_id=str(trigger.id),
user_id=self.user.id,
message_text=str(trigger.text),
payload={},
)
)
self.assertEqual(1, len(results))
self.assertFalse(results[0].ok)
self.assertEqual("reply_required_for_claude_plan", results[0].error)
def test_approve_command_queues_resume_event(self):
waiting_event = ExternalSyncEvent.objects.create(
user=self.user,
task=self.task,
provider="claude_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="cl-ak-123",
summary="Need approval",
requested_permissions={"items": ["write"]},
resume_payload={"resume": True},
status="pending",
)
CommandChannelBinding.objects.create(
profile=self.profile,
direction="ingress",
service="web",
channel_identifier="approver-chan",
enabled=True,
)
trigger = self._msg(
"#claude approve cl-ak-123#", 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)
run.refresh_from_db()
waiting_event.refresh_from_db()
self.assertEqual("approved_waiting_resume", run.status)
self.assertEqual("ok", waiting_event.status)
self.assertTrue(
ExternalSyncEvent.objects.filter(
idempotency_key="claude_approval:cl-ak-123:approved",
status="pending",
).exists()
)
def test_deny_command_marks_run_denied(self):
waiting_event = ExternalSyncEvent.objects.create(
user=self.user,
task=self.task,
provider="claude_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={},
result_payload={},
)
CodexPermissionRequest.objects.create(
user=self.user,
codex_run=run,
external_sync_event=waiting_event,
approval_key="cl-deny-1",
summary="Need approval",
requested_permissions={"items": ["write"]},
resume_payload={},
status="pending",
)
CommandChannelBinding.objects.get_or_create(
profile=self.profile,
direction="ingress",
service="web",
channel_identifier="approver-chan",
defaults={"enabled": True},
)
trigger = self._msg(".claude deny cl-deny-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)
run.refresh_from_db()
self.assertEqual("denied", run.status)

View File

@@ -1,167 +0,0 @@
from __future__ import annotations
from subprocess import CompletedProcess, TimeoutExpired
from unittest.mock import patch
from django.test import SimpleTestCase
from core.tasks.providers.codex_cli import CodexCLITaskProvider
class CodexCLITaskProviderTests(SimpleTestCase):
def setUp(self):
self.provider = CodexCLITaskProvider()
@patch("core.tasks.providers.codex_cli.subprocess.run")
def test_healthcheck_success(self, run_mock):
run_mock.return_value = CompletedProcess(
args=["codex", "--version"],
returncode=0,
stdout="codex 1.2.3\n",
stderr="",
)
result = self.provider.healthcheck({"command": "codex", "timeout_seconds": 5})
self.assertTrue(result.ok)
self.assertIn("codex", str(result.payload.get("stdout") or ""))
@patch("core.tasks.providers.codex_cli.subprocess.run")
def test_create_task_builds_task_sync_command(self, run_mock):
run_mock.return_value = CompletedProcess(
args=[],
returncode=0,
stdout='{"external_key":"cx-123"}',
stderr="",
)
result = self.provider.create_task(
{
"command": "codex",
"workspace_root": "/tmp/work",
"default_profile": "default",
"timeout_seconds": 30,
},
{
"task_id": "t1",
"title": "hello",
"reference_code": "42",
},
)
self.assertTrue(result.ok)
self.assertEqual("cx-123", result.external_key)
args = run_mock.call_args.args[0]
self.assertEqual(["codex", "task-sync", "--op", "create"], args[:4])
self.assertIn("--workspace", args)
self.assertIn("--payload-json", args)
@patch("core.tasks.providers.codex_cli.subprocess.run")
def test_timeout_maps_to_failed_result(self, run_mock):
run_mock.side_effect = TimeoutExpired(cmd=["codex"], timeout=10)
result = self.provider.append_update(
{"command": "codex", "timeout_seconds": 10}, {"task_id": "t1"}
)
self.assertFalse(result.ok)
self.assertIn("timeout", result.error)
@patch("core.tasks.providers.codex_cli.subprocess.run")
def test_requires_approval_parsed_from_stdout(self, run_mock):
run_mock.return_value = CompletedProcess(
args=[],
returncode=0,
stdout='{"status":"requires_approval","approval_key":"ak-1","permission_request":{"requested_permissions":["write"]}}',
stderr="",
)
result = self.provider.append_update({"command": "codex"}, {"task_id": "t1"})
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

@@ -1,289 +0,0 @@
from __future__ import annotations
from asgiref.sync import async_to_sync
from django.test import TestCase
from core.commands.base import CommandContext
from core.commands.engine import process_inbound_message
from core.commands.handlers.codex import parse_codex_command
from core.models import (
ChatSession,
CodexPermissionRequest,
CodexRun,
CommandChannelBinding,
CommandProfile,
DerivedTask,
ExternalSyncEvent,
Message,
Person,
PersonIdentifier,
TaskProject,
TaskProviderConfig,
User,
)
class CodexCommandParserTests(TestCase):
def test_parse_variants(self):
self.assertEqual("default", parse_codex_command("#codex# run this").command)
self.assertEqual("plan", parse_codex_command("#codex plan# run this").command)
self.assertEqual("status", parse_codex_command("#codex status#").command)
parsed = parse_codex_command("#codex approve abc123#")
self.assertEqual("approve", parsed.command)
self.assertEqual("abc123", parsed.approval_key)
self.assertEqual("default", parse_codex_command(".codex run this").command)
self.assertEqual("plan", parse_codex_command(".CODEX plan run this").command)
self.assertEqual("status", parse_codex_command(".codex status").command)
parsed_dot = parse_codex_command(".codex approve abc123")
self.assertEqual("approve", parsed_dot.command)
self.assertEqual("abc123", parsed_dot.approval_key)
class CodexCommandExecutionTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
"codex-cmd-user", "codex-cmd@example.com", "x"
)
self.person = Person.objects.create(user=self.user, name="Codex Cmd")
self.identifier = PersonIdentifier.objects.create(
user=self.user,
person=self.person,
service="web",
identifier="web-chan-1",
)
self.session = ChatSession.objects.create(
user=self.user, identifier=self.identifier
)
self.project = TaskProject.objects.create(user=self.user, name="Project A")
self.task = DerivedTask.objects.create(
user=self.user,
project=self.project,
epic=None,
title="Task A",
source_service="web",
source_channel="web-chan-1",
reference_code="1",
status_snapshot="open",
)
self.profile = CommandProfile.objects.create(
user=self.user,
slug="codex",
name="Codex",
enabled=True,
trigger_token="#codex#",
reply_required=False,
exact_match_only=False,
)
CommandChannelBinding.objects.create(
profile=self.profile,
direction="ingress",
service="web",
channel_identifier="web-chan-1",
enabled=True,
)
TaskProviderConfig.objects.create(
user=self.user,
provider="codex_cli",
enabled=True,
settings={
"command": "codex",
"workspace_root": "",
"default_profile": "",
"timeout_seconds": 60,
"chat_link_mode": "task-sync",
"instance_label": "default",
"approver_mode": "channel",
"approver_service": "web",
"approver_identifier": "approver-chan",
},
)
def _msg(self, text: str, *, source_chat_id: str = "web-chan-1", reply_to=None):
return Message.objects.create(
user=self.user,
session=self.session,
sender_uuid="",
text=text,
ts=1000 + Message.objects.filter(user=self.user).count(),
source_service="web",
source_chat_id=source_chat_id,
reply_to=reply_to,
message_meta={},
)
def test_default_submission_creates_run_and_event(self):
trigger = self._msg("#codex# please update #1")
results = async_to_sync(process_inbound_message)(
CommandContext(
service="web",
channel_identifier="web-chan-1",
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)
run = CodexRun.objects.order_by("-created_at").first()
self.assertIsNotNone(run)
self.assertEqual("waiting_approval", run.status)
event = ExternalSyncEvent.objects.order_by("-created_at").first()
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")
results = async_to_sync(process_inbound_message)(
CommandContext(
service="web",
channel_identifier="web-chan-1",
message_id=str(trigger.id),
user_id=self.user.id,
message_text=str(trigger.text),
payload={},
)
)
self.assertEqual(1, len(results))
self.assertFalse(results[0].ok)
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,
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={},
)
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"]},
resume_payload={"resume": True},
status="pending",
)
CommandChannelBinding.objects.create(
profile=self.profile,
direction="ingress",
service="web",
channel_identifier="approver-chan",
enabled=True,
)
trigger = self._msg("#codex approve ak-123#", 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)
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

@@ -1,200 +0,0 @@
from __future__ import annotations
from unittest.mock import patch
from django.test import TestCase
from core.management.commands.codex_worker import Command as CodexWorkerCommand
from core.models import (
CodexPermissionRequest,
CodexRun,
ExternalSyncEvent,
TaskProject,
TaskProviderConfig,
User,
)
from core.tasks.providers.base import ProviderResult
class CodexWorkerPhase1Tests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
"codex-worker-user", "codex-worker@example.com", "x"
)
self.project = TaskProject.objects.create(user=self.user, name="Worker Project")
self.cfg = TaskProviderConfig.objects.create(
user=self.user,
provider="codex_cli",
enabled=True,
settings={
"command": "codex",
"workspace_root": "",
"default_profile": "",
"timeout_seconds": 60,
"chat_link_mode": "task-sync",
"instance_label": "default",
"approver_mode": "channel",
"approver_service": "",
"approver_identifier": "",
},
)
@patch("core.management.commands.codex_worker.get_provider")
def test_pending_to_ok_updates_run(self, get_provider_mock):
run = CodexRun.objects.create(
user=self.user,
project=self.project,
source_service="web",
source_channel="web-chan-1",
status="queued",
request_payload={},
result_payload={},
)
event = ExternalSyncEvent.objects.create(
user=self.user,
provider="codex_cli",
status="pending",
payload={
"action": "append_update",
"provider_payload": {
"codex_run_id": str(run.id),
},
},
)
class _Provider:
run_in_worker = True
def append_update(self, config, payload):
return ProviderResult(
ok=True, payload={"status": "ok", "summary": "done"}
)
create_task = mark_complete = link_task = append_update
get_provider_mock.return_value = _Provider()
CodexWorkerCommand()._run_event(event)
event.refresh_from_db()
run.refresh_from_db()
self.assertEqual("ok", event.status)
self.assertEqual("ok", run.status)
self.assertEqual("done", str(run.result_payload.get("summary") or ""))
@patch("core.management.commands.codex_worker.get_provider")
def test_requires_approval_moves_to_waiting_and_creates_permission_request(
self, get_provider_mock
):
run = CodexRun.objects.create(
user=self.user,
project=self.project,
source_service="web",
source_channel="web-chan-1",
status="queued",
request_payload={},
result_payload={},
)
event = ExternalSyncEvent.objects.create(
user=self.user,
provider="codex_cli",
status="pending",
payload={
"action": "append_update",
"provider_payload": {
"codex_run_id": str(run.id),
},
},
)
class _Provider:
run_in_worker = True
def append_update(self, config, payload):
return ProviderResult(
ok=True,
payload={
"status": "requires_approval",
"requires_approval": True,
"approval_key": "ak-worker-1",
"summary": "needs permissions",
"permission_request": {"requested_permissions": ["write"]},
"resume_payload": {"resume": True},
},
)
create_task = mark_complete = link_task = append_update
get_provider_mock.return_value = _Provider()
CodexWorkerCommand()._run_event(event)
event.refresh_from_db()
run.refresh_from_db()
self.assertEqual("waiting_approval", event.status)
self.assertEqual("waiting_approval", run.status)
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

@@ -32,8 +32,7 @@ class CommandRoutingVariantUITests(TestCase):
self.assertContains(response, "Variant Policies")
self.assertContains(response, "bp set range")
self.assertContains(response, "Send status to egress")
self.assertContains(response, "Codex (codex)")
self.assertContains(response, "Claude (claude)")
self.assertContains(response, "Business Plan (bp)")
def test_variant_policy_update_persists(self):
response = self.client.post(

View File

@@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch
from django.test import TestCase
from django.urls import reverse
from core.models import User
from core.models import ChatSession, Message, Person, PersonIdentifier, User
class ComposeSendCapabilityTests(TestCase):
@@ -78,6 +78,9 @@ class ComposeSendCapabilityTests(TestCase):
self.assertEqual(200, response.status_code)
content = response.content.decode("utf-8")
self.assertIn("compose-panel.css", content)
self.assertIn("compose-panel-core.js", content)
self.assertIn("compose-panel-thread.js", content)
self.assertIn("compose-panel-send.js", content)
self.assertIn("compose-panel.js", content)
self.assertNotIn("const initialTyping = JSON.parse(", content)
self.assertNotIn("data-drafts-url=", content)
@@ -89,6 +92,33 @@ class ComposeSendCapabilityTests(TestCase):
self.assertNotIn("compose-ticks", content)
self.assertNotIn("compose-receipt-modal", content)
def test_compose_widget_declares_compose_assets_on_widget_shell(self):
response = self.client.get(
reverse("compose_widget"),
{
"service": "signal",
"identifier": "+15551230000",
},
)
self.assertEqual(200, response.status_code)
content = response.content.decode("utf-8")
self.assertIn("data-gia-style-hrefs=", content)
self.assertIn("/static/css/compose-panel.css", content)
self.assertIn("data-gia-script-srcs=", content)
self.assertIn("/static/js/compose-panel-core.js", content)
self.assertIn("/static/js/compose-panel-thread.js", content)
self.assertIn("/static/js/compose-panel-send.js", content)
self.assertIn("/static/js/compose-panel.js", content)
self.assertNotIn("<script defer src=\"/static/js/compose-panel.js\">", content)
self.assertNotIn("<link rel=\"stylesheet\" href=\"/static/css/compose-panel.css\">", content)
def test_compose_contacts_dropdown_includes_workspace_link(self):
response = self.client.get(reverse("compose_contacts_dropdown"))
self.assertEqual(200, response.status_code)
self.assertContains(response, reverse("compose_workspace"))
@patch("core.views.compose._recent_manual_contacts")
def test_compose_contact_options_use_compact_service_map(self, mocked_recent_contacts):
mocked_recent_contacts.return_value = [
@@ -129,6 +159,80 @@ class ComposeSendCapabilityTests(TestCase):
self.assertEqual(200, response.status_code)
payload = response.json()
self.assertIn("messages", payload)
self.assertIn("messages_html", payload)
self.assertIn("typing", payload)
self.assertNotIn("availability_slices", payload)
self.assertNotIn("availability_summary", payload)
def test_compose_thread_payload_includes_rendered_message_rows(self):
person = Person.objects.create(user=self.user, name="Rendered Contact")
identifier = PersonIdentifier.objects.create(
user=self.user,
person=person,
service="signal",
identifier="+15551230000",
)
session = ChatSession.objects.create(user=self.user, identifier=identifier)
Message.objects.create(
user=self.user,
session=session,
sender_uuid="contact",
text="Rendered thread row",
ts=1710000000000,
custom_author="CONTACT",
)
response = self.client.get(
reverse("compose_thread"),
{
"service": "signal",
"identifier": "+15551230000",
},
)
self.assertEqual(200, response.status_code)
payload = response.json()
self.assertIsInstance(payload.get("messages_html"), str)
self.assertIn("compose-row", str(payload.get("messages_html") or ""))
self.assertIn("Rendered thread row", str(payload.get("messages_html") or ""))
def test_compose_thread_payload_renders_reply_link_text_server_side(self):
person = Person.objects.create(user=self.user, name="Reply Contact")
identifier = PersonIdentifier.objects.create(
user=self.user,
person=person,
service="signal",
identifier="+15551239999",
)
session = ChatSession.objects.create(user=self.user, identifier=identifier)
anchor = Message.objects.create(
user=self.user,
session=session,
sender_uuid="contact",
text="Anchor message for reply preview",
ts=1710000000000,
custom_author="CONTACT",
)
Message.objects.create(
user=self.user,
session=session,
sender_uuid="self",
text="Reply message",
ts=1710000001000,
custom_author="USER",
reply_to=anchor,
)
response = self.client.get(
reverse("compose_thread"),
{
"service": "signal",
"identifier": "+15551239999",
},
)
self.assertEqual(200, response.status_code)
payload = response.json()
html = str(payload.get("messages_html") or "")
self.assertIn("Reply to: Anchor message for reply preview", html)
self.assertNotIn("data-reply-preview=", html)

View File

@@ -363,62 +363,8 @@ class Phase1CommandEngineTests(TestCase):
self.assertIn("bp", names)
self.assertIn("bp set", names)
self.assertIn("bp set range", names)
self.assertIn("codex", names)
self.assertNotIn("announce task ids", names)
def test_first_user_codex_command_auto_enables_defaults_for_channel(self):
CommandProfile.objects.filter(user=self.user, slug="codex").delete()
msg = Message.objects.create(
user=self.user,
session=self.session,
sender_uuid="",
custom_author="USER",
text="#codex status#",
ts=6000,
source_service="web",
source_chat_id="web-chan-2",
message_meta={},
)
results = async_to_sync(process_inbound_message)(
CommandContext(
service="web",
channel_identifier="web-chan-2",
message_id=str(msg.id),
user_id=self.user.id,
message_text="#codex status#",
payload={},
)
)
self.assertEqual(1, len(results))
self.assertTrue(results[0].ok)
profile = CommandProfile.objects.filter(user=self.user, slug="codex").first()
self.assertIsNotNone(profile)
self.assertTrue(bool(profile.enabled if profile else False))
ingress_exists = CommandChannelBinding.objects.filter(
profile=profile,
direction="ingress",
enabled=True,
service="signal",
channel_identifier="+15550000002",
).exists()
egress_exists = CommandChannelBinding.objects.filter(
profile=profile,
direction="egress",
enabled=True,
service="signal",
channel_identifier="+15550000002",
).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()
msg1 = Message.objects.create(

View File

@@ -3,15 +3,27 @@ from __future__ import annotations
import json
from django.test import TestCase
from django.urls import reverse
from unittest.mock import patch
from core.clients import transport
from core.models import ChatSession, ConversationEvent, Person, PersonIdentifier, PlatformChatLink, User
from core.models import (
ChatSession,
ConversationEvent,
Message,
Person,
PersonIdentifier,
PlatformChatLink,
User,
)
from core.presence import latest_state_for_people
from core.presence.inference import now_ms
from core.views.compose import (
COMPOSE_WORKSPACE_CONTACTS_PAGE_SIZE,
COMPOSE_WORKSPACE_HISTORY_PAGE_SIZE,
_compose_availability_payload,
_context_base,
_filter_manual_contact_rows,
_manual_contact_rows,
_recent_manual_contacts,
)
@@ -78,6 +90,29 @@ class PresenceQueryAndComposeContextTests(TestCase):
self.assertIsNotNone(base["person_identifier"])
self.assertEqual(str(self.person.id), str(base["person"].id))
def test_filter_manual_contact_rows_matches_person_identifier_and_service(self):
rows = [
{
"person_name": "Alice Example",
"linked_person_name": "Alice Example",
"detected_name": "",
"service": "whatsapp",
"identifier": "447700000001",
},
{
"person_name": "Bob Example",
"linked_person_name": "Bob Example",
"detected_name": "",
"service": "signal",
"identifier": "+15551230000",
},
]
filtered = _filter_manual_contact_rows(rows, "alice whatsapp 000001")
self.assertEqual(1, len(filtered))
self.assertEqual("Alice Example", filtered[0]["person_name"])
@patch("core.views.compose._manual_contact_rows")
def test_recent_manual_contacts_keeps_current_person_name_for_signal_alias(
self, mocked_manual_contact_rows
@@ -433,3 +468,147 @@ class PresenceQueryAndComposeContextTests(TestCase):
self.assertTrue(bool(rows[0].get("linked_person")))
self.assertEqual("Mapped Contact", str(rows[0].get("linked_person_name") or ""))
self.assertEqual("Detected Contact", str(rows[0].get("detected_name") or ""))
class ComposeWorkspaceContactsWidgetTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
"workspace-user",
"workspace@example.com",
"x",
)
self.client.force_login(self.user)
for index in range(65):
person = Person.objects.create(
user=self.user,
name=f"Person {index:03d}",
)
PersonIdentifier.objects.create(
user=self.user,
person=person,
service="whatsapp",
identifier=f"4477000{index:03d}",
)
def test_workspace_contact_widget_limits_initial_render(self):
response = self.client.get(reverse("compose_workspace_contacts_widget"))
self.assertEqual(200, response.status_code)
content = response.content.decode()
self.assertIn("Person 000", content)
self.assertIn(
f"Showing {COMPOSE_WORKSPACE_CONTACTS_PAGE_SIZE} contacts.",
content,
)
self.assertNotIn("Window", content)
self.assertNotIn("Match</span>", content)
self.assertNotIn(f"Person {COMPOSE_WORKSPACE_CONTACTS_PAGE_SIZE:03d}", content)
self.assertIn(
f"Show {COMPOSE_WORKSPACE_CONTACTS_PAGE_SIZE} more",
content,
)
def test_workspace_contact_widget_results_fragment_filters_rows(self):
response = self.client.get(
reverse("compose_workspace_contacts_widget"),
{"fragment": "results", "q": "Person 064"},
)
self.assertEqual(200, response.status_code)
content = response.content.decode()
self.assertIn("Person 064", content)
self.assertNotIn("Person 000", content)
self.assertNotIn('id="widget-', content)
def test_compose_workspace_page_uses_shared_workspace_shell_asset(self):
response = self.client.get(reverse("compose_workspace"))
self.assertEqual(200, response.status_code)
content = response.content.decode()
self.assertIn("js/workspace-shell.js", content)
self.assertNotIn("window.giaPrepareWidgetTarget = function", content)
class ComposeWorkspaceHistoryWidgetTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
"history-user",
"history@example.com",
"x",
)
self.client.force_login(self.user)
self.person_signal = Person.objects.create(user=self.user, name="Ada Signal")
self.person_whatsapp = Person.objects.create(
user=self.user,
name="Willa WhatsApp",
)
self.signal_identifier = PersonIdentifier.objects.create(
user=self.user,
person=self.person_signal,
service="signal",
identifier="15551230001",
)
self.whatsapp_identifier = PersonIdentifier.objects.create(
user=self.user,
person=self.person_whatsapp,
service="whatsapp",
identifier="447700000001",
)
self.signal_session = ChatSession.objects.create(
user=self.user,
identifier=self.signal_identifier,
)
self.whatsapp_session = ChatSession.objects.create(
user=self.user,
identifier=self.whatsapp_identifier,
)
base_ts = now_ms()
for index in range(32):
Message.objects.create(
user=self.user,
session=self.signal_session,
ts=base_ts + index,
text=f"Ada message {index:02d}",
custom_author="USER" if index % 2 else "",
)
Message.objects.create(
user=self.user,
session=self.whatsapp_session,
ts=base_ts - 1000,
text="WhatsApp follow up",
custom_author="",
)
def test_history_widget_limits_initial_render(self):
response = self.client.get(reverse("compose_workspace_history_widget"))
self.assertEqual(200, response.status_code)
content = response.content.decode()
self.assertIn("Ada message 31", content)
lower_bound = 32 - COMPOSE_WORKSPACE_HISTORY_PAGE_SIZE
self.assertIn(f"Ada message {lower_bound:02d}", content)
self.assertNotIn(f"Ada message {lower_bound - 1:02d}", content)
self.assertIn("Show more history", content)
def test_history_widget_filters_by_service_and_fragment_mode(self):
response = self.client.get(
reverse("compose_workspace_history_widget"),
{"fragment": "results", "service": "whatsapp", "q": "follow up"},
)
self.assertEqual(200, response.status_code)
content = response.content.decode()
self.assertIn("WhatsApp follow up", content)
self.assertNotIn("Ada message 32", content)
self.assertNotIn('id="widget-', content)
def test_history_widget_respects_person_scope(self):
response = self.client.get(
reverse("compose_workspace_history_widget"),
{"person": str(self.person_signal.id)},
)
self.assertEqual(200, response.status_code)
content = response.content.decode()
self.assertIn("Scoped to <strong>Ada Signal</strong>.", content)
self.assertNotIn("WhatsApp follow up", content)

View File

@@ -30,8 +30,8 @@ class SettingsIntegrityTests(TestCase):
def test_capability_registry_excludes_removed_totp_scope(self):
self.assertNotIn("gateway.totp", all_scope_keys())
def test_codex_settings_receives_modules_settings_nav(self):
response = self.client.get(reverse("codex_settings"))
def test_tasks_settings_receives_modules_settings_nav(self):
response = self.client.get(reverse("tasks_settings"))
self.assertEqual(200, response.status_code)
settings_nav = response.context.get("settings_nav")
self.assertIsNotNone(settings_nav)
@@ -39,6 +39,9 @@ class SettingsIntegrityTests(TestCase):
labels = [str(item["label"]) for item in settings_nav["tabs"]]
self.assertIn("Commands", labels)
self.assertIn("Task Automation", labels)
self.assertNotIn("Codex", labels)
group_labels = [str(item["label"]) for item in settings_nav["groups"]]
self.assertEqual(["General", "Security", "AI", "Modules"], group_labels)
def test_business_plan_inbox_receives_modules_settings_nav(self):
response = self.client.get(reverse("business_plan_inbox"))
@@ -46,6 +49,8 @@ class SettingsIntegrityTests(TestCase):
settings_nav = response.context.get("settings_nav")
self.assertIsNotNone(settings_nav)
self.assertEqual("Modules", settings_nav["title"])
active_tabs = [str(item["label"]) for item in settings_nav["tabs"] if item["active"]]
self.assertEqual(["Business Plans"], active_tabs)
def test_behavioral_settings_receives_modules_settings_nav(self):
response = self.client.get(reverse("behavioral_signals_settings"))
@@ -56,6 +61,17 @@ class SettingsIntegrityTests(TestCase):
labels = [str(item["label"]) for item in settings_nav["tabs"]]
self.assertIn("Behavioral Signals", labels)
def test_security_settings_receives_group_and_child_tabs(self):
response = self.client.get(reverse("encryption_settings"))
self.assertEqual(200, response.status_code)
settings_nav = response.context.get("settings_nav")
self.assertIsNotNone(settings_nav)
self.assertEqual("Security", settings_nav["title"])
active_groups = [str(item["label"]) for item in settings_nav["groups"] if item["active"]]
self.assertEqual(["Security"], active_groups)
active_tabs = [str(item["label"]) for item in settings_nav["tabs"] if item["active"]]
self.assertEqual(["Encryption"], active_tabs)
def test_tasks_settings_cross_links_commands_and_permissions(self):
TaskProject.objects.create(user=self.user, name="Integrity Project")
response = self.client.get(reverse("tasks_settings"))
@@ -79,12 +95,14 @@ class SettingsIntegrityTests(TestCase):
self.assertContains(response, reverse("tasks_settings"))
self.assertContains(response, reverse("permission_settings"))
def test_settings_nav_includes_codex_approval_route(self):
request = self.factory.post(reverse("codex_approval"))
def test_settings_nav_includes_tasks_route(self):
request = self.factory.get(reverse("tasks_settings"))
request.user = self.user
request.resolver_match = resolve(reverse("codex_approval"))
request.resolver_match = resolve(reverse("tasks_settings"))
settings_nav = settings_hierarchy_nav(request)["settings_nav"]
self.assertEqual("Modules", settings_nav["title"])
active_tabs = [str(item["label"]) for item in settings_nav["tabs"] if item["active"]]
self.assertEqual(["Task Automation"], active_tabs)
def test_settings_nav_includes_translation_preview_route(self):
request = self.factory.post(reverse("translation_preview"))
@@ -92,3 +110,18 @@ class SettingsIntegrityTests(TestCase):
request.resolver_match = resolve(reverse("translation_preview"))
settings_nav = settings_hierarchy_nav(request)["settings_nav"]
self.assertEqual("Modules", settings_nav["title"])
active_tabs = [str(item["label"]) for item in settings_nav["tabs"] if item["active"]]
self.assertEqual(["Translation"], active_tabs)
def test_settings_pages_with_shared_shell_render_section_container(self):
for route_name in (
"notifications_settings",
"security_2fa",
"ai_models",
"ai_execution_log",
):
with self.subTest(route_name=route_name):
response = self.client.get(reverse(route_name))
self.assertEqual(200, response.status_code)
self.assertContains(response, '<section class="section">', html=False)
self.assertContains(response, '<div class="container">', html=False)

View File

@@ -11,6 +11,7 @@ from core.models import (
ChatTaskSource,
DerivedTask,
DerivedTaskEvent,
ExternalSyncEvent,
Message,
Person,
PersonIdentifier,
@@ -228,8 +229,6 @@ class TaskEngineMemoryContextTests(TestCase):
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 = [
{
@@ -244,11 +243,13 @@ class TaskEngineMemoryContextTests(TestCase):
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 {}
event = ExternalSyncEvent.objects.filter(task=task).order_by("-created_at").first()
self.assertIsNotNone(event, "Expected ExternalSyncEvent created for task")
provider_payload = (
((event.payload or {}).get("provider_payload") or {}) if event else {}
)
memory_context = provider_payload.get("memory_context")
self.assertIsNotNone(
memory_context, "Expected memory_context in CodexRun provider payload"
memory_context, "Expected memory_context in sync event payload"
)
self.assertEqual(1, len(memory_context))

View File

@@ -1,9 +0,0 @@
from django.test import SimpleTestCase
from core.management.commands.codex_worker import Command as LegacyWorkerCommand
from core.management.commands.task_sync_worker import Command as TaskSyncWorkerCommand
class TaskSyncWorkerCommandAliasTests(SimpleTestCase):
def test_task_sync_worker_is_legacy_worker_alias(self):
self.assertTrue(issubclass(TaskSyncWorkerCommand, LegacyWorkerCommand))

View File

@@ -9,11 +9,7 @@ from django.urls import reverse
from core.models import (
ChatSession,
ChatTaskSource,
CodexPermissionRequest,
CodexRun,
DerivedTask,
ExternalChatLink,
ExternalSyncEvent,
Message,
Person,
PersonIdentifier,
@@ -134,7 +130,10 @@ class TaskAnnounceToggleTests(TestCase):
self.assertIn("bp", names)
self.assertIn("bp set", names)
self.assertIn("bp set range", names)
self.assertIn("codex", names)
self.assertEqual(
{"bp", "bp set", "bp set range"},
{name for name in names if name.startswith("bp")},
)
@override_settings(TASK_DERIVATION_USE_AI=False)
@@ -402,200 +401,24 @@ class TaskHubEmptyProjectVisibilityTests(TestCase):
self.assertEqual(["Empty", "Used"], names)
class TaskSettingsExternalChatLinkScopeTests(TestCase):
class MockProviderSettingsTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
"task-link-user", "task-link@example.com", "x"
"task-provider-user", "task-provider@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()
)
class CodexSettingsAndSubmitTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
"codex-settings-user", "codex-settings@example.com", "x"
)
self.client.force_login(self.user)
self.project = TaskProject.objects.create(user=self.user, name="Codex Project")
self.task = DerivedTask.objects.create(
user=self.user,
project=self.project,
epic=None,
title="Task X",
source_service="web",
source_channel="web-chan-1",
reference_code="11",
status_snapshot="open",
)
def test_provider_update_persists_phase1_codex_settings(self):
def test_provider_update_persists_mock_enabled_state(self):
response = self.client.post(
reverse("tasks_settings"),
{
"action": "provider_update",
"provider": "codex_cli",
"provider": "mock",
"enabled": "1",
"command": "codex",
"workspace_root": "/code/xf",
"default_profile": "default",
"timeout_seconds": "120",
"instance_label": "team-a",
"approver_service": "web",
"approver_identifier": "approver-chan",
},
follow=True,
)
self.assertEqual(200, response.status_code)
cfg = TaskProviderConfig.objects.get(user=self.user, provider="codex_cli")
cfg = TaskProviderConfig.objects.get(user=self.user, provider="mock")
self.assertTrue(cfg.enabled)
self.assertEqual("team-a", str(cfg.settings.get("instance_label") or ""))
self.assertEqual("web", str(cfg.settings.get("approver_service") or ""))
self.assertEqual(
"approver-chan", str(cfg.settings.get("approver_identifier") or "")
)
def test_task_submit_endpoint_creates_codex_run_and_event(self):
TaskProviderConfig.objects.create(
user=self.user,
provider="codex_cli",
enabled=True,
settings={"command": "codex", "timeout_seconds": 60},
)
response = self.client.post(
reverse("tasks_codex_submit"),
{
"task_id": str(self.task.id),
"next": reverse("tasks_hub"),
},
follow=True,
)
self.assertEqual(200, response.status_code)
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,
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={},
)
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"]},
resume_payload={"resume": True},
status="pending",
)
response = self.client.get(reverse("codex_settings"))
self.assertEqual(200, response.status_code)
self.assertContains(response, "Codex Status")
response = self.client.post(
reverse("codex_approval"),
{"request_id": str(req.id), "decision": "approve"},
follow=True,
)
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)
self.assertEqual({}, dict(cfg.settings or {}))

View File

@@ -1,194 +0,0 @@
from __future__ import annotations
from unittest.mock import MagicMock
from asgiref.sync import async_to_sync
from django.test import TestCase
from core.gateway.builtin import (
gateway_help_lines,
handle_approval_command,
handle_tasks_command,
)
from core.models import (
CodexPermissionRequest,
CodexRun,
DerivedTask,
ExternalSyncEvent,
TaskProject,
User,
)
class XMPPGatewayApprovalCommandTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
"xmpp-approval-user", "xmpp-approval@example.com", "x"
)
self.project = TaskProject.objects.create(
user=self.user, name="Approval Project"
)
self.task = DerivedTask.objects.create(
user=self.user,
project=self.project,
epic=None,
title="Approve me",
source_service="xmpp",
source_channel="component.example.test",
reference_code="77",
status_snapshot="open",
)
self.waiting_event = ExternalSyncEvent.objects.create(
user=self.user,
task=self.task,
provider="codex_cli",
status="waiting_approval",
payload={},
error="",
)
self.run = CodexRun.objects.create(
user=self.user,
task=self.task,
project=self.project,
source_service="xmpp",
source_channel="component.example.test",
status="waiting_approval",
request_payload={
"action": "append_update",
"provider_payload": {"task_id": str(self.task.id)},
},
result_payload={},
)
self.request = CodexPermissionRequest.objects.create(
user=self.user,
codex_run=self.run,
external_sync_event=self.waiting_event,
approval_key="ak-xmpp-1",
summary="Need auth approval",
requested_permissions={"items": ["workspace_write"]},
resume_payload={},
status="pending",
)
self.probe = MagicMock()
def _run_command(self, text: str) -> list[str]:
messages = []
def _sym(value):
messages.append(str(value))
handled = async_to_sync(handle_approval_command)(
self.user,
text,
_sym,
)
self.assertTrue(handled)
self.assertTrue(messages)
return messages
def test_approval_approve_command_resolves_request_and_queues_resume(self):
rows = self._run_command(".approval approve ak-xmpp-1")
self.assertIn("approved", "\n".join(rows).lower())
self.request.refresh_from_db()
self.run.refresh_from_db()
self.waiting_event.refresh_from_db()
self.assertEqual("approved", self.request.status)
self.assertEqual("approved_waiting_resume", self.run.status)
self.assertEqual("ok", self.waiting_event.status)
resume = ExternalSyncEvent.objects.filter(
idempotency_key="codex_approval:ak-xmpp-1:approved"
).first()
self.assertIsNotNone(resume)
self.assertEqual("pending", resume.status)
def test_approval_list_pending_and_status(self):
rows = self._run_command(".approval list-pending all")
text = "\n".join(rows)
self.assertIn("pending=1", text)
self.assertIn("ak-xmpp-1", text)
status_rows = self._run_command(".approval status ak-xmpp-1")
self.assertIn("status=pending", "\n".join(status_rows))
def test_provider_specific_command_rejects_mismatched_key(self):
rows = self._run_command(".claude approve ak-xmpp-1")
self.assertIn("approval_key_not_for_provider", "\n".join(rows))
self.request.refresh_from_db()
self.assertEqual("pending", self.request.status)
class XMPPGatewayTasksCommandTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
"xmpp-task-user", "xmpp-task@example.com", "x"
)
self.project = TaskProject.objects.create(user=self.user, name="Task Project")
self.task = DerivedTask.objects.create(
user=self.user,
project=self.project,
epic=None,
title="Ship CLI",
source_service="xmpp",
source_channel="component.example.test",
reference_code="12",
status_snapshot="open",
)
self.probe = MagicMock()
def _run_tasks(self, text: str) -> list[str]:
messages = []
def _sym(value):
messages.append(str(value))
handled = async_to_sync(handle_tasks_command)(
self.user,
text,
_sym,
)
self.assertTrue(handled)
self.assertTrue(messages)
return messages
def test_help_contains_approval_and_tasks_sections(self):
lines = gateway_help_lines()
text = "\n".join(lines)
self.assertIn(".approval list-pending", text)
self.assertIn(".tasks list", text)
self.assertIn(".tasks add", text)
self.assertIn(".l", text)
def test_tasks_list_show_complete_and_undo(self):
rows = self._run_tasks(".tasks list open 10")
self.assertIn("#12", "\n".join(rows))
rows = self._run_tasks(".l")
self.assertIn("#12", "\n".join(rows))
rows = self._run_tasks(".tasks show #12")
self.assertIn("status: open", "\n".join(rows))
rows = self._run_tasks(".tasks complete #12")
self.assertIn("completed #12", "\n".join(rows))
self.task.refresh_from_db()
self.assertEqual("completed", self.task.status_snapshot)
rows = self._run_tasks(".tasks undo #12")
self.assertIn("removed #12", "\n".join(rows))
self.assertFalse(DerivedTask.objects.filter(id=self.task.id).exists())
def test_tasks_add_creates_task_in_named_project(self):
rows = []
handled = async_to_sync(handle_tasks_command)(
self.user,
".tasks add Task Project :: Wire XMPP manual task create",
lambda value: rows.append(str(value)),
service="xmpp",
channel_identifier="component.example.test",
sender_identifier="operator@example.test",
)
self.assertTrue(handled)
self.assertTrue(any("created #" in row.lower() for row in rows))
created = DerivedTask.objects.filter(
user=self.user,
project=self.project,
title="Wire XMPP manual task create",
source_service="xmpp",
source_channel="component.example.test",
).first()
self.assertIsNotNone(created)