mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-30 06:41:51 +00:00
fix(kanban-dashboard): restore implementations dropped during salvages (#28481)
Four kanban dashboard test failures, all from PR salvages that picked up the test additions but dropped the corresponding implementations. - BOARD_COLUMNS: add 'review' (status added by PRf55d94a1ebut the board API never grew the column → test_board_empty failed because VALID_STATUSES - {archived} mismatched the rendered columns). - update_task: enrich the 'ready' 409 detail with the blocking parent list (id, title, status) and add _parents_blocking_ready helper. Implementation lost in the #26744 salvage (commite215558ba) which pinned the test but not the server-side code. - dist/index.js: add parseApiErrorMessage helper, wire it through the drag/drop banner, add patchErr state to the TaskDrawer and surface it inline by the action row. Lost in the same #26744 salvage. - test_diagnostics_endpoint_severity_filter: update to at-or-above semantics (PRa94ddd807changed the filter from exact-match so the warning filter now correctly includes error+critical too).
This commit is contained in:
parent
b58b4188f6
commit
362ef912ea
3 changed files with 76 additions and 7 deletions
31
plugins/kanban/dashboard/dist/index.js
vendored
31
plugins/kanban/dashboard/dist/index.js
vendored
|
|
@ -51,6 +51,24 @@
|
|||
return str;
|
||||
}
|
||||
|
||||
// ``fetchJSON`` throws ``Error("<status>: <raw body>")`` on non-2xx, and
|
||||
// FastAPI bodies look like ``{"detail":"<message>"}``. Pull the
|
||||
// human-readable message out so banners/toasts don't have to leak HTTP
|
||||
// plumbing at the user (e.g. ``409: {"detail":"…"}``). See #26744.
|
||||
function parseApiErrorMessage(err) {
|
||||
const raw = (err && err.message) ? String(err.message) : String(err || "");
|
||||
const m = raw.match(/^(\d{3}):\s*(.*)$/s);
|
||||
const body = m ? m[2] : raw;
|
||||
try {
|
||||
const parsed = JSON.parse(body);
|
||||
if (parsed && typeof parsed.detail === "string") return parsed.detail;
|
||||
if (parsed && parsed.detail && typeof parsed.detail.message === "string") {
|
||||
return parsed.detail.message;
|
||||
}
|
||||
} catch (_e) { /* not JSON — fall through to raw body */ }
|
||||
return body || raw;
|
||||
}
|
||||
|
||||
// Order matches BOARD_COLUMNS in plugin_api.py.
|
||||
const COLUMN_ORDER = ["triage", "todo", "ready", "running", "blocked", "done"];
|
||||
// English fallback dictionaries — used when the i18n catalog is missing
|
||||
|
|
@ -654,7 +672,7 @@
|
|||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(patch),
|
||||
}).catch(function (err) {
|
||||
setError(tx(t, "moveFailed", "Move failed: ") + (err.message || err));
|
||||
setError(tx(t, "moveFailed", "Move failed: ") + parseApiErrorMessage(err));
|
||||
loadBoard();
|
||||
});
|
||||
}, [loadBoard, board, t]);
|
||||
|
|
@ -2709,6 +2727,11 @@
|
|||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [err, setErr] = useState(null);
|
||||
// Surface PATCH failures (e.g. 409 "parent not done") right next to
|
||||
// the drawer's action row — without it, the drawer's only error
|
||||
// surface (``err``) is hidden behind the loaded ``data`` and the
|
||||
// Ready/Block/Complete buttons feel like no-ops. See #26744.
|
||||
const [patchErr, setPatchErr] = useState(null);
|
||||
const [newComment, setNewComment] = useState("");
|
||||
const [editing, setEditing] = useState(false);
|
||||
// Home-channel notification toggles. homeChannels is the list of platforms
|
||||
|
|
@ -2720,7 +2743,7 @@
|
|||
|
||||
const load = useCallback(function () {
|
||||
return SDK.fetchJSON(withBoard(`${API}/tasks/${encodeURIComponent(props.taskId)}`, boardSlug))
|
||||
.then(function (d) { setData(d); setErr(null); })
|
||||
.then(function (d) { setData(d); setErr(null); setPatchErr(null); })
|
||||
.catch(function (e) { setErr(String(e.message || e)); })
|
||||
.finally(function () { setLoading(false); });
|
||||
}, [props.taskId, boardSlug]);
|
||||
|
|
@ -2764,11 +2787,13 @@
|
|||
}
|
||||
const finalPatch = withCompletionSummary(patch, 1);
|
||||
if (!finalPatch) return Promise.resolve();
|
||||
setPatchErr(null);
|
||||
return SDK.fetchJSON(withBoard(`${API}/tasks/${encodeURIComponent(props.taskId)}`, boardSlug), {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(finalPatch),
|
||||
}).then(function () { load(); props.onRefresh(); });
|
||||
}).then(function () { load(); props.onRefresh(); })
|
||||
.catch(function (e) { setPatchErr(parseApiErrorMessage(e)); });
|
||||
};
|
||||
|
||||
// Triage specifier — calls the auxiliary LLM to flesh out a rough
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ def _conn(board: Optional[str] = None):
|
|||
# tasks into ``todo`` and makes the dashboard look like the Scheduled column
|
||||
# disappeared.
|
||||
BOARD_COLUMNS: list[str] = [
|
||||
"triage", "todo", "scheduled", "ready", "running", "blocked", "done",
|
||||
"triage", "todo", "scheduled", "ready", "running", "blocked", "review", "done",
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -683,6 +683,23 @@ def update_task(task_id: str, payload: UpdateTaskBody, board: Optional[str] = Qu
|
|||
else:
|
||||
raise HTTPException(status_code=400, detail=f"unknown status: {s}")
|
||||
if not ok:
|
||||
# For ``ready``, name the blocking parent(s) so the dashboard
|
||||
# can render an actionable toast instead of a silent no-op.
|
||||
# See #26744.
|
||||
if s == "ready":
|
||||
blockers = _parents_blocking_ready(conn, task_id)
|
||||
if blockers:
|
||||
names = ", ".join(
|
||||
f"{p['title']!r} ({p['id']}, status={p['status']})"
|
||||
for p in blockers
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=(
|
||||
f"Cannot move to 'ready': blocked by parent(s) "
|
||||
f"not done — {names}"
|
||||
),
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"status transition to {s!r} not valid from current state",
|
||||
|
|
@ -747,6 +764,29 @@ def delete_task(task_id: str, board: Optional[str] = Query(None)):
|
|||
conn.close()
|
||||
|
||||
|
||||
def _parents_blocking_ready(
|
||||
conn: sqlite3.Connection, task_id: str,
|
||||
) -> list:
|
||||
"""Return parent rows (``id``, ``title``, ``status``) that aren't ``done``
|
||||
and therefore prevent ``task_id`` from being promoted to ``ready``.
|
||||
|
||||
Used to enrich the 409 response from :func:`update_task` so the
|
||||
dashboard can show an actionable toast (#26744) instead of a silent
|
||||
no-op. Returns ``[]`` when nothing blocks the transition (e.g. no
|
||||
parents, or all parents already done).
|
||||
"""
|
||||
rows = conn.execute(
|
||||
"SELECT t.id, t.title, t.status FROM tasks t "
|
||||
"JOIN task_links l ON l.parent_id = t.id "
|
||||
"WHERE l.child_id = ? AND t.status != 'done'",
|
||||
(task_id,),
|
||||
).fetchall()
|
||||
return [
|
||||
{"id": r["id"], "title": r["title"], "status": r["status"]}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def _set_status_direct(
|
||||
conn: sqlite3.Connection, task_id: str, new_status: str,
|
||||
) -> bool:
|
||||
|
|
|
|||
|
|
@ -1939,7 +1939,8 @@ def test_diagnostics_endpoint_surfaces_blocked_hallucination(client):
|
|||
|
||||
|
||||
def test_diagnostics_endpoint_severity_filter(client):
|
||||
"""Warning-severity filter excludes error-severity entries."""
|
||||
"""Severity filter is at-or-above: warning includes warning+error+critical,
|
||||
error includes error+critical, critical is exact (no higher level)."""
|
||||
conn = kb.connect()
|
||||
try:
|
||||
# A warning-severity diagnostic (prose phantom) on one task.
|
||||
|
|
@ -1958,12 +1959,15 @@ def test_diagnostics_endpoint_severity_filter(client):
|
|||
finally:
|
||||
conn.close()
|
||||
|
||||
# warning filter is at-or-above → both the warning AND the error pass.
|
||||
r = client.get("/api/plugins/kanban/diagnostics?severity=warning")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["count"] == 1
|
||||
assert data["diagnostics"][0]["task_id"] == p1
|
||||
assert data["count"] == 2
|
||||
task_ids = {row["task_id"] for row in data["diagnostics"]}
|
||||
assert task_ids == {p1, p2}
|
||||
|
||||
# error filter is at-or-above → only the error passes (warning is below).
|
||||
r = client.get("/api/plugins/kanban/diagnostics?severity=error")
|
||||
data = r.json()
|
||||
assert data["count"] == 1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue