Initial commit
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
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")])
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from opc.plugins.cli_board.state.models import BoardSnapshot, BoardTaskView, PendingCheckpointView
|
||||
from opc.plugins.cli_board.state.store import BoardStateStore
|
||||
|
||||
|
||||
def _task(task_id: str, title: str, column_id: str, *, status: str | None = None) -> BoardTaskView:
|
||||
return BoardTaskView(
|
||||
task_id=task_id,
|
||||
title=title,
|
||||
description=f"{title} description",
|
||||
status=status or ("pending" if column_id == "todo" else "running"),
|
||||
column_id=column_id,
|
||||
priority="medium",
|
||||
created_at=1.0,
|
||||
updated_at=1.0,
|
||||
)
|
||||
|
||||
|
||||
class BoardStateStoreTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.store = BoardStateStore()
|
||||
self.store.replace_snapshot(
|
||||
BoardSnapshot(
|
||||
project_id="demo",
|
||||
tasks=[
|
||||
_task("todo-1", "Todo 1", "todo"),
|
||||
_task("todo-2", "Todo 2", "todo"),
|
||||
_task("run-1", "Run 1", "in-progress"),
|
||||
_task("done-1", "Done 1", "done", status="done"),
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
def test_initial_selection_uses_first_visible_task(self) -> None:
|
||||
selected = self.store.selected_task()
|
||||
self.assertIsNotNone(selected)
|
||||
self.assertEqual(selected.task_id, "todo-1")
|
||||
|
||||
def test_move_selection_crosses_columns_and_rows(self) -> None:
|
||||
self.store.move_selection(row_delta=1)
|
||||
self.assertEqual(self.store.selected_task().task_id, "todo-2")
|
||||
|
||||
self.store.move_selection(column_delta=1)
|
||||
self.assertEqual(self.store.selected_task().task_id, "run-1")
|
||||
|
||||
self.store.move_selection(column_delta=1)
|
||||
self.assertEqual(self.store.selected_task().task_id, "done-1")
|
||||
|
||||
def test_toggle_done_hides_done_column_tasks(self) -> None:
|
||||
showing = self.store.toggle_show_done()
|
||||
self.assertFalse(showing)
|
||||
counts = self.store.board_counts()
|
||||
self.assertEqual(counts["done"], 0)
|
||||
|
||||
def test_search_filter_updates_selection(self) -> None:
|
||||
self.store.set_search_query("run")
|
||||
selected = self.store.selected_task()
|
||||
self.assertIsNotNone(selected)
|
||||
self.assertEqual(selected.task_id, "run-1")
|
||||
|
||||
def test_runtime_updates_and_progress_are_tracked(self) -> None:
|
||||
self.store.apply_runtime_update(
|
||||
"run-1",
|
||||
status="tool_active",
|
||||
current_tool="shell_exec",
|
||||
iteration=2,
|
||||
tool_elapsed_ms=420,
|
||||
last_tool_summary="pytest -q completed",
|
||||
context_tokens=1200,
|
||||
context_window=4000,
|
||||
context_remaining_pct=70,
|
||||
turn_cost_usd=0.0123,
|
||||
session_cost_usd=0.0456,
|
||||
pending_permission_count=1,
|
||||
drain_mode="smooth",
|
||||
)
|
||||
self.store.append_progress("run-1", "[Tool: shell_exec] pytest -q")
|
||||
|
||||
runtime = self.store.runtime_for("run-1")
|
||||
self.assertIsNotNone(runtime)
|
||||
self.assertEqual(runtime.status, "tool_active")
|
||||
self.assertEqual(runtime.current_tool, "shell_exec")
|
||||
self.assertEqual(runtime.iteration, 2)
|
||||
self.assertEqual(runtime.tool_elapsed_ms, 420)
|
||||
self.assertEqual(runtime.last_tool_summary, "pytest -q completed")
|
||||
self.assertEqual(runtime.context_remaining_pct, 70)
|
||||
self.assertEqual(runtime.turn_cost_usd, 0.0123)
|
||||
self.assertEqual(runtime.pending_permission_count, 1)
|
||||
self.assertEqual(runtime.drain_mode, "smooth")
|
||||
self.assertEqual(len(runtime.progress_entries), 1)
|
||||
|
||||
def test_metrics_and_alerts_include_snapshot_and_runtime_state(self) -> None:
|
||||
self.store.snapshot.hidden_task_count = 2
|
||||
self.store.snapshot.pending_checkpoint_count = 1
|
||||
self.store.snapshot.tasks[2].pending_checkpoint = PendingCheckpointView(
|
||||
checkpoint_id="cp-1",
|
||||
checkpoint_type="task_user_input",
|
||||
status="pending",
|
||||
session_id="session-run-1",
|
||||
task_id="run-1",
|
||||
summary="Need approval",
|
||||
prompt="Approve this step",
|
||||
)
|
||||
self.store.apply_runtime_update("run-1", status="tool_active", current_tool="shell_exec")
|
||||
|
||||
metrics = self.store.metrics()
|
||||
alerts = self.store.alerts()
|
||||
|
||||
self.assertEqual(metrics.total_tasks, 6)
|
||||
self.assertEqual(metrics.pending_checkpoint_count, 1)
|
||||
self.assertEqual(metrics.running_count, 1)
|
||||
self.assertGreaterEqual(metrics.alert_count, 1)
|
||||
self.assertTrue(any(alert.task_id == "run-1" for alert in alerts))
|
||||
|
||||
def test_session_navigation_and_view_state_controls(self) -> None:
|
||||
self.store.select_task("run-1")
|
||||
self.store.set_pane_focus("session-rail")
|
||||
self.store.move_session_selection(1)
|
||||
self.assertEqual(self.store.selected_task().task_id, "todo-1")
|
||||
|
||||
self.store.set_view_mode("list")
|
||||
self.assertEqual(self.store.view_mode, "list")
|
||||
self.assertEqual(self.store.toggle_density(), "comfortable")
|
||||
self.assertEqual(self.store.cycle_context_tab(1), "session")
|
||||
@@ -0,0 +1,365 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
|
||||
from opc.core.models import (
|
||||
DelegationRun,
|
||||
DelegationWorkItem,
|
||||
ExecutionCheckpoint,
|
||||
Phase,
|
||||
SessionMessageRecord,
|
||||
SessionPartRecord,
|
||||
Task,
|
||||
TaskStatus,
|
||||
)
|
||||
from opc.plugins.cli_board.services.board_repository import BoardRepository
|
||||
from opc.layer2_organization.work_item_links import set_linked_work_item_id
|
||||
|
||||
|
||||
class _StubStore:
|
||||
def __init__(
|
||||
self,
|
||||
tasks,
|
||||
checkpoints,
|
||||
transcripts,
|
||||
*,
|
||||
delegation_runs=None,
|
||||
work_items_by_run=None,
|
||||
) -> None:
|
||||
self._tasks = tasks
|
||||
self._checkpoints = checkpoints
|
||||
self._transcripts = transcripts
|
||||
self._delegation_runs = list(delegation_runs or [])
|
||||
self._work_items_by_run = dict(work_items_by_run or {})
|
||||
|
||||
async def get_tasks(self, **_kw):
|
||||
return list(self._tasks)
|
||||
|
||||
async def get_pending_checkpoints(self, **_kw):
|
||||
return list(self._checkpoints)
|
||||
|
||||
async def get_session_transcript(self, session_id):
|
||||
return list(self._transcripts.get(session_id, []))
|
||||
|
||||
# The company-mode branch in BoardRepository feature-detects these methods.
|
||||
# Only present them when the test wires a delegation run, so the standard
|
||||
# tests above keep exercising the Task path.
|
||||
async def list_open_delegation_runs(self, *, project_id=None):
|
||||
return list(self._delegation_runs)
|
||||
|
||||
async def list_delegation_work_items(self, run_id):
|
||||
return list(self._work_items_by_run.get(run_id, []))
|
||||
|
||||
|
||||
class _StubFacade:
|
||||
def __init__(self, store) -> None:
|
||||
self.project_id = "demo"
|
||||
self._engine = type("Engine", (), {"store": store})()
|
||||
|
||||
async def ensure_ready(self):
|
||||
return self._engine
|
||||
|
||||
|
||||
class BoardRepositoryTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_snapshot_hides_internal_origin_tasks(self) -> None:
|
||||
visible = Task(
|
||||
id="visible-1",
|
||||
title="Visible task",
|
||||
description="Board-visible task",
|
||||
status=TaskStatus.PENDING,
|
||||
session_id="session-visible",
|
||||
project_id="demo",
|
||||
)
|
||||
hidden = Task(
|
||||
id="hidden-1",
|
||||
title="Internal task",
|
||||
description="Background execution",
|
||||
status=TaskStatus.RUNNING,
|
||||
session_id="session-hidden",
|
||||
project_id="demo",
|
||||
metadata={"origin_task_id": "visible-1"},
|
||||
)
|
||||
checkpoint = ExecutionCheckpoint(
|
||||
checkpoint_id="cp-1",
|
||||
project_id="demo",
|
||||
session_id="session-visible",
|
||||
checkpoint_type="company_delivery_feedback",
|
||||
status="pending",
|
||||
payload={"prompt": "Approve delivery?"},
|
||||
)
|
||||
repo = BoardRepository(_StubFacade(_StubStore([visible, hidden], [checkpoint], {})), project_id="demo")
|
||||
|
||||
snapshot = await repo.load_snapshot()
|
||||
|
||||
self.assertEqual(len(snapshot.tasks), 1)
|
||||
self.assertEqual(snapshot.hidden_task_count, 1)
|
||||
self.assertEqual(snapshot.tasks[0].task_id, "visible-1")
|
||||
self.assertIsNotNone(snapshot.tasks[0].pending_checkpoint)
|
||||
self.assertEqual(len(snapshot.session_summaries), 1)
|
||||
self.assertEqual(snapshot.metrics.visible_tasks, 1)
|
||||
self.assertEqual(snapshot.metrics.pending_checkpoint_count, 1)
|
||||
self.assertTrue(any(alert.task_id == "visible-1" for alert in snapshot.alerts))
|
||||
|
||||
async def test_load_task_detail_includes_transcript_and_linked_executions(self) -> None:
|
||||
task = Task(
|
||||
id="task-1",
|
||||
title="Primary task",
|
||||
description="Do the work",
|
||||
status=TaskStatus.RUNNING,
|
||||
session_id="session-1",
|
||||
project_id="demo",
|
||||
metadata={"progress_log": ["planning", "running"]},
|
||||
)
|
||||
linked = Task(
|
||||
id="task-2",
|
||||
title="Linked task",
|
||||
description="Hidden execution",
|
||||
status=TaskStatus.RUNNING,
|
||||
session_id="session-2",
|
||||
project_id="demo",
|
||||
metadata={"origin_task_id": "task-1"},
|
||||
)
|
||||
transcript = {
|
||||
"session-1": [
|
||||
{
|
||||
"message": SessionMessageRecord(
|
||||
message_id="m1",
|
||||
session_id="session-1",
|
||||
role="user",
|
||||
created_at=datetime.now(),
|
||||
),
|
||||
"parts": [
|
||||
SessionPartRecord(
|
||||
message_id="m1",
|
||||
session_id="session-1",
|
||||
part_type="text",
|
||||
payload={"text": "Please implement the feature"},
|
||||
)
|
||||
],
|
||||
},
|
||||
{
|
||||
"message": SessionMessageRecord(
|
||||
message_id="m2",
|
||||
session_id="session-1",
|
||||
role="assistant",
|
||||
created_at=datetime.now(),
|
||||
),
|
||||
"parts": [
|
||||
SessionPartRecord(
|
||||
message_id="m2",
|
||||
session_id="session-1",
|
||||
part_type="text",
|
||||
payload={"text": "Working on it"},
|
||||
)
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
repo = BoardRepository(_StubFacade(_StubStore([task, linked], [], transcript)), project_id="demo")
|
||||
|
||||
detail = await repo.load_task_detail("task-1")
|
||||
|
||||
self.assertIsNotNone(detail)
|
||||
assert detail is not None
|
||||
self.assertEqual(detail.task.task_id, "task-1")
|
||||
self.assertEqual(len(detail.transcript), 2)
|
||||
self.assertEqual(len(detail.linked_executions), 1)
|
||||
self.assertEqual(detail.linked_executions[0].task_id, "task-2")
|
||||
self.assertEqual(detail.progress_entries, ["planning", "running"])
|
||||
self.assertEqual(detail.task.display_id, "OPC-1")
|
||||
|
||||
async def test_company_mode_snapshot_uses_work_items_as_cards(self) -> None:
|
||||
"""In company mode, kanban cards come from DelegationWorkItem (not Task).
|
||||
|
||||
Asserts: card task_id == work_item_id; canonical four-state column
|
||||
mapping (todo / in-progress / in-review / done); session_id remains
|
||||
only as an audit reference; column_order includes "in-review".
|
||||
"""
|
||||
run = DelegationRun(
|
||||
run_id="run-1",
|
||||
project_id="demo",
|
||||
company_profile="corporate",
|
||||
lifecycle_status="active",
|
||||
)
|
||||
# Synthetic root work item — must be hidden (no parent_work_item_id).
|
||||
root_item = DelegationWorkItem(
|
||||
work_item_id="wi-root",
|
||||
run_id="run-1",
|
||||
title="Root",
|
||||
summary="root",
|
||||
phase=Phase.READY,
|
||||
)
|
||||
wi_todo = DelegationWorkItem(
|
||||
work_item_id="wi-todo",
|
||||
run_id="run-1",
|
||||
parent_work_item_id="wi-root",
|
||||
role_id="researcher",
|
||||
title="Investigate API",
|
||||
summary="Find current rate-limit behavior.",
|
||||
phase=Phase.READY,
|
||||
kind="execute",
|
||||
metadata={"dependency_work_item_ids": []},
|
||||
)
|
||||
wi_running = DelegationWorkItem(
|
||||
work_item_id="wi-running",
|
||||
run_id="run-1",
|
||||
parent_work_item_id="wi-root",
|
||||
role_id="engineer",
|
||||
title="Implement fix",
|
||||
summary="Patch the throttle.",
|
||||
phase=Phase.RUNNING,
|
||||
kind="execute",
|
||||
claimed_by_role_runtime_session_id="role-rt-1",
|
||||
metadata={"activation_state": "active"},
|
||||
)
|
||||
wi_review = DelegationWorkItem(
|
||||
work_item_id="wi-review",
|
||||
run_id="run-1",
|
||||
parent_work_item_id="wi-root",
|
||||
role_id="qa",
|
||||
title="Verify deliverable",
|
||||
summary="Check the test plan.",
|
||||
phase=Phase.AWAITING_MANAGER_REVIEW,
|
||||
kind="review",
|
||||
metadata={"review_state": "pending_manager"},
|
||||
)
|
||||
wi_done = DelegationWorkItem(
|
||||
work_item_id="wi-done",
|
||||
run_id="run-1",
|
||||
parent_work_item_id="wi-root",
|
||||
role_id="engineer",
|
||||
title="Earlier patch",
|
||||
summary="Already shipped.",
|
||||
phase=Phase.APPROVED,
|
||||
kind="execute",
|
||||
)
|
||||
# A runtime Task linked to wi-running for audit/transcript only.
|
||||
linked_task = Task(
|
||||
id="task-running",
|
||||
title="Implement fix",
|
||||
description="runtime execution",
|
||||
status=TaskStatus.RUNNING,
|
||||
session_id="session-running",
|
||||
project_id="demo",
|
||||
metadata={},
|
||||
)
|
||||
set_linked_work_item_id(linked_task, "wi-running")
|
||||
|
||||
store = _StubStore(
|
||||
tasks=[linked_task],
|
||||
checkpoints=[],
|
||||
transcripts={},
|
||||
delegation_runs=[run],
|
||||
work_items_by_run={"run-1": [root_item, wi_todo, wi_running, wi_review, wi_done]},
|
||||
)
|
||||
repo = BoardRepository(_StubFacade(store), project_id="demo")
|
||||
snapshot = await repo.load_snapshot()
|
||||
|
||||
self.assertEqual(snapshot.mode, "company")
|
||||
self.assertEqual(snapshot.column_order, ["todo", "in-progress", "in-review", "done"])
|
||||
# Root item is filtered; the four leaf work items become cards.
|
||||
self.assertEqual([t.task_id for t in snapshot.tasks], ["wi-todo", "wi-running", "wi-review", "wi-done"])
|
||||
# Card identity is the work_item_id, not Task.id.
|
||||
self.assertEqual(snapshot.tasks[0].work_item_id, "wi-todo")
|
||||
# Four-state canonical column mapping.
|
||||
column_by_id = {t.task_id: t.column_id for t in snapshot.tasks}
|
||||
self.assertEqual(column_by_id["wi-todo"], "todo")
|
||||
self.assertEqual(column_by_id["wi-running"], "in-progress")
|
||||
self.assertEqual(column_by_id["wi-review"], "in-review")
|
||||
self.assertEqual(column_by_id["wi-done"], "done")
|
||||
# session_id is an audit reference only (populated for the linked card).
|
||||
running_card = next(t for t in snapshot.tasks if t.task_id == "wi-running")
|
||||
self.assertEqual(running_card.session_id, "session-running")
|
||||
self.assertEqual(running_card.runtime_task_id, "task-running")
|
||||
self.assertEqual(running_card.execution_turn_id, "task-running")
|
||||
todo_card = next(t for t in snapshot.tasks if t.task_id == "wi-todo")
|
||||
self.assertIsNone(todo_card.session_id)
|
||||
self.assertIsNone(todo_card.runtime_task_id)
|
||||
self.assertIsNone(todo_card.execution_turn_id)
|
||||
# Metrics surface the new in_review_count.
|
||||
self.assertEqual(snapshot.metrics.in_review_count, 1)
|
||||
self.assertEqual(snapshot.metrics.in_progress_count, 1)
|
||||
self.assertEqual(snapshot.metrics.todo_count, 1)
|
||||
self.assertEqual(snapshot.metrics.done_count, 1)
|
||||
# Hidden count = synthetic root item.
|
||||
self.assertEqual(snapshot.hidden_task_count, 1)
|
||||
|
||||
async def test_company_mode_load_task_detail_resolves_work_item_id(self) -> None:
|
||||
run = DelegationRun(run_id="run-2", project_id="demo", lifecycle_status="active")
|
||||
wi = DelegationWorkItem(
|
||||
work_item_id="wi-x",
|
||||
run_id="run-2",
|
||||
parent_work_item_id="wi-root",
|
||||
role_id="engineer",
|
||||
title="Detail target",
|
||||
summary="check detail path",
|
||||
phase=Phase.RUNNING,
|
||||
metadata={"handoff_context": "from upstream"},
|
||||
)
|
||||
linked_task = Task(
|
||||
id="task-x",
|
||||
title="runtime",
|
||||
description="",
|
||||
status=TaskStatus.RUNNING,
|
||||
session_id="session-x",
|
||||
project_id="demo",
|
||||
metadata={"progress_log": ["start"]},
|
||||
)
|
||||
set_linked_work_item_id(linked_task, "wi-x")
|
||||
transcript = {
|
||||
"session-x": [
|
||||
{
|
||||
"message": SessionMessageRecord(
|
||||
message_id="m1", session_id="session-x", role="assistant", created_at=datetime.now()
|
||||
),
|
||||
"parts": [SessionPartRecord(message_id="m1", session_id="session-x", part_type="text", payload={"text": "ok"})],
|
||||
}
|
||||
]
|
||||
}
|
||||
store = _StubStore(
|
||||
tasks=[linked_task],
|
||||
checkpoints=[],
|
||||
transcripts=transcript,
|
||||
delegation_runs=[run],
|
||||
work_items_by_run={"run-2": [wi]},
|
||||
)
|
||||
repo = BoardRepository(_StubFacade(store), project_id="demo")
|
||||
|
||||
detail = await repo.load_task_detail("wi-x")
|
||||
self.assertIsNotNone(detail)
|
||||
assert detail is not None
|
||||
self.assertEqual(detail.task.task_id, "wi-x")
|
||||
self.assertEqual(detail.task.work_item_id, "wi-x")
|
||||
self.assertEqual(detail.task.runtime_task_id, "task-x")
|
||||
self.assertEqual(detail.task.execution_turn_id, "task-x")
|
||||
self.assertEqual(detail.context_preview, "from upstream")
|
||||
self.assertEqual(len(detail.transcript), 1)
|
||||
self.assertEqual(detail.progress_entries, ["start"])
|
||||
|
||||
async def test_snapshot_preserves_adaptive_metadata_for_widgets(self) -> None:
|
||||
task = Task(
|
||||
id="task-adaptive",
|
||||
title="Adaptive task",
|
||||
description="Blocked on signals",
|
||||
status=TaskStatus.BLOCKED,
|
||||
session_id="session-adaptive",
|
||||
project_id="demo",
|
||||
metadata={
|
||||
"adaptive": {
|
||||
"normalized_state": "waiting_for_gate",
|
||||
"blocked_reason": "Waiting for required signals: implementation_ready",
|
||||
"work_item_profile": {"gate_owner_role_id": "cto"},
|
||||
"signals": [
|
||||
{"name": "implementation_ready", "required": True, "satisfied": False},
|
||||
],
|
||||
"confidence": 0.72,
|
||||
}
|
||||
},
|
||||
)
|
||||
repo = BoardRepository(_StubFacade(_StubStore([task], [], {})), project_id="demo")
|
||||
|
||||
snapshot = await repo.load_snapshot()
|
||||
|
||||
self.assertEqual(snapshot.tasks[0].metadata["adaptive"]["normalized_state"], "waiting_for_gate")
|
||||
self.assertEqual(snapshot.tasks[0].metadata["adaptive"]["work_item_profile"]["gate_owner_role_id"], "cto")
|
||||
@@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from opc.plugins.cli_board.state.models import BoardSnapshot, BoardTaskView, TaskDetailView
|
||||
|
||||
try: # pragma: no cover - optional dependency
|
||||
from opc.plugins.cli_board.tui.app import CliBoardApp
|
||||
from opc.plugins.cli_board.tui.screens.help import HelpScreen
|
||||
from opc.plugins.cli_board.tui.screens.palette import CommandPaletteScreen
|
||||
from opc.plugins.cli_board.tui.screens.prompt import PromptScreen
|
||||
except ImportError: # pragma: no cover - optional dependency
|
||||
CliBoardApp = None
|
||||
HelpScreen = None
|
||||
CommandPaletteScreen = None
|
||||
PromptScreen = None
|
||||
|
||||
|
||||
class _StubRepository:
|
||||
def __init__(self) -> None:
|
||||
self.snapshot = BoardSnapshot(
|
||||
project_id="demo",
|
||||
tasks=[
|
||||
BoardTaskView(
|
||||
task_id="todo-1",
|
||||
title="Todo task",
|
||||
description="Pending task",
|
||||
status="pending",
|
||||
column_id="todo",
|
||||
priority="medium",
|
||||
created_at=1.0,
|
||||
updated_at=1.0,
|
||||
),
|
||||
BoardTaskView(
|
||||
task_id="run-1",
|
||||
title="Running task",
|
||||
description="In progress",
|
||||
status="running",
|
||||
column_id="in-progress",
|
||||
priority="high",
|
||||
created_at=2.0,
|
||||
updated_at=2.0,
|
||||
),
|
||||
BoardTaskView(
|
||||
task_id="done-1",
|
||||
title="Done task",
|
||||
description="Completed",
|
||||
status="done",
|
||||
column_id="done",
|
||||
priority="low",
|
||||
created_at=3.0,
|
||||
updated_at=3.0,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
async def load_snapshot(self) -> BoardSnapshot:
|
||||
return self.snapshot
|
||||
|
||||
async def load_task_detail(self, task_id: str):
|
||||
task = next((task for task in self.snapshot.tasks if task.task_id == task_id), None)
|
||||
return TaskDetailView(task=task) if task else None
|
||||
|
||||
|
||||
@unittest.skipIf(CliBoardApp is None, "textual is not installed")
|
||||
class CliBoardAppPilotTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_keyboard_navigation_view_switching_and_modals(self) -> None:
|
||||
app = CliBoardApp(project_id="demo", refresh_interval=60.0, bootstrap_services=False)
|
||||
app.repository = _StubRepository()
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
app.state.replace_snapshot(await app.repository.load_snapshot())
|
||||
await app._load_selected_detail()
|
||||
app.status_widget.set_message("Harness ready.")
|
||||
self.assertEqual(app.state.selected_task().task_id, "todo-1")
|
||||
|
||||
await pilot.press("right")
|
||||
self.assertEqual(app.state.selected_task().task_id, "run-1")
|
||||
|
||||
await pilot.press("2")
|
||||
self.assertEqual(app.state.view_mode, "list")
|
||||
|
||||
await pilot.press("3")
|
||||
self.assertEqual(app.state.view_mode, "focus")
|
||||
|
||||
app.action_focus_next_pane()
|
||||
await pilot.pause()
|
||||
self.assertEqual(app.state.pane_focus, "context")
|
||||
|
||||
await pilot.press("right")
|
||||
self.assertEqual(app.state.context_tab, "session")
|
||||
|
||||
await pilot.press("f")
|
||||
self.assertFalse(app.state.show_done)
|
||||
|
||||
await pilot.press("n")
|
||||
self.assertIsInstance(app.screen, PromptScreen)
|
||||
await pilot.press("escape")
|
||||
|
||||
app.action_open_palette()
|
||||
await pilot.pause()
|
||||
self.assertIsInstance(app.screen, CommandPaletteScreen)
|
||||
await pilot.press("escape")
|
||||
|
||||
await pilot.press("?")
|
||||
self.assertIsInstance(app.screen, HelpScreen)
|
||||
await pilot.press("escape")
|
||||
|
||||
Reference in New Issue
Block a user