From a80b7695d8a4a41b58fe2c8e9b7b31f4dfd27ee2 Mon Sep 17 00:00:00 2001 From: fxd-jason Date: Thu, 7 May 2026 11:53:12 +0800 Subject: [PATCH] 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 --- api/kanban_bridge.py | 23 +++++++++----- tests/test_kanban_bridge.py | 63 +++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/api/kanban_bridge.py b/api/kanban_bridge.py index 0f34172e..403f2710 100644 --- a/api/kanban_bridge.py +++ b/api/kanban_bridge.py @@ -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: diff --git a/tests/test_kanban_bridge.py b/tests/test_kanban_bridge.py index 7903d422..bbcae21d 100644 --- a/tests/test_kanban_bridge.py +++ b/tests/test_kanban_bridge.py @@ -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."""