mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 19:20:16 +00:00
fix(kanban): update stale read-only docstring + board_exists early-out in board counts
The bridge module docstring still described the API as 'deliberately
read-only' but it now exposes full CRUD (tasks, boards, comments,
links, SSE). Updated to list the supported operations.
For _board_counts_for_slug (the hot path for the board-switcher badge),
added a board_exists() early-out that mirrors the agent's own helper
in plugin_api.py (path.exists() before connect()). This avoids a
redundant init_db()+connect() schema pass per board per list refresh.
connect() already handles auto-init for fresh databases via its
needs_init check, so the extra init_db was unnecessary overhead on
the hot path that scales linearly with board count.
Tests:
- test_board_counts_returns_empty_for_nonexistent_board: verifies the
early-out (no connect() call, returns {})
- test_board_counts_returns_real_counts_for_populated_board: verifies
actual per-status counts are returned for existing boards
This commit is contained in:
+15
-8
@@ -1,9 +1,14 @@
|
||||
"""Read-only Hermes Kanban bridge for the WebUI.
|
||||
"""Hermes Kanban bridge for the WebUI.
|
||||
|
||||
This module exposes a small WebUI-native API under ``/api/kanban/*`` while
|
||||
keeping Hermes Agent's ``hermes_cli.kanban_db`` as the only source of truth.
|
||||
The first integration is deliberately read-only; write/move semantics can be
|
||||
added in later focused PRs.
|
||||
This module exposes a full CRUD API under ``/api/kanban/*`` while keeping
|
||||
Hermes Agent's ``hermes_cli.kanban_db`` as the only source of truth.
|
||||
|
||||
Supported operations:
|
||||
- Task CRUD (create, read, patch, bulk update, archive)
|
||||
- Multi-board management (list, create, archive, switch)
|
||||
- Task dependency links (create, delete)
|
||||
- SSE live event stream for real-time updates
|
||||
- Comments and worker dispatch integration
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -702,10 +707,12 @@ def _board_meta_dict(meta):
|
||||
def _board_counts_for_slug(slug):
|
||||
"""Per-status task counts for a board, used to populate the board
|
||||
switcher with a live "12 tasks" badge. Mirrors the agent dashboard's
|
||||
``_board_counts`` helper. Best-effort — empty dict if the board's
|
||||
sqlite is missing (which can happen on a freshly-created board before
|
||||
the first task is added)."""
|
||||
``_board_counts`` helper. Returns an empty dict for boards whose
|
||||
sqlite file has not been materialized yet (freshly-created boards
|
||||
with no tasks)."""
|
||||
kb = _kb()
|
||||
if not kb.board_exists(slug):
|
||||
return {}
|
||||
try:
|
||||
conn = kb.connect(board=slug)
|
||||
except Exception:
|
||||
|
||||
@@ -695,6 +695,69 @@ def test_list_boards_includes_default_when_only_default_exists(monkeypatch):
|
||||
assert "default" in slugs
|
||||
|
||||
|
||||
def test_board_counts_returns_empty_for_nonexistent_board(monkeypatch):
|
||||
"""_board_counts_for_slug returns {} early for boards whose sqlite
|
||||
file has not been materialized yet (board_exists returns False),
|
||||
avoiding an unnecessary connect() call on the hot board-list path."""
|
||||
fake_kanban = FakeKanbanDB()
|
||||
connect_calls = []
|
||||
orig_connect = fake_kanban.connect
|
||||
def tracking_connect(*, board=None):
|
||||
connect_calls.append(("connect", board))
|
||||
return orig_connect(board=board)
|
||||
fake_kanban.connect = tracking_connect
|
||||
|
||||
fake_hermes_cli = types.ModuleType("hermes_cli")
|
||||
fake_hermes_cli.kanban_db = fake_kanban
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli", fake_hermes_cli)
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli.kanban_db", fake_kanban)
|
||||
import api.kanban_bridge as bridge
|
||||
bridge = importlib.reload(bridge)
|
||||
|
||||
counts = bridge._board_counts_for_slug("no-such-board")
|
||||
assert counts == {}
|
||||
# connect must NOT have been called — early-out via board_exists
|
||||
assert connect_calls == []
|
||||
|
||||
|
||||
def test_board_counts_returns_real_counts_for_populated_board(monkeypatch):
|
||||
"""When a board has tasks, _board_counts_for_slug must return actual
|
||||
per-status counts. The FakeConn needs to handle the board-counts SQL
|
||||
pattern (which differs from the dashboard stats SQL)."""
|
||||
fake_kanban = FakeKanbanDB()
|
||||
fake_hermes_cli = types.ModuleType("hermes_cli")
|
||||
fake_hermes_cli.kanban_db = fake_kanban
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli", fake_hermes_cli)
|
||||
monkeypatch.setitem(sys.modules, "hermes_cli.kanban_db", fake_kanban)
|
||||
import api.kanban_bridge as bridge
|
||||
bridge = importlib.reload(bridge)
|
||||
|
||||
# Patch FakeConn.execute to handle the board-counts SQL:
|
||||
# SELECT status, COUNT(*) AS n FROM tasks WHERE status != 'archived' GROUP BY status
|
||||
orig_execute = FakeConn.execute
|
||||
def patched_execute(self, sql, params=()):
|
||||
if "SELECT status, COUNT(*) AS n FROM tasks" in sql and "GROUP BY status" in sql:
|
||||
rows = []
|
||||
grouped = {}
|
||||
for task in self.tasks:
|
||||
if task.status == "archived":
|
||||
continue
|
||||
grouped[task.status] = grouped.get(task.status, 0) + 1
|
||||
for status, n in grouped.items():
|
||||
rows.append(FakeRow(status=status, n=n))
|
||||
return SimpleNamespace(fetchall=lambda: rows)
|
||||
return orig_execute(self, sql, params)
|
||||
FakeConn.execute = patched_execute
|
||||
|
||||
try:
|
||||
counts = bridge._board_counts_for_slug("default")
|
||||
# Default fake has t_1=ready, t_2=blocked
|
||||
assert counts.get("ready") == 1
|
||||
assert counts.get("blocked") == 1
|
||||
finally:
|
||||
FakeConn.execute = orig_execute
|
||||
|
||||
|
||||
def test_create_board_payload_creates_and_optionally_switches(monkeypatch):
|
||||
"""POST /boards must create a board and, when ``switch=true``, also set
|
||||
it as the active board so subsequent requests resolve to it."""
|
||||
|
||||
Reference in New Issue
Block a user