fix(dashboard): return authoritative archived state and cover ai-session archive

Archive/unarchive routes were returning the row-update boolean rather than
the actual archived flag, so a no-op unarchive replied with `archived: true`.
Re-read the row and report its current state instead. Also adds tests for
archive (terminal-only), unarchive event emission, and listAll filtering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-01 10:57:53 -07:00
parent c6fbedbd75
commit 9b415e7169
3 changed files with 83 additions and 6 deletions

View File

@@ -319,6 +319,81 @@ describe("AiSessionStore", () => {
expect(projectA.every((session) => session.projectId === "project-a")).toBe(true);
});
describe("archive / unarchive / listAll", () => {
it("archive only flips terminal sessions and emits updated", () => {
seedSession({ id: "S-complete", status: "complete" });
seedSession({ id: "S-error", status: "error" });
seedSession({ id: "S-generating", status: "generating" });
seedSession({ id: "S-awaiting", status: "awaiting_input" });
const updated: string[] = [];
store.on("ai_session:updated", (summary) => updated.push(summary.id));
expect(store.archive("S-complete")).toBe(true);
expect(store.archive("S-error")).toBe(true);
expect(store.archive("S-generating")).toBe(false);
expect(store.archive("S-awaiting")).toBe(false);
expect(store.archive("missing")).toBe(false);
expect(store.get("S-complete")?.archived).toBe(1);
expect(store.get("S-error")?.archived).toBe(1);
expect(store.get("S-generating")?.archived ?? 0).toBe(0);
expect(updated.sort()).toEqual(["S-complete", "S-error"]);
});
it("unarchive flips archived sessions back and emits updated only when changed", () => {
seedSession({ id: "S-complete", status: "complete" });
store.archive("S-complete");
const updated: string[] = [];
store.on("ai_session:updated", (summary) => updated.push(summary.id));
expect(store.unarchive("S-complete")).toBe(true);
expect(store.get("S-complete")?.archived ?? 0).toBe(0);
expect(updated).toEqual(["S-complete"]);
// No-op unarchive of an already-unarchived row still updates the row
// (touches updatedAt) — this test pins the current behavior so the
// route handler can rely on `get()` for the authoritative state.
updated.length = 0;
expect(store.unarchive("S-complete")).toBe(true);
expect(store.get("S-complete")?.archived ?? 0).toBe(0);
expect(store.unarchive("missing")).toBe(false);
});
it("listAll excludes archived rows by default and includes them when requested", () => {
seedSession({ id: "S-active", status: "generating" });
seedSession({ id: "S-done-visible", status: "complete" });
seedSession({ id: "S-done-hidden", status: "complete" });
store.archive("S-done-hidden");
const visible = store.listAll();
expect(visible.map((s) => s.id).sort()).toEqual(["S-active", "S-done-visible"]);
const all = store.listAll(undefined, { includeArchived: true });
expect(all.map((s) => s.id).sort()).toEqual(["S-active", "S-done-hidden", "S-done-visible"]);
});
it("listAll filters by projectId with and without archived", () => {
seedSession({ id: "S-a-active", status: "generating", projectId: "project-a" });
seedSession({ id: "S-a-done", status: "complete", projectId: "project-a" });
seedSession({ id: "S-a-archived", status: "complete", projectId: "project-a" });
seedSession({ id: "S-b-done", status: "complete", projectId: "project-b" });
store.archive("S-a-archived");
const projectA = store.listAll("project-a");
expect(projectA.map((s) => s.id).sort()).toEqual(["S-a-active", "S-a-done"]);
const projectAWithArchived = store.listAll("project-a", { includeArchived: true });
expect(projectAWithArchived.map((s) => s.id).sort()).toEqual([
"S-a-active",
"S-a-archived",
"S-a-done",
]);
});
});
it("ping updates updatedAt for existing sessions without emitting updates", () => {
seedSession({ id: "S-ping", status: "awaiting_input" });