176 lines
6.4 KiB
Python
176 lines
6.4 KiB
Python
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock
|
|
|
|
from asgiref.sync import async_to_sync
|
|
from django.test import TestCase
|
|
|
|
from core.clients.xmpp import XMPPComponent
|
|
from core.models import (
|
|
CodexPermissionRequest,
|
|
CodexRun,
|
|
DerivedTask,
|
|
ExternalSyncEvent,
|
|
TaskProject,
|
|
User,
|
|
)
|
|
|
|
|
|
class _ApprovalProbe:
|
|
_resolve_request_provider = XMPPComponent._resolve_request_provider
|
|
_approval_event_prefix = XMPPComponent._approval_event_prefix
|
|
_APPROVAL_PROVIDER_COMMANDS = XMPPComponent._APPROVAL_PROVIDER_COMMANDS
|
|
_ACTION_TO_STATUS = XMPPComponent._ACTION_TO_STATUS
|
|
_apply_approval_decision = XMPPComponent._apply_approval_decision
|
|
_approval_list_pending = XMPPComponent._approval_list_pending
|
|
_approval_status = XMPPComponent._approval_status
|
|
_handle_approval_command = XMPPComponent._handle_approval_command
|
|
_gateway_help_lines = XMPPComponent._gateway_help_lines
|
|
_handle_tasks_command = XMPPComponent._handle_tasks_command
|
|
|
|
|
|
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="jews.zm.is",
|
|
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="jews.zm.is",
|
|
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 = _ApprovalProbe()
|
|
self.probe.log = MagicMock()
|
|
|
|
def _run_command(self, text: str) -> list[str]:
|
|
messages = []
|
|
|
|
def _sym(value):
|
|
messages.append(str(value))
|
|
|
|
handled = async_to_sync(XMPPComponent._handle_approval_command)(
|
|
self.probe,
|
|
self.user,
|
|
text,
|
|
"xmpp-approval-user@zm.is/mobile",
|
|
_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="jews.zm.is",
|
|
reference_code="12",
|
|
status_snapshot="open",
|
|
)
|
|
self.probe = _ApprovalProbe()
|
|
self.probe.log = MagicMock()
|
|
|
|
def _run_tasks(self, text: str) -> list[str]:
|
|
messages = []
|
|
|
|
def _sym(value):
|
|
messages.append(str(value))
|
|
|
|
handled = async_to_sync(XMPPComponent._handle_tasks_command)(
|
|
self.probe,
|
|
self.user,
|
|
text,
|
|
_sym,
|
|
)
|
|
self.assertTrue(handled)
|
|
self.assertTrue(messages)
|
|
return messages
|
|
|
|
def test_help_contains_approval_and_tasks_sections(self):
|
|
lines = self.probe._gateway_help_lines()
|
|
text = "\n".join(lines)
|
|
self.assertIn(".approval list-pending", text)
|
|
self.assertIn(".tasks list", 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(".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())
|