| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import type { AgentApproval, WorkspaceSession } from "@g1t/contracts"; |
| 5 | |
| 6 | import { filterSessions, recentSessions, shortRecents } from "./recents.ts"; |
| 7 | |
| 8 | const session = (id: string, at: string, over: Partial<WorkspaceSession> = {}): WorkspaceSession => ({ |
| 9 | kind: "hosted", |
| 10 | id, |
| 11 | repo: "acme/web", |
| 12 | repoId: "rep_1", |
| 13 | title: `Session ${id}`, |
| 14 | actor: "ada", |
| 15 | harness: "claude_managed_agents", |
| 16 | agent: null, |
| 17 | branch: null, |
| 18 | item: null, |
| 19 | itemTitle: null, |
| 20 | pull: null, |
| 21 | pullTitle: null, |
| 22 | pullStatus: null, |
| 23 | state: "active", |
| 24 | startedAt: at, |
| 25 | lastActivityAt: at, |
| 26 | subagents: 0, |
| 27 | step: null, |
| 28 | session: null, |
| 29 | ...over, |
| 30 | }); |
| 31 | |
| 32 | const waitingOn = (sessionId: string) => ({ sessionId, status: "waiting" }) as AgentApproval; |
| 33 | |
| 34 | test("Sessions in the sidebar are the viewer's, those on their work items and those waiting on them, each once, newest first", () => { |
| 35 | const sessions = recentSessions({ |
| 36 | me: "Ada", |
| 37 | mine: [session("ses_mine", "2026-10-21T12:00:00Z", { state: "ended" }), session("ses_both", "2026-10-21T12:03:00Z")], |
| 38 | live: [ |
| 39 | session("ses_both", "2026-10-21T12:03:00Z"), |
| 40 | session("ses_item", "2026-10-21T12:02:00Z", { actor: "bea", item: 4, state: "idle" }), |
| 41 | session("ses_asks", "2026-10-21T12:01:00Z", { actor: "bea" }), |
| 42 | session("ses_other", "2026-10-21T12:04:00Z", { actor: "bea", item: 5 }), |
| 43 | session("run_1", "2026-10-21T12:05:00Z", { kind: "agent", actor: "ada" }), |
| 44 | ], |
| 45 | items: [4], |
| 46 | approvals: [waitingOn("ses_asks")], |
| 47 | }); |
| 48 | assert.deepEqual( |
| 49 | sessions.map((s) => [s.id, s.status]), |
| 50 | [ |
| 51 | ["ses_both", "working"], |
| 52 | ["ses_item", "idle"], |
| 53 | ["ses_asks", "waiting"], |
| 54 | ["ses_mine", "ended"], |
| 55 | ], |
| 56 | ); |
| 57 | // The calls could not be read: no session reads as waiting, and one only waiting on the viewer is not listed. |
| 58 | const unread = recentSessions({ me: "ada", mine: [], live: [session("ses_asks", "2026-10-21T12:01:00Z", { actor: "bea" })], items: [], approvals: null }); |
| 59 | assert.deepEqual(unread, []); |
| 60 | }); |
| 61 | |
| 62 | test("Sessions is short, but what must stay in view stays; the filter and search reach sessions", () => { |
| 63 | const { shown, more } = shortRecents(["a", "b", "c", "d"], 2, (row) => row === "d"); |
| 64 | assert.deepEqual(shown, ["a", "b", "d"]); |
| 65 | assert.equal(more, 1); |
| 66 | const sessions = recentSessions({ |
| 67 | me: "ada", |
| 68 | mine: [session("ses_1", "2026-10-21T12:00:00Z", { title: "Fix login" }), session("ses_2", "2026-10-21T12:01:00Z", { title: "Docs" })], |
| 69 | live: [], |
| 70 | items: [], |
| 71 | approvals: [waitingOn("ses_2")], |
| 72 | }); |
| 73 | assert.deepEqual(filterSessions(sessions, "all", "").length, 2); |
| 74 | assert.deepEqual( |
| 75 | filterSessions(sessions, "unread", "").map((s) => s.id), |
| 76 | ["ses_2"], |
| 77 | ); |
| 78 | assert.deepEqual(filterSessions(sessions, "mentions", ""), []); |
| 79 | assert.deepEqual( |
| 80 | filterSessions(sessions, "all", "LOGIN").map((s) => s.id), |
| 81 | ["ses_1"], |
| 82 | ); |
| 83 | }); |