127 lines
4.4 KiB
Python
127 lines
4.4 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
from unittest.mock import AsyncMock
|
|
|
|
from opc.core.models import ExecutionCheckpoint, Task, TaskStatus
|
|
from opc.plugins.cli_board.services.actions import BoardActions
|
|
|
|
|
|
class _StubStore:
|
|
def __init__(self) -> None:
|
|
self.tasks: dict[str, Task] = {}
|
|
self.checkpoints: list[ExecutionCheckpoint] = []
|
|
self.resolved: list[tuple[str, str]] = []
|
|
|
|
async def save_task(self, task: Task) -> None:
|
|
self.tasks[task.id] = task
|
|
|
|
async def get_task(self, task_id: str) -> Task | None:
|
|
return self.tasks.get(task_id)
|
|
|
|
async def get_tasks(self, **_kw):
|
|
return list(self.tasks.values())
|
|
|
|
async def get_pending_checkpoints(self, **_kw):
|
|
return [checkpoint for checkpoint in self.checkpoints if checkpoint.status == "pending"]
|
|
|
|
async def resolve_execution_checkpoint(self, checkpoint_id: str, status: str = "resolved") -> None:
|
|
self.resolved.append((checkpoint_id, status))
|
|
for checkpoint in self.checkpoints:
|
|
if checkpoint.checkpoint_id == checkpoint_id:
|
|
checkpoint.status = status
|
|
|
|
|
|
class _StubMemory:
|
|
def __init__(self) -> None:
|
|
self.ensure_session = AsyncMock()
|
|
|
|
|
|
class _StubEngine:
|
|
def __init__(self) -> None:
|
|
self.store = _StubStore()
|
|
self.memory = _StubMemory()
|
|
self.process_message = AsyncMock(return_value="ok")
|
|
|
|
|
|
class _StubFacade:
|
|
def __init__(self, engine: _StubEngine) -> None:
|
|
self.project_id = "demo"
|
|
self._engine = engine
|
|
|
|
async def ensure_ready(self):
|
|
return self._engine
|
|
|
|
|
|
class BoardActionsTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_create_task_creates_session_backed_placeholder(self) -> None:
|
|
engine = _StubEngine()
|
|
actions = BoardActions(_StubFacade(engine), project_id="demo")
|
|
|
|
task = await actions.create_task(title="Draft feature", description="Initial plan")
|
|
|
|
self.assertIn(task.id, engine.store.tasks)
|
|
engine.memory.ensure_session.assert_awaited()
|
|
self.assertEqual(engine.store.tasks[task.id].title, "Draft feature")
|
|
self.assertEqual(engine.store.tasks[task.id].metadata["source"], "cli_board")
|
|
|
|
async def test_send_session_message_routes_through_origin_task(self) -> None:
|
|
engine = _StubEngine()
|
|
task = Task(
|
|
id="task-1",
|
|
title="Feature task",
|
|
description="Implement the feature",
|
|
status=TaskStatus.PENDING,
|
|
session_id="session-1",
|
|
project_id="demo",
|
|
)
|
|
await engine.store.save_task(task)
|
|
actions = BoardActions(_StubFacade(engine), project_id="demo")
|
|
|
|
response = await actions.send_session_message("task-1", "please continue")
|
|
|
|
self.assertEqual(response, "ok")
|
|
engine.process_message.assert_awaited_once()
|
|
kwargs = engine.process_message.await_args.kwargs
|
|
self.assertEqual(kwargs["session_id"], "session-1")
|
|
self.assertEqual(kwargs["origin_task_id"], "task-1")
|
|
self.assertEqual(engine.store.tasks["task-1"].status, TaskStatus.IDLE)
|
|
|
|
async def test_cancel_task_marks_related_tasks_and_checkpoints_cancelled(self) -> None:
|
|
engine = _StubEngine()
|
|
root = Task(
|
|
id="root",
|
|
title="Root task",
|
|
description="Run runtime",
|
|
status=TaskStatus.RUNNING,
|
|
session_id="session-root",
|
|
project_id="demo",
|
|
)
|
|
linked = Task(
|
|
id="child",
|
|
title="Child task",
|
|
description="Background child",
|
|
status=TaskStatus.RUNNING,
|
|
session_id="session-child",
|
|
project_id="demo",
|
|
metadata={"origin_task_id": "root"},
|
|
)
|
|
await engine.store.save_task(root)
|
|
await engine.store.save_task(linked)
|
|
engine.store.checkpoints.append(
|
|
ExecutionCheckpoint(
|
|
checkpoint_id="cp-root",
|
|
project_id="demo",
|
|
session_id="session-root",
|
|
task_id="root",
|
|
)
|
|
)
|
|
actions = BoardActions(_StubFacade(engine), project_id="demo")
|
|
|
|
await actions.cancel_task("root")
|
|
|
|
self.assertEqual(engine.store.tasks["root"].status, TaskStatus.CANCELLED)
|
|
self.assertEqual(engine.store.tasks["child"].status, TaskStatus.CANCELLED)
|
|
self.assertEqual(engine.store.resolved, [("cp-root", "cancelled")])
|
|
|