Implement plans
This commit is contained in:
@@ -79,6 +79,13 @@ class BPSubcommandTests(TransactionTestCase):
|
||||
parsed = parse_bp_subcommand("#bp set range# now")
|
||||
self.assertEqual("set_range", parsed.command)
|
||||
|
||||
def test_parser_detects_dot_prefix_forms(self):
|
||||
parsed = parse_bp_subcommand(".BP set addendum text")
|
||||
self.assertEqual("set", parsed.command)
|
||||
self.assertEqual("addendum text", parsed.remainder_text)
|
||||
parsed_range = parse_bp_subcommand(".bp set range")
|
||||
self.assertEqual("set_range", parsed_range.command)
|
||||
|
||||
def test_set_standalone_uses_remainder_only(self):
|
||||
trigger = Message.objects.create(
|
||||
user=self.user,
|
||||
|
||||
@@ -58,3 +58,16 @@ class CodexCLITaskProviderTests(SimpleTestCase):
|
||||
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"))
|
||||
|
||||
193
core/tests/test_codex_commands_phase1.py
Normal file
193
core/tests/test_codex_commands_phase1.py
Normal file
@@ -0,0 +1,193 @@
|
||||
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,
|
||||
CommandChannelBinding,
|
||||
CommandProfile,
|
||||
CodexPermissionRequest,
|
||||
CodexRun,
|
||||
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("queued", run.status)
|
||||
event = ExternalSyncEvent.objects.order_by("-created_at").first()
|
||||
self.assertEqual("pending", event.status)
|
||||
self.assertEqual("default", str((event.payload or {}).get("provider_payload", {}).get("mode") or ""))
|
||||
|
||||
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):
|
||||
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,
|
||||
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()
|
||||
self.assertEqual("approved", req.status)
|
||||
self.assertEqual("approved_waiting_resume", run.status)
|
||||
self.assertTrue(
|
||||
ExternalSyncEvent.objects.filter(idempotency_key="codex_approval:ak-123:approved", status="pending").exists()
|
||||
)
|
||||
123
core/tests/test_codex_worker_phase1.py
Normal file
123
core/tests/test_codex_worker_phase1.py
Normal file
@@ -0,0 +1,123 @@
|
||||
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))
|
||||
@@ -32,6 +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)")
|
||||
|
||||
def test_variant_policy_update_persists(self):
|
||||
response = self.client.post(
|
||||
|
||||
227
core/tests/test_compose_react.py
Normal file
227
core/tests/test_compose_react.py
Normal file
@@ -0,0 +1,227 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from core.models import ChatSession, Message, Person, PersonIdentifier, User
|
||||
|
||||
|
||||
class ComposeReactTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user("compose-react", "react@example.com", "pw")
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def _build_message(self, *, service: str, identifier: str, source_message_id: str = ""):
|
||||
person = Person.objects.create(user=self.user, name=f"{service} person")
|
||||
person_identifier = PersonIdentifier.objects.create(
|
||||
user=self.user,
|
||||
person=person,
|
||||
service=service,
|
||||
identifier=identifier,
|
||||
)
|
||||
session = ChatSession.objects.create(
|
||||
user=self.user,
|
||||
identifier=person_identifier,
|
||||
)
|
||||
message = Message.objects.create(
|
||||
user=self.user,
|
||||
session=session,
|
||||
ts=1770000000123,
|
||||
sender_uuid="sender-1",
|
||||
text="hello",
|
||||
source_service=service,
|
||||
source_message_id=source_message_id or "",
|
||||
source_chat_id=identifier,
|
||||
receipt_payload={},
|
||||
)
|
||||
return person, person_identifier, message
|
||||
|
||||
@patch("core.views.compose.history.apply_reaction", new_callable=AsyncMock)
|
||||
@patch("core.views.compose.transport.send_reaction", new_callable=AsyncMock)
|
||||
def test_signal_react_uses_numeric_source_message_id_timestamp(
|
||||
self, mocked_send_reaction, mocked_apply_reaction
|
||||
):
|
||||
person, _, message = self._build_message(
|
||||
service="signal",
|
||||
identifier="+15551230000",
|
||||
source_message_id="1771234567000",
|
||||
)
|
||||
mocked_send_reaction.return_value = True
|
||||
mocked_apply_reaction.return_value = message
|
||||
|
||||
response = self.client.post(
|
||||
reverse("compose_react"),
|
||||
{
|
||||
"service": "signal",
|
||||
"identifier": "+15551230000",
|
||||
"person": str(person.id),
|
||||
"message_id": str(message.id),
|
||||
"emoji": "👍",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(200, response.status_code)
|
||||
payload = response.json()
|
||||
self.assertTrue(payload["ok"])
|
||||
self.assertEqual("👍", payload["emoji"])
|
||||
self.assertFalse(payload["remove"])
|
||||
mocked_send_reaction.assert_awaited_once()
|
||||
_, kwargs = mocked_send_reaction.await_args
|
||||
self.assertEqual("signal", mocked_send_reaction.await_args.args[0])
|
||||
self.assertEqual("+15551230000", mocked_send_reaction.await_args.args[1])
|
||||
self.assertEqual(1771234567000, kwargs["target_timestamp"])
|
||||
self.assertEqual("sender-1", kwargs["target_author"])
|
||||
self.assertEqual("", kwargs["target_message_id"])
|
||||
|
||||
@patch("core.views.compose.history.apply_reaction", new_callable=AsyncMock)
|
||||
@patch("core.views.compose.transport.send_reaction", new_callable=AsyncMock)
|
||||
def test_whatsapp_react_uses_upstream_message_id(
|
||||
self, mocked_send_reaction, mocked_apply_reaction
|
||||
):
|
||||
person, _, message = self._build_message(
|
||||
service="whatsapp",
|
||||
identifier="12345@s.whatsapp.net",
|
||||
source_message_id="wamid.ABC123",
|
||||
)
|
||||
mocked_send_reaction.return_value = True
|
||||
mocked_apply_reaction.return_value = message
|
||||
|
||||
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)
|
||||
payload = response.json()
|
||||
self.assertTrue(payload["ok"])
|
||||
self.assertEqual("wamid.ABC123", payload["target_upstream_id"])
|
||||
mocked_send_reaction.assert_awaited_once()
|
||||
_, kwargs = mocked_send_reaction.await_args
|
||||
self.assertEqual("wamid.ABC123", kwargs["target_message_id"])
|
||||
|
||||
@patch("core.views.compose.transport.send_reaction", new_callable=AsyncMock)
|
||||
def test_toggle_removes_existing_actor_reaction(self, mocked_send_reaction):
|
||||
person, _, message = self._build_message(
|
||||
service="signal",
|
||||
identifier="+15551230000",
|
||||
source_message_id="1771234567000",
|
||||
)
|
||||
message.receipt_payload = {
|
||||
"reactions": [
|
||||
{
|
||||
"emoji": "👍",
|
||||
"actor": f"web:{self.user.id}:signal",
|
||||
"source_service": "signal",
|
||||
"removed": False,
|
||||
}
|
||||
]
|
||||
}
|
||||
message.save(update_fields=["receipt_payload"])
|
||||
mocked_send_reaction.return_value = True
|
||||
|
||||
with patch(
|
||||
"core.views.compose.history.apply_reaction",
|
||||
new=AsyncMock(return_value=message),
|
||||
):
|
||||
response = self.client.post(
|
||||
reverse("compose_react"),
|
||||
{
|
||||
"service": "signal",
|
||||
"identifier": "+15551230000",
|
||||
"person": str(person.id),
|
||||
"message_id": str(message.id),
|
||||
"emoji": "👍",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(200, response.status_code)
|
||||
payload = response.json()
|
||||
self.assertTrue(payload["ok"])
|
||||
self.assertTrue(payload["remove"])
|
||||
_, kwargs = mocked_send_reaction.await_args
|
||||
self.assertTrue(kwargs["remove"])
|
||||
|
||||
def test_unsupported_service_returns_error(self):
|
||||
response = self.client.post(
|
||||
reverse("compose_react"),
|
||||
{
|
||||
"service": "xmpp",
|
||||
"identifier": "someone@example.com",
|
||||
"message_id": "does-not-matter",
|
||||
"emoji": "👍",
|
||||
},
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual(
|
||||
{"ok": False, "error": "service_not_supported"},
|
||||
response.json(),
|
||||
)
|
||||
|
||||
def test_missing_whatsapp_target_returns_error(self):
|
||||
person, _, message = self._build_message(
|
||||
service="whatsapp",
|
||||
identifier="12345@s.whatsapp.net",
|
||||
source_message_id="",
|
||||
)
|
||||
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(),
|
||||
)
|
||||
|
||||
def test_compose_page_renders_reaction_actions_for_signal(self):
|
||||
person, _, _ = self._build_message(
|
||||
service="signal",
|
||||
identifier="+15551230000",
|
||||
source_message_id="1771234567000",
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse("compose_page"),
|
||||
{
|
||||
"service": "signal",
|
||||
"identifier": "+15551230000",
|
||||
"person": str(person.id),
|
||||
},
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
content = response.content.decode("utf-8")
|
||||
self.assertIn("data-react-url=", content)
|
||||
self.assertIn("compose-reaction-actions", content)
|
||||
|
||||
def test_compose_page_hides_reaction_actions_for_unsupported_service(self):
|
||||
person, _, _ = self._build_message(
|
||||
service="xmpp",
|
||||
identifier="person@example.com",
|
||||
source_message_id="msg-1",
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse("compose_page"),
|
||||
{
|
||||
"service": "xmpp",
|
||||
"identifier": "person@example.com",
|
||||
"person": str(person.id),
|
||||
},
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertNotIn(
|
||||
'class="compose-reaction-actions"',
|
||||
response.content.decode("utf-8"),
|
||||
)
|
||||
@@ -9,8 +9,10 @@ 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 (
|
||||
ChatSession,
|
||||
CommandAction,
|
||||
CommandChannelBinding,
|
||||
CommandProfile,
|
||||
CommandVariantPolicy,
|
||||
Message,
|
||||
Person,
|
||||
PersonIdentifier,
|
||||
@@ -313,4 +315,124 @@ 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)
|
||||
|
||||
def test_first_user_bp_command_auto_setup_is_idempotent(self):
|
||||
CommandProfile.objects.filter(user=self.user, slug="bp").delete()
|
||||
msg1 = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="",
|
||||
custom_author="USER",
|
||||
text="#bp#",
|
||||
ts=7000,
|
||||
source_service="web",
|
||||
source_chat_id="web-chan-3",
|
||||
message_meta={},
|
||||
)
|
||||
first_results = async_to_sync(process_inbound_message)(
|
||||
CommandContext(
|
||||
service="web",
|
||||
channel_identifier="web-chan-3",
|
||||
message_id=str(msg1.id),
|
||||
user_id=self.user.id,
|
||||
message_text="#bp#",
|
||||
payload={},
|
||||
)
|
||||
)
|
||||
self.assertEqual(1, len(first_results))
|
||||
self.assertEqual("reply_required", first_results[0].error)
|
||||
profile = CommandProfile.objects.filter(user=self.user, slug="bp").first()
|
||||
self.assertIsNotNone(profile)
|
||||
if profile is None:
|
||||
return
|
||||
self.assertEqual(3, CommandAction.objects.filter(profile=profile).count())
|
||||
self.assertEqual(3, CommandVariantPolicy.objects.filter(profile=profile).count())
|
||||
self.assertEqual(
|
||||
2,
|
||||
CommandChannelBinding.objects.filter(
|
||||
profile=profile,
|
||||
service="signal",
|
||||
channel_identifier="+15550000002",
|
||||
).count(),
|
||||
)
|
||||
|
||||
msg2 = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="",
|
||||
custom_author="USER",
|
||||
text="#bp#",
|
||||
ts=8000,
|
||||
source_service="web",
|
||||
source_chat_id="web-chan-3",
|
||||
message_meta={},
|
||||
)
|
||||
second_results = async_to_sync(process_inbound_message)(
|
||||
CommandContext(
|
||||
service="web",
|
||||
channel_identifier="web-chan-3",
|
||||
message_id=str(msg2.id),
|
||||
user_id=self.user.id,
|
||||
message_text="#bp#",
|
||||
payload={},
|
||||
)
|
||||
)
|
||||
self.assertEqual(1, len(second_results))
|
||||
self.assertEqual("reply_required", second_results[0].error)
|
||||
self.assertEqual(3, CommandAction.objects.filter(profile=profile).count())
|
||||
self.assertEqual(3, CommandVariantPolicy.objects.filter(profile=profile).count())
|
||||
self.assertEqual(
|
||||
2,
|
||||
CommandChannelBinding.objects.filter(
|
||||
profile=profile,
|
||||
service="signal",
|
||||
channel_identifier="+15550000002",
|
||||
).count(),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
@@ -13,6 +15,7 @@ from core.models import (
|
||||
Person,
|
||||
PersonIdentifier,
|
||||
TaskCompletionPattern,
|
||||
TaskEpic,
|
||||
TaskProject,
|
||||
User,
|
||||
Message,
|
||||
@@ -197,3 +200,208 @@ class TaskEngineTests(TestCase):
|
||||
DerivedTask.objects.filter(origin_message=m).exists(),
|
||||
"Expected Signal UUID source chat to match source mapping by companion number.",
|
||||
)
|
||||
|
||||
def test_lenient_prefix_parsing_allows_hash_todo_in_strict_mode(self):
|
||||
source = ChatTaskSource.objects.filter(
|
||||
user=self.user,
|
||||
service="whatsapp",
|
||||
channel_identifier="120363402761690215@g.us",
|
||||
).first()
|
||||
source.settings = {
|
||||
"derive_enabled": True,
|
||||
"match_mode": "strict",
|
||||
"require_prefix": True,
|
||||
"allowed_prefixes": ["task:", "todo:"],
|
||||
"ai_title_enabled": False,
|
||||
}
|
||||
source.save(update_fields=["settings", "updated_at"])
|
||||
m = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="peer",
|
||||
text="#todo: rotate SSL certs",
|
||||
ts=1400,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215@g.us",
|
||||
)
|
||||
async_to_sync(process_inbound_task_intelligence)(m)
|
||||
task = DerivedTask.objects.filter(origin_message=m).first()
|
||||
self.assertIsNotNone(task)
|
||||
self.assertEqual("rotate SSL certs", str(task.title or ""))
|
||||
|
||||
def test_lenient_prefix_parsing_allows_task_dash_form(self):
|
||||
source = ChatTaskSource.objects.filter(
|
||||
user=self.user,
|
||||
service="whatsapp",
|
||||
channel_identifier="120363402761690215@g.us",
|
||||
).first()
|
||||
source.settings = {
|
||||
"derive_enabled": True,
|
||||
"match_mode": "strict",
|
||||
"require_prefix": True,
|
||||
"allowed_prefixes": ["task:", "todo:"],
|
||||
"ai_title_enabled": False,
|
||||
}
|
||||
source.save(update_fields=["settings", "updated_at"])
|
||||
m = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="peer",
|
||||
text="task - setup password sharing",
|
||||
ts=1500,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215@g.us",
|
||||
)
|
||||
async_to_sync(process_inbound_task_intelligence)(m)
|
||||
task = DerivedTask.objects.filter(origin_message=m).first()
|
||||
self.assertIsNotNone(task)
|
||||
self.assertEqual("setup password sharing", str(task.title or ""))
|
||||
|
||||
def test_lenient_prefix_parsing_allows_double_dot_task_form(self):
|
||||
source = ChatTaskSource.objects.filter(
|
||||
user=self.user,
|
||||
service="whatsapp",
|
||||
channel_identifier="120363402761690215@g.us",
|
||||
).first()
|
||||
source.settings = {
|
||||
"derive_enabled": True,
|
||||
"match_mode": "strict",
|
||||
"require_prefix": True,
|
||||
"allowed_prefixes": ["task:", "todo:"],
|
||||
"ai_title_enabled": False,
|
||||
}
|
||||
source.save(update_fields=["settings", "updated_at"])
|
||||
m = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="peer",
|
||||
text="..task setup password sharing",
|
||||
ts=1600,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215@g.us",
|
||||
)
|
||||
async_to_sync(process_inbound_task_intelligence)(m)
|
||||
task = DerivedTask.objects.filter(origin_message=m).first()
|
||||
self.assertIsNotNone(task)
|
||||
self.assertEqual("setup password sharing", str(task.title or ""))
|
||||
|
||||
@patch("core.tasks.engine.send_message_raw", new_callable=AsyncMock)
|
||||
def test_dot_l_lists_tasks_in_scope(self, mocked_send):
|
||||
seed = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="peer",
|
||||
text="task: rotate keys",
|
||||
ts=1700,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215@g.us",
|
||||
)
|
||||
async_to_sync(process_inbound_task_intelligence)(seed)
|
||||
cmd = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="peer",
|
||||
text=".l",
|
||||
ts=1701,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215@g.us",
|
||||
)
|
||||
async_to_sync(process_inbound_task_intelligence)(cmd)
|
||||
self.assertTrue(mocked_send.await_count >= 1)
|
||||
list_payloads = [str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list]
|
||||
self.assertTrue(any("open tasks" in row.lower() for row in list_payloads))
|
||||
self.assertTrue(any("#1" in row for row in list_payloads))
|
||||
|
||||
@patch("core.tasks.engine.send_message_raw", new_callable=AsyncMock)
|
||||
def test_dot_undo_uncreates_latest_task(self, mocked_send):
|
||||
m1 = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="peer",
|
||||
text="task: one",
|
||||
ts=1800,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215@g.us",
|
||||
)
|
||||
m2 = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="peer",
|
||||
text="task: two",
|
||||
ts=1801,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215@g.us",
|
||||
)
|
||||
async_to_sync(process_inbound_task_intelligence)(m1)
|
||||
async_to_sync(process_inbound_task_intelligence)(m2)
|
||||
self.assertEqual(2, DerivedTask.objects.filter(user=self.user, project=self.project).count())
|
||||
cmd = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="peer",
|
||||
text=".undo",
|
||||
ts=1802,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215@g.us",
|
||||
)
|
||||
async_to_sync(process_inbound_task_intelligence)(cmd)
|
||||
remaining = list(
|
||||
DerivedTask.objects.filter(user=self.user, project=self.project)
|
||||
.order_by("created_at")
|
||||
.values_list("title", flat=True)
|
||||
)
|
||||
self.assertEqual(1, len(remaining))
|
||||
self.assertEqual("one", remaining[0])
|
||||
payloads = [str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list]
|
||||
self.assertTrue(any("removed #2" in row.lower() for row in payloads))
|
||||
|
||||
@patch("core.tasks.engine.send_message_raw", new_callable=AsyncMock)
|
||||
def test_every_tenth_task_sends_l_and_undo_reminder(self, mocked_send):
|
||||
for idx in range(1, 11):
|
||||
m = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="peer",
|
||||
text=f"task: item {idx}",
|
||||
ts=1900 + idx,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215@g.us",
|
||||
)
|
||||
async_to_sync(process_inbound_task_intelligence)(m)
|
||||
payloads = [str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list]
|
||||
self.assertTrue(
|
||||
any(".l list tasks" in row.lower() and ".undo" in row.lower() for row in payloads),
|
||||
"Expected periodic reminder to mention both .l and .undo.",
|
||||
)
|
||||
|
||||
@patch("core.tasks.engine.send_message_raw", new_callable=AsyncMock)
|
||||
def test_epic_create_command_from_chat(self, mocked_send):
|
||||
msg = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="peer",
|
||||
text="epic: Security",
|
||||
ts=2001,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215@g.us",
|
||||
)
|
||||
async_to_sync(process_inbound_task_intelligence)(msg)
|
||||
self.assertTrue(TaskEpic.objects.filter(project=self.project, name="Security").exists())
|
||||
payloads = [str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list]
|
||||
self.assertTrue(any("whatsapp usage" in row.lower() for row in payloads))
|
||||
|
||||
def test_task_with_epic_token_assigns_epic(self):
|
||||
msg = Message.objects.create(
|
||||
user=self.user,
|
||||
session=self.session,
|
||||
sender_uuid="peer",
|
||||
text="task: setup ssl cert rotation [epic:Security]",
|
||||
ts=2002,
|
||||
source_service="whatsapp",
|
||||
source_chat_id="120363402761690215@g.us",
|
||||
)
|
||||
async_to_sync(process_inbound_task_intelligence)(msg)
|
||||
task = DerivedTask.objects.filter(origin_message=msg).first()
|
||||
self.assertIsNotNone(task)
|
||||
self.assertIsNotNone(task.epic)
|
||||
self.assertEqual("Security", str(task.epic.name or ""))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
@@ -73,6 +75,16 @@ class TasksPagesManagementTests(TestCase):
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_tasks_hub_settings_link_preserves_scope_context(self):
|
||||
response = self.client.get(
|
||||
f"{reverse('tasks_hub')}?person={self.person.id}&service=signal&identifier=147e75bd-91b7-4014-b9e5-12a44b978f7b"
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertContains(
|
||||
response,
|
||||
f"{reverse('tasks_settings')}?person={self.person.id}&service=signal&identifier=147e75bd-91b7-4014-b9e5-12a44b978f7b",
|
||||
)
|
||||
|
||||
def test_project_page_can_create_and_delete_epic(self):
|
||||
project = TaskProject.objects.create(user=self.user, name="Roadmap")
|
||||
create_response = self.client.post(
|
||||
@@ -98,6 +110,81 @@ class TasksPagesManagementTests(TestCase):
|
||||
self.assertEqual(200, delete_response.status_code)
|
||||
self.assertFalse(TaskEpic.objects.filter(project=project, name="Phase 1").exists())
|
||||
|
||||
def test_project_page_can_assign_and_clear_task_epic(self):
|
||||
project = TaskProject.objects.create(user=self.user, name="Roadmap")
|
||||
epic = TaskEpic.objects.create(project=project, name="Sprint A")
|
||||
session = ChatSession.objects.create(user=self.user, identifier=self.pid_signal)
|
||||
origin = Message.objects.create(
|
||||
user=self.user,
|
||||
session=session,
|
||||
ts=1_700_000_000_100,
|
||||
text="task: assign epic",
|
||||
sender_uuid="+15551230000",
|
||||
custom_author="OTHER",
|
||||
source_service="signal",
|
||||
source_chat_id="+15551230000",
|
||||
)
|
||||
task = DerivedTask.objects.create(
|
||||
user=self.user,
|
||||
project=project,
|
||||
title="Assign me",
|
||||
source_service="signal",
|
||||
source_channel="+15551230000",
|
||||
origin_message=origin,
|
||||
reference_code="9",
|
||||
status_snapshot="open",
|
||||
)
|
||||
assign_response = self.client.post(
|
||||
reverse("tasks_project", kwargs={"project_id": str(project.id)}),
|
||||
{
|
||||
"action": "task_set_epic",
|
||||
"task_id": str(task.id),
|
||||
"epic_id": str(epic.id),
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(200, assign_response.status_code)
|
||||
task.refresh_from_db()
|
||||
self.assertEqual(epic.id, task.epic_id)
|
||||
|
||||
clear_response = self.client.post(
|
||||
reverse("tasks_project", kwargs={"project_id": str(project.id)}),
|
||||
{
|
||||
"action": "task_set_epic",
|
||||
"task_id": str(task.id),
|
||||
"epic_id": "",
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(200, clear_response.status_code)
|
||||
task.refresh_from_db()
|
||||
self.assertIsNone(task.epic_id)
|
||||
|
||||
@patch("core.views.tasks.send_message_raw", new_callable=AsyncMock)
|
||||
def test_project_epic_create_announces_to_project_chats(self, mocked_send):
|
||||
project = TaskProject.objects.create(user=self.user, name="Roadmap")
|
||||
ChatTaskSource.objects.create(
|
||||
user=self.user,
|
||||
service="whatsapp",
|
||||
channel_identifier="120363402761690215@g.us",
|
||||
project=project,
|
||||
enabled=True,
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("tasks_project", kwargs={"project_id": str(project.id)}),
|
||||
{
|
||||
"action": "epic_create",
|
||||
"name": "Phase 2",
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertTrue(TaskEpic.objects.filter(project=project, name="Phase 2").exists())
|
||||
self.assertTrue(mocked_send.await_count >= 1)
|
||||
payloads = [str(call.kwargs.get("text") or "") for call in mocked_send.await_args_list]
|
||||
self.assertTrue(any("whatsapp usage" in row.lower() for row in payloads))
|
||||
self.assertTrue(any("add task to epic" in row.lower() for row in payloads))
|
||||
|
||||
def test_group_page_create_and_map_project(self):
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
@@ -156,6 +243,36 @@ class TasksPagesManagementTests(TestCase):
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertContains(response, "Scope Person")
|
||||
|
||||
def test_project_page_creator_column_links_to_compose(self):
|
||||
project = TaskProject.objects.create(user=self.user, name="Creator Link Test")
|
||||
session = ChatSession.objects.create(user=self.user, identifier=self.pid_signal)
|
||||
origin = Message.objects.create(
|
||||
user=self.user,
|
||||
session=session,
|
||||
ts=1_700_000_000_111,
|
||||
text="task: creator link",
|
||||
sender_uuid="+15551230000",
|
||||
custom_author="OTHER",
|
||||
source_service="signal",
|
||||
source_chat_id="+15551230000",
|
||||
)
|
||||
DerivedTask.objects.create(
|
||||
user=self.user,
|
||||
project=project,
|
||||
title="Creator link task",
|
||||
source_service="signal",
|
||||
source_channel="+15551230000",
|
||||
origin_message=origin,
|
||||
reference_code="2",
|
||||
status_snapshot="open",
|
||||
)
|
||||
response = self.client.get(reverse("tasks_project", kwargs={"project_id": str(project.id)}))
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'{reverse("compose_page")}?service=signal&identifier=%2B15551230000&person={self.person.id}',
|
||||
)
|
||||
|
||||
def test_task_detail_renders_payload_summary_and_json(self):
|
||||
project = TaskProject.objects.create(user=self.user, name="Payload Test")
|
||||
session = ChatSession.objects.create(user=self.user, identifier=self.pid_signal)
|
||||
|
||||
@@ -9,12 +9,16 @@ from django.test import TestCase, override_settings
|
||||
from core.models import (
|
||||
ChatSession,
|
||||
ChatTaskSource,
|
||||
CodexPermissionRequest,
|
||||
CodexRun,
|
||||
DerivedTask,
|
||||
ExternalSyncEvent,
|
||||
ExternalChatLink,
|
||||
Message,
|
||||
Person,
|
||||
PersonIdentifier,
|
||||
TaskCompletionPattern,
|
||||
TaskProviderConfig,
|
||||
TaskProject,
|
||||
User,
|
||||
)
|
||||
@@ -118,6 +122,7 @@ class TaskAnnounceToggleTests(TestCase):
|
||||
self.assertIn("bp", names)
|
||||
self.assertIn("bp set", names)
|
||||
self.assertIn("bp set range", names)
|
||||
self.assertIn("codex", names)
|
||||
|
||||
|
||||
@override_settings(TASK_DERIVATION_USE_AI=False)
|
||||
@@ -268,3 +273,97 @@ class TaskSettingsExternalChatLinkScopeTests(TestCase):
|
||||
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):
|
||||
response = self.client.post(
|
||||
reverse("tasks_settings"),
|
||||
{
|
||||
"action": "provider_update",
|
||||
"provider": "codex_cli",
|
||||
"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")
|
||||
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)
|
||||
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())
|
||||
|
||||
def test_codex_settings_page_and_approval_action(self):
|
||||
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,
|
||||
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()
|
||||
self.assertEqual("approved", req.status)
|
||||
self.assertEqual("approved_waiting_resume", run.status)
|
||||
|
||||
Reference in New Issue
Block a user