mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-30 06:41:51 +00:00
test(kanban-dashboard): cover _task_dict task_age fallback
The fix in061a1830added an outer try/except in plugin_api._task_dict so that a future failure mode in kanban_db.task_age (anything _safe_int doesn't already absorb) cannot 500 the GET /board response. The _safe_int / task_age corruption paths got regression coverage in tests/hermes_cli/test_kanban_db.py, but the OUTER fallback contract remained untested -- meaning a refactor that drops the try/except would not be caught by CI. Pin that contract from both consumers of _task_dict: - GET /board returns 200 with the literal fallback age dict for the affected card (other cards continue to render via the same path) - GET /tasks/:id (drawer view) returns 200 with the same fallback, so a single corrupt task can't block its own drawer Both tests force task_age to raise RuntimeError rather than ValueError on '%s', because ValueError is absorbed by _safe_int and never reaches the outer try/except -- testing that path would only re-cover what test_kanban_db.py already pins. Manually verified the regression discipline: git checkout061a1830^ -- plugins/kanban/dashboard/plugin_api.py pytest -k task_age_exception # both FAIL with 500 git checkout HEAD -- plugins/kanban/dashboard/plugin_api.py pytest -k task_age_exception # both PASS
This commit is contained in:
parent
f01ee0b575
commit
028bbc5425
1 changed files with 81 additions and 0 deletions
|
|
@ -1193,6 +1193,87 @@ def test_create_task_no_warning_on_triage(client, monkeypatch):
|
|||
assert "warning" not in r.json() or not r.json()["warning"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _task_dict — outer try/except fallback when task_age raises
|
||||
#
|
||||
# Background: kanban_db.task_age was hardened in 061a1830 to return None for
|
||||
# corrupt timestamp values via _safe_int. The companion fix added a belt-and-
|
||||
# suspenders try/except in plugin_api._task_dict so that *any future* exception
|
||||
# from task_age (not just ValueError on '%s') still yields a usable dict
|
||||
# instead of 500'ing GET /board for the entire org.
|
||||
#
|
||||
# kanban_db._safe_int / task_age corruption paths are covered in
|
||||
# tests/hermes_cli/test_kanban_db.py. The OUTER fallback here is not, which
|
||||
# means a refactor that drops the try/except would not be caught by CI. The
|
||||
# tests below pin that contract.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
_FALLBACK_AGE = {
|
||||
"created_age_seconds": None,
|
||||
"started_age_seconds": None,
|
||||
"time_to_complete_seconds": None,
|
||||
}
|
||||
|
||||
|
||||
def test_board_endpoint_survives_task_age_exception(client, monkeypatch):
|
||||
"""If task_age raises for any reason, GET /board must NOT 500.
|
||||
|
||||
Pre-fix behavior (without the try/except in _task_dict): a single corrupt
|
||||
row turned the entire board response into a 500. The fallback dict lets
|
||||
the dashboard render every other card normally.
|
||||
"""
|
||||
create = client.post(
|
||||
"/api/plugins/kanban/tasks",
|
||||
json={"title": "doomed", "assignee": "alice"},
|
||||
)
|
||||
assert create.status_code == 200, create.text
|
||||
|
||||
# Force task_age to raise an exception type _safe_int does NOT handle —
|
||||
# simulates a future regression where someone re-introduces an unguarded
|
||||
# operation in task_age. ValueError on '%s' would be absorbed by _safe_int
|
||||
# and never reach the outer try/except, so it would not exercise the
|
||||
# contract this test pins.
|
||||
def _boom(_task):
|
||||
raise RuntimeError("simulated future task_age bug")
|
||||
monkeypatch.setattr("hermes_cli.kanban_db.task_age", _boom)
|
||||
|
||||
r = client.get("/api/plugins/kanban/board")
|
||||
assert r.status_code == 200, r.text
|
||||
|
||||
payload = r.json()
|
||||
# /board returns columns as a list of {name, tasks} — not a dict — so
|
||||
# flatten across all columns to find our seeded task.
|
||||
tasks = [t for col in payload["columns"] for t in col["tasks"]]
|
||||
assert len(tasks) == 1, f"expected exactly the seeded task, got {tasks!r}"
|
||||
# Strict equality: the literal fallback dict from plugin_api._task_dict
|
||||
# is the published contract the dashboard UI relies on. Key renames or
|
||||
# silent additions should fail this test on purpose.
|
||||
assert tasks[0]["age"] == _FALLBACK_AGE
|
||||
|
||||
|
||||
def test_single_task_endpoint_survives_task_age_exception(client, monkeypatch):
|
||||
"""GET /tasks/:id also calls _task_dict — same fallback should kick in.
|
||||
|
||||
This is the "drawer view" path: the user clicks one card and we serialize
|
||||
just that task. A corrupt timestamp on a single task should not block the
|
||||
user from opening its drawer.
|
||||
"""
|
||||
create = client.post(
|
||||
"/api/plugins/kanban/tasks",
|
||||
json={"title": "drawer-target", "assignee": "bob"},
|
||||
)
|
||||
task_id = create.json()["task"]["id"]
|
||||
|
||||
def _boom(_task):
|
||||
raise RuntimeError("simulated future task_age bug")
|
||||
monkeypatch.setattr("hermes_cli.kanban_db.task_age", _boom)
|
||||
|
||||
r = client.get(f"/api/plugins/kanban/tasks/{task_id}")
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json()["task"]["age"] == _FALLBACK_AGE
|
||||
|
||||
|
||||
def test_create_task_probe_error_does_not_break_create(client, monkeypatch):
|
||||
"""Probe failure must never break task creation."""
|
||||
def _raise():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue