Files
OpenOPC/tests/cli_board/test_actions.py
T
2026-07-01 17:56:31 +08:00

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