From eccf895f79dd29e6974b0dbf6887377e465abdfd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 2 Aug 2026 20:52:27 -0700 Subject: [PATCH] refactor(ui): consolidate workboard test fixtures (#118415) --- ui/src/lib/workboard/index.test.ts | 1225 +++++++++++----------------- 1 file changed, 457 insertions(+), 768 deletions(-) diff --git a/ui/src/lib/workboard/index.test.ts b/ui/src/lib/workboard/index.test.ts index a05bae05744f..956060b57862 100644 --- a/ui/src/lib/workboard/index.test.ts +++ b/ui/src/lib/workboard/index.test.ts @@ -44,12 +44,93 @@ function requestPatch(client: ReturnType, index: number) { ?.patch; } +function requestCalls(client: WorkboardTestClient, method: string) { + return client.request.mock.calls.filter(([calledMethod]) => calledMethod === method); +} + +function createSequencedClient(routes: Record, fallback: unknown = {}) { + const remaining = Object.fromEntries( + Object.entries(routes).map(([method, replies]) => [method, [...replies]]), + ); + return createClient((method) => { + const replies = remaining[method]; + const reply = replies && replies.length > 0 ? replies.shift() : fallback; + if (reply instanceof Error) { + throw reply; + } + return reply; + }); +} + const sampleCard = createWorkboardCard(); const sampleSession = createGatewaySession(); const sampleTaskSessionKey = "subagent:workboard-default-card-1"; const sampleTask = createWorkboardTask(); +function listResult(cards: unknown[] = [sampleCard], statuses: string[] = ["todo", "done"]) { + return { cards, statuses }; +} + +function createLinkedCard(overrides: Partial = {}) { + return createWorkboardCard({ + status: "running", + sessionKey: sampleTaskSessionKey, + runId: "run-1", + taskId: sampleTask.taskId, + ...overrides, + }); +} + +function createSessionCard(overrides: Partial = {}) { + return createWorkboardCard({ sessionKey: sampleSession.key, ...overrides }); +} + +function createStaleSessionCard( + lastSessionUpdatedAt: number, + overrides: Partial = {}, +) { + return createSessionCard({ + status: "running", + ...overrides, + metadata: { + ...overrides.metadata, + stale: { + detectedAt: 1, + lastSessionUpdatedAt, + reason: "Linked thread has not reported recent activity.", + ...overrides.metadata?.stale, + }, + }, + }); +} + +function createConfirmationCards(count: number) { + return Array.from({ length: count }, (_, index) => + createWorkboardCard({ + id: `card-${index}`, + status: "running", + taskId: `task-${index}`, + }), + ); +} + +function createConfirmationClient(failTaskId?: string) { + return createClient((method, params) => { + if (method === "tasks.list") { + return { tasks: [] }; + } + if (method !== "tasks.get") { + return {}; + } + const taskId = (params as { taskId: string }).taskId; + if (taskId === failTaskId) { + throw new Error("task confirmation unavailable"); + } + return { task: createWorkboardTask({ id: taskId, taskId }) }; + }); +} + let host: object; let state: ReturnType; @@ -79,6 +160,57 @@ function captureSession(client: WorkboardTestClient, session: GatewaySessionRow return captureSessionToWorkboard({ host, client, session }); } +function dispatchBoard( + client: WorkboardTestClient, + options: Omit[0], "host" | "client"> = {}, +) { + return dispatchWorkboard({ host, client, ...options }); +} + +function saveDraft(client: WorkboardTestClient) { + return saveWorkboardCardDraft({ host, client }); +} + +function moveCard( + client: WorkboardTestClient, + options: Omit[0], "host" | "client">, +) { + return moveWorkboardCard({ host, client, ...options }); +} + +function startCard( + client: WorkboardTestClient, + options: Omit[0], "host" | "client">, +) { + return startWorkboardCard({ host, client, ...options }); +} + +function startSampleCard( + client: WorkboardTestClient, + options: Omit[0], "host" | "client" | "card"> = {}, +) { + return startCard(client, { card: sampleCard, ...options }); +} + +function stopCard(client: WorkboardTestClient, card: WorkboardCard) { + return stopWorkboardCard({ host, client, card }); +} + +function commentCard( + client: WorkboardTestClient, + options: Omit[0], "host" | "client">, +) { + return addWorkboardCardComment({ host, client, ...options }); +} + +function deleteCard(client: WorkboardTestClient, cardId: string) { + return deleteWorkboardCard({ host, client, cardId }); +} + +function archiveCard(client: WorkboardTestClient, cardId: string) { + return archiveWorkboardCard({ host, client, cardId }); +} + function setLoadedCard(card: WorkboardCard, task?: WorkboardTaskSummary) { state.loaded = true; state.cards = [card]; @@ -145,10 +277,10 @@ describe("workboard controller", () => { const firstCard = { ...sampleCard, title: "First host" }; const secondCard = { ...sampleCard, title: "Second host" }; const firstClient = createClient({ - "workboard.cards.list": { cards: [firstCard], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([firstCard], ["todo", "done"]), }); const secondClient = createClient({ - "workboard.cards.list": { cards: [secondCard], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([secondCard], ["todo", "done"]), }); await Promise.all([ @@ -224,15 +356,8 @@ describe("workboard controller", () => { it("rejects an invalidated generation after its replacement loads", async () => { const staleList = createDeferred(); const currentCard = { ...sampleCard, title: "Current generation" }; - let listCalls = 0; - const client = createClient((method) => { - if (method === "workboard.cards.list") { - listCalls += 1; - return listCalls === 1 - ? staleList.promise - : { cards: [currentCard], statuses: ["todo", "done"] }; - } - return {}; + const client = createSequencedClient({ + "workboard.cards.list": [staleList.promise, listResult([currentCard])], }); const staleLoad = loadBoard(client); @@ -246,7 +371,7 @@ describe("workboard controller", () => { }); await staleLoad; - expect(listCalls).toBe(2); + expect(requestCalls(client, "workboard.cards.list").length).toBe(2); expect(getWorkboardState(host).cards).toEqual([currentCard]); }); @@ -263,7 +388,7 @@ describe("workboard controller", () => { return lifecycleWrite.promise; } if (method === "workboard.cards.list") { - return { cards: [updatedCard], statuses: ["todo", "running"] }; + return listResult([updatedCard], ["todo", "running"]); } return {}; }); @@ -296,7 +421,7 @@ describe("workboard controller", () => { it("loads cards through the plugin gateway method", async () => { const client = createClient({ - "workboard.cards.list": { cards: [sampleCard], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([sampleCard], ["todo", "done"]), }); await loadBoard(client); @@ -308,7 +433,7 @@ describe("workboard controller", () => { it("refreshes diagnostics before listing cards when requested", async () => { const client = createClient({ "workboard.cards.diagnostics.refresh": { diagnostics: [], count: 0 }, - "workboard.cards.list": { cards: [sampleCard], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([sampleCard], ["todo", "done"]), }); await loadBoard(client, { refreshDiagnostics: true }); @@ -322,7 +447,7 @@ describe("workboard controller", () => { if (method === "workboard.cards.diagnostics.refresh") { throw new Error("diagnostics denied"); } - return { cards: [sampleCard], statuses: ["todo", "done"] }; + return listResult([sampleCard], ["todo", "done"]); }); await loadBoard(client, { refreshDiagnostics: true }); @@ -341,7 +466,7 @@ describe("workboard controller", () => { runId: "run-1", } satisfies WorkboardCard; const client = createClient({ - "workboard.cards.list": { cards: [linked], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([linked], ["todo", "done"]), "tasks.list": { tasks: [sampleTask] }, }); @@ -365,7 +490,7 @@ describe("workboard controller", () => { state.tasksByCardId.set(sampleCard.id, sampleTask); const client = createClient((method) => { if (method === "workboard.cards.list") { - return { cards: [linked], statuses: ["todo", "running", "done"] }; + return listResult([linked], ["todo", "running", "done"]); } if (method === "tasks.list") { throw new Error("task ledger unavailable"); @@ -391,7 +516,7 @@ describe("workboard controller", () => { } satisfies WorkboardCard; const client = createClient((method) => { if (method === "workboard.cards.list") { - return { cards: [linked], statuses: ["todo", "done"] }; + return listResult([linked], ["todo", "done"]); } if (method === "tasks.list") { return { tasks: [] }; @@ -420,7 +545,7 @@ describe("workboard controller", () => { runId: "run-1", } satisfies WorkboardCard; const client = createClient({ - "workboard.cards.list": { cards: [linked], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([linked], ["todo", "done"]), "tasks.list": { tasks: [] }, "tasks.get": { task: sampleTask }, }); @@ -443,7 +568,7 @@ describe("workboard controller", () => { } satisfies WorkboardCard; const client = createClient((method) => { if (method === "workboard.cards.list") { - return { cards: [linked], statuses: ["todo", "running", "done"] }; + return listResult([linked], ["todo", "running", "done"]); } if (method === "tasks.list") { return { tasks: [] }; @@ -476,7 +601,7 @@ describe("workboard controller", () => { state.tasksByCardId.set(linked.id, sampleTask); const client = createClient((method) => { if (method === "workboard.cards.list") { - return { cards: [linked], statuses: ["todo", "running", "done"] }; + return listResult([linked], ["todo", "running", "done"]); } if (method === "tasks.list") { return { tasks: [] }; @@ -581,7 +706,7 @@ describe("workboard controller", () => { runId: "run-1", } satisfies WorkboardCard; const client = createClient({ - "workboard.cards.list": { cards: [linked], statuses: ["todo", "running", "done"] }, + "workboard.cards.list": listResult([linked], ["todo", "running", "done"]), "tasks.list": { tasks: [] }, "tasks.get": { task: sampleTask }, }); @@ -611,7 +736,7 @@ describe("workboard controller", () => { updatedAt: 10, }; const client = createClient({ - "workboard.cards.list": { cards: [linked], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([linked], ["todo", "done"]), "tasks.list": { tasks: [sampleTask, unrelated] }, }); @@ -623,7 +748,7 @@ describe("workboard controller", () => { it("records live refresh metadata after reconciliation", async () => { const client = createClient({ - "workboard.cards.list": { cards: [sampleCard], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([sampleCard], ["todo", "done"]), "tasks.list": { tasks: [] }, }); await refreshBoard(client, "live"); @@ -638,7 +763,7 @@ describe("workboard controller", () => { it("preserves mutation errors during successful live refreshes", async () => { state.error = "move denied"; const client = createClient({ - "workboard.cards.list": { cards: [sampleCard], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([sampleCard], ["todo", "done"]), "tasks.list": { tasks: [] }, }); @@ -656,7 +781,7 @@ describe("workboard controller", () => { if (!cardsAvailable) { throw new Error("cards unavailable"); } - return { cards: [sampleCard], statuses: ["todo", "done"] }; + return listResult([sampleCard], ["todo", "done"]); } if (method === "tasks.list") { return { tasks: [] }; @@ -689,7 +814,7 @@ describe("workboard controller", () => { if (!cardsAvailable) { throw new Error("cards unavailable"); } - return { cards: [sampleCard], statuses: ["todo", "done"] }; + return listResult([sampleCard], ["todo", "done"]); } if (method === "tasks.list") { return { tasks: [] }; @@ -742,7 +867,7 @@ describe("workboard controller", () => { await refreshBoard(null, "manual"); const client = createClient({ - "workboard.cards.list": { cards: [sampleCard], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([sampleCard], ["todo", "done"]), "tasks.list": { tasks: [] }, }); await loadBoard(client); @@ -756,7 +881,7 @@ describe("workboard controller", () => { const refreshedCard = { ...sampleCard, title: "Refreshed card" }; const client = createClient((method) => { if (method === "workboard.cards.list") { - return { cards: [refreshedCard], statuses: ["todo", "done"] }; + return listResult([refreshedCard], ["todo", "done"]); } if (method === "tasks.list") { throw new Error("tasks unavailable"); @@ -783,7 +908,7 @@ describe("workboard controller", () => { let tasksAvailable = false; const client = createClient((method) => { if (method === "workboard.cards.list") { - return { cards: [linkedCard], statuses: ["todo", "done"] }; + return listResult([linkedCard], ["todo", "done"]); } if (method === "tasks.list") { if (!tasksAvailable) { @@ -821,7 +946,7 @@ describe("workboard controller", () => { state.tasksByCardId.set(sampleCard.id, sampleTask); const client = createClient((method) => { if (method === "workboard.cards.list") { - return { cards: [linkedCard], statuses: ["todo", "done"] }; + return listResult([linkedCard], ["todo", "done"]); } if (method === "tasks.get") { throw new Error("tasks unavailable"); @@ -846,7 +971,7 @@ describe("workboard controller", () => { state.tasksByCardId.set(sampleCard.id, { ...sampleTask, status: "completed" }); const client = createClient((method) => { if (method === "workboard.cards.list") { - return { cards: [linkedCard], statuses: ["todo", "done"] }; + return listResult([linkedCard], ["todo", "done"]); } if (method === "tasks.get") { throw new GatewayRequestError({ @@ -874,7 +999,7 @@ describe("workboard controller", () => { it("keeps canonical task unlinks during bounded live refreshes", async () => { state.tasksByCardId.set(sampleCard.id, sampleTask); const client = createClient({ - "workboard.cards.list": { cards: [sampleCard], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([sampleCard], ["todo", "done"]), }); await refreshBoard(client, "live"); @@ -953,7 +1078,7 @@ describe("workboard controller", () => { state.tasksByCardId.set(sampleCard.id, sampleTask); const client = createClient((method) => { if (method === "workboard.cards.list") { - return { cards: [replacementCard], statuses: ["todo", "done"] }; + return listResult([replacementCard], ["todo", "done"]); } if (method === "tasks.get") { return { task: replacementTask }; @@ -980,7 +1105,7 @@ describe("workboard controller", () => { })); const client = createClient((method, params) => { if (method === "workboard.cards.list") { - return { cards: state.cards, statuses: ["todo", "done"] }; + return listResult(state.cards, ["todo", "done"]); } if (method === "tasks.get") { const taskId = (params as { taskId: string }).taskId; @@ -1071,9 +1196,7 @@ describe("workboard controller", () => { }); await refreshBoard(client, "live"); - const firstDiscoveryCalls = client.request.mock.calls.filter( - ([method]) => method === "tasks.list", - ); + const firstDiscoveryCalls = requestCalls(client, "tasks.list"); expect(firstDiscoveryCalls).toHaveLength(4); expect(firstDiscoveryCalls[0]?.[1]).toMatchObject({ sessionKey: "agent:worker-0:subagent:workboard-default-card-0", @@ -1083,9 +1206,7 @@ describe("workboard controller", () => { vi.clearAllMocks(); await refreshBoard(client, "live"); - const secondDiscoveryCalls = client.request.mock.calls.filter( - ([method]) => method === "tasks.list", - ); + const secondDiscoveryCalls = requestCalls(client, "tasks.list"); expect(secondDiscoveryCalls).toHaveLength(2); expect(getWorkboardState(host).cards.every((card) => Boolean(card.taskId))).toBe(true); }); @@ -1099,7 +1220,7 @@ describe("workboard controller", () => { } satisfies WorkboardCard; const client = createClient((method) => { if (method === "workboard.cards.list") { - return { cards: [linkedCard], statuses: ["todo", "running", "done"] }; + return listResult([linkedCard], ["todo", "running", "done"]); } if (method === "tasks.list") { return { @@ -1134,7 +1255,7 @@ describe("workboard controller", () => { state.missingTaskIds = new Set([missingTaskId]); const client = createClient((method, params) => { if (method === "workboard.cards.list") { - return { cards: [linkedCard], statuses: ["todo", "running", "done"] }; + return listResult([linkedCard], ["todo", "running", "done"]); } if (method === "tasks.list") { return { tasks: [replacementTask] }; @@ -1180,7 +1301,7 @@ describe("workboard controller", () => { } satisfies WorkboardCard; const client = createClient((method, params) => { if (method === "workboard.cards.list") { - return { cards: [linkedCard], statuses: ["todo", "running", "done"] }; + return listResult([linkedCard], ["todo", "running", "done"]); } if (method === "tasks.list") { return (params as { cursor?: string }).cursor === "500" @@ -1212,7 +1333,7 @@ describe("workboard controller", () => { } satisfies WorkboardCard; const client = createClient((method, params) => { if (method === "workboard.cards.list") { - return { cards: [linkedCard], statuses: ["todo", "running", "done"] }; + return listResult([linkedCard], ["todo", "running", "done"]); } if (method === "tasks.list") { return (params as { cursor?: string }).cursor === "500" @@ -1226,7 +1347,7 @@ describe("workboard controller", () => { await refreshBoard(client, "live"); await refreshBoard(client, "live"); - const discoveryCalls = client.request.mock.calls.filter(([method]) => method === "tasks.list"); + const discoveryCalls = requestCalls(client, "tasks.list"); expect(discoveryCalls.map(([, params]) => params)).toEqual([ { limit: 500 }, { limit: 500, cursor: "500" }, @@ -1285,13 +1406,11 @@ describe("workboard controller", () => { orchestrated: [], count: 0, }, - "workboard.cards.list": { cards: [sampleCard], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([sampleCard], ["todo", "done"]), "tasks.list": { tasks: [] }, }); - await dispatchWorkboard({ - host, - client: client as never, + await dispatchBoard(client, { requestUpdate: () => requestUpdates.push([state.loading, state.dispatching]), }); @@ -1315,11 +1434,11 @@ describe("workboard controller", () => { orchestrated: [], count: 0, }, - "workboard.cards.list": { cards: [sampleCard], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([sampleCard], ["todo", "done"]), "tasks.list": { tasks: [] }, }); - await dispatchWorkboard({ host, client: client as never }); + await dispatchBoard(client); expect(client.request).toHaveBeenCalledWith("workboard.cards.dispatch", { boardId: "ops" }); }); @@ -1334,11 +1453,11 @@ describe("workboard controller", () => { orchestrated: [], count: 0, }, - "workboard.cards.list": { cards: [sampleCard], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([sampleCard], ["todo", "done"]), "tasks.list": { tasks: [] }, }); - await dispatchWorkboard({ host, client: client as never }); + await dispatchBoard(client); expect(state.lastRefreshError).toBeNull(); }); @@ -1357,7 +1476,7 @@ describe("workboard controller", () => { return { promoted: [], reclaimed: [], blocked: [], orchestrated: [] }; } if (method === "workboard.cards.list") { - return { cards: [sampleCard], statuses: ["todo", "done"] }; + return listResult([sampleCard], ["todo", "done"]); } if (method === "tasks.list") { return { tasks: [] }; @@ -1365,9 +1484,9 @@ describe("workboard controller", () => { return {}; }); - const save = saveWorkboardCardDraft({ host, client: client as never }); + const save = saveDraft(client); await Promise.resolve(); - await dispatchWorkboard({ host, client: client as never }); + await dispatchBoard(client); expect(client.request).not.toHaveBeenCalledWith("workboard.cards.dispatch", {}); @@ -1389,16 +1508,12 @@ describe("workboard controller", () => { return {}; }); - const firstMove = moveWorkboardCard({ - host, - client: client as never, + const firstMove = moveCard(client, { cardId: sampleCard.id, status: "review", position: 1000, }); - const secondMove = moveWorkboardCard({ - host, - client: client as never, + const secondMove = moveCard(client, { cardId: secondCard.id, status: "review", position: 2000, @@ -1406,9 +1521,7 @@ describe("workboard controller", () => { await Promise.resolve(); expect(getWorkboardState(host).busyCardIds).toEqual(new Set([sampleCard.id, secondCard.id])); - await moveWorkboardCard({ - host, - client: client as never, + await moveCard(client, { cardId: sampleCard.id, status: "blocked", position: 3000, @@ -1427,7 +1540,7 @@ describe("workboard controller", () => { expect(getWorkboardState(host).busyCardIds).toEqual(new Set([secondCard.id])); expect(getWorkboardState(host).draggedCardId).toBe(secondCard.id); - await dispatchWorkboard({ host, client: client as never }); + await dispatchBoard(client); expect(client.request).not.toHaveBeenCalledWith("workboard.cards.dispatch", {}); second.resolve({ card: { ...secondCard, status: "review" } }); @@ -1445,7 +1558,7 @@ describe("workboard controller", () => { state.dispatching = true; } const client = createClient({ - "workboard.cards.list": { cards: [sampleCard], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([sampleCard], ["todo", "done"]), "tasks.list": { tasks: [] }, }); @@ -1472,7 +1585,7 @@ describe("workboard controller", () => { }; } if (method === "workboard.cards.list") { - return { cards: [dispatchedCard], statuses: ["todo", "ready", "done"] }; + return listResult([dispatchedCard], ["todo", "ready", "done"]); } if (method === "tasks.list") { throw new Error("task ledger unavailable"); @@ -1480,7 +1593,7 @@ describe("workboard controller", () => { return {}; }); - await dispatchWorkboard({ host, client: client as never }); + await dispatchBoard(client); expect(state.cards).toEqual([dispatchedCard]); expect(state.loaded).toBe(true); @@ -1498,7 +1611,7 @@ describe("workboard controller", () => { state.busyCardIds.add(sampleCard.id); } const client = createClient({ - "workboard.cards.list": { cards: [sampleCard], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([sampleCard], ["todo", "done"]), }); await expect(loadBoard(client)).resolves.toBe(false); @@ -1514,19 +1627,15 @@ describe("workboard controller", () => { state.editingCardId = sampleCard.id; const client = createClient({}); - await saveWorkboardCardDraft({ host, client: client as never }); - await moveWorkboardCard({ - host, - client: client as never, + await saveDraft(client); + await moveCard(client, { cardId: sampleCard.id, status: "review", position: 2000, }); - await deleteWorkboardCard({ host, client: client as never, cardId: sampleCard.id }); - await archiveWorkboardCard({ host, client: client as never, cardId: sampleCard.id }); - await addWorkboardCardComment({ - host, - client: client as never, + await deleteCard(client, sampleCard.id); + await archiveCard(client, sampleCard.id); + await commentCard(client, { cardId: sampleCard.id, body: "hold", }); @@ -1539,34 +1648,25 @@ describe("workboard controller", () => { const refreshList = createDeferred(); const staleCard = { ...sampleCard, title: "Stale refresh card" }; const dispatchedCard = { ...sampleCard, title: "Dispatched card" }; - let listCalls = 0; - const client = createClient((method) => { - if (method === "workboard.cards.list") { - listCalls += 1; - return listCalls === 1 - ? refreshList.promise - : { cards: [dispatchedCard], statuses: ["todo", "done"] }; - } - if (method === "workboard.cards.dispatch") { - return { + const client = createSequencedClient({ + "workboard.cards.list": [refreshList.promise, listResult([dispatchedCard])], + "workboard.cards.dispatch": [ + { promoted: [], reclaimed: [], blocked: [], orchestrated: [], count: 0, - }; - } - if (method === "tasks.list") { - return { tasks: [] }; - } - return {}; + }, + ], + "tasks.list": [{ tasks: [] }], }); const refresh = refreshBoard(client, "manual"); await Promise.resolve(); expect(getWorkboardState(host).loading).toBe(true); - await dispatchWorkboard({ host, client: client as never }); + await dispatchBoard(client); expect(getWorkboardState(host).cards).toMatchObject([{ title: "Dispatched card" }]); refreshList.resolve({ cards: [staleCard], statuses: ["todo", "done"] }); @@ -1597,9 +1697,7 @@ describe("workboard controller", () => { const refresh = refreshBoard(client, "manual"); await Promise.resolve(); - await moveWorkboardCard({ - host, - client: client as never, + await moveCard(client, { cardId: sampleCard.id, status: "review", position: 2000, @@ -1615,26 +1713,15 @@ describe("workboard controller", () => { const reloadedList = createDeferred(); const movedCard = { ...sampleCard, title: "Moved during initial load" }; const reloadedCard = { ...sampleCard, title: "Reloaded canonical card" }; - let listCalls = 0; - const client = createClient((method) => { - if (method === "workboard.cards.list") { - listCalls += 1; - return listCalls === 1 ? initialList.promise : reloadedList.promise; - } - if (method === "workboard.cards.move") { - return { card: movedCard }; - } - if (method === "tasks.list") { - return { tasks: [] }; - } - return {}; + const client = createSequencedClient({ + "workboard.cards.list": [initialList.promise, reloadedList.promise], + "workboard.cards.move": [{ card: movedCard }], + "tasks.list": [{ tasks: [] }], }); const initialLoad = loadWorkboard({ host, client: client as never }); await Promise.resolve(); - await moveWorkboardCard({ - host, - client: client as never, + await moveCard(client, { cardId: sampleCard.id, status: "review", position: 2000, @@ -1645,7 +1732,7 @@ describe("workboard controller", () => { expect(state.loading).toBe(false); const reload = loadWorkboard({ host, client: client as never }); - expect(listCalls).toBe(2); + expect(requestCalls(client, "workboard.cards.list").length).toBe(2); reloadedList.resolve({ cards: [reloadedCard], statuses: ["todo", "done"] }); await reload; expect(state.cards).toMatchObject([{ title: "Reloaded canonical card" }]); @@ -1674,7 +1761,7 @@ describe("workboard controller", () => { const refresh = loadBoard(client); await Promise.resolve(); - const save = saveWorkboardCardDraft({ host, client: client as never }); + const save = saveDraft(client); await waitForFast(() => { expect(client.request).toHaveBeenCalledWith("workboard.cards.update", expect.anything()); }); @@ -1683,9 +1770,7 @@ describe("workboard controller", () => { expect(state.draftSaving).toBe(true); expect(state.loading).toBe(true); - await addWorkboardCardComment({ - host, - client: client as never, + await commentCard(client, { cardId: sampleCard.id, body: "must wait for save", }); @@ -1700,18 +1785,9 @@ describe("workboard controller", () => { it("queues a forced full refresh behind an in-flight bounded poll load", async () => { const pollList = createDeferred(); const forcedCard = { ...sampleCard, title: "Forced full refresh" }; - let cardListCalls = 0; - const client = createClient((method) => { - if (method === "workboard.cards.list") { - cardListCalls += 1; - return cardListCalls === 1 - ? pollList.promise - : { cards: [forcedCard], statuses: ["todo", "done"] }; - } - if (method === "tasks.list") { - return { tasks: [] }; - } - return {}; + const client = createSequencedClient({ + "workboard.cards.list": [pollList.promise, listResult([forcedCard])], + "tasks.list": [{ tasks: [] }], }); const poll = loadBoard(client, { taskRefresh: "linked" }); @@ -1721,10 +1797,8 @@ describe("workboard controller", () => { await Promise.all([poll, forced]); expect(client.request).toHaveBeenCalledWith("workboard.cards.diagnostics.refresh", {}); - expect( - client.request.mock.calls.filter(([method]) => method === "workboard.cards.list"), - ).toHaveLength(2); - expect(client.request.mock.calls.filter(([method]) => method === "tasks.list")).toHaveLength(1); + expect(requestCalls(client, "workboard.cards.list")).toHaveLength(2); + expect(requestCalls(client, "tasks.list")).toHaveLength(1); expect(getWorkboardState(host).cards).toMatchObject([{ title: "Forced full refresh" }]); }); @@ -1732,22 +1806,13 @@ describe("workboard controller", () => { const initialList = createDeferred(); const weakerCard = { ...sampleCard, title: "Weaker queued refresh" }; const strongerCard = { ...sampleCard, title: "Stronger queued refresh" }; - let cardListCalls = 0; - const client = createClient((method) => { - if (method === "workboard.cards.list") { - cardListCalls += 1; - if (cardListCalls === 1) { - return initialList.promise; - } - return { - cards: [cardListCalls === 2 ? weakerCard : strongerCard], - statuses: ["todo", "done"], - }; - } - if (method === "tasks.list") { - return { tasks: [] }; - } - return {}; + const client = createSequencedClient({ + "workboard.cards.list": [ + initialList.promise, + listResult([weakerCard]), + listResult([strongerCard]), + ], + "tasks.list": [{ tasks: [] }], }); const initial = loadBoard(client, { taskRefresh: "linked" }); @@ -1758,10 +1823,8 @@ describe("workboard controller", () => { await Promise.all([initial, weaker, stronger]); expect(client.request).toHaveBeenCalledWith("workboard.cards.diagnostics.refresh", {}); - expect( - client.request.mock.calls.filter(([method]) => method === "workboard.cards.list"), - ).toHaveLength(3); - expect(client.request.mock.calls.filter(([method]) => method === "tasks.list")).toHaveLength(1); + expect(requestCalls(client, "workboard.cards.list")).toHaveLength(3); + expect(requestCalls(client, "tasks.list")).toHaveLength(1); expect(getWorkboardState(host).cards).toMatchObject([{ title: "Stronger queued refresh" }]); }); @@ -1782,24 +1845,14 @@ describe("workboard controller", () => { await Promise.all([poll, forced]); expect(client.request).not.toHaveBeenCalledWith("workboard.cards.diagnostics.refresh", {}); - expect( - client.request.mock.calls.filter(([method]) => method === "workboard.cards.list"), - ).toHaveLength(1); + expect(requestCalls(client, "workboard.cards.list")).toHaveLength(1); expect(getWorkboardState(host).loaded).toBe(false); }); it("reloads a previously loaded board after lifecycle teardown", async () => { const reopenedCard = { ...sampleCard, title: "Reopened board" }; - let listCalls = 0; - const client = createClient((method) => { - if (method === "workboard.cards.list") { - listCalls += 1; - return { - cards: [listCalls === 1 ? sampleCard : reopenedCard], - statuses: ["todo", "done"], - }; - } - return {}; + const client = createSequencedClient({ + "workboard.cards.list": [listResult(), listResult([reopenedCard])], }); await loadWorkboard({ host, client: client as never }); @@ -1813,7 +1866,7 @@ describe("workboard controller", () => { expect(state.loadAttempted).toBe(false); expect(state.mutationReadiness).toBe("canonical_reload_required"); await expect(loadWorkboard({ host, client: client as never })).resolves.toBe(true); - expect(listCalls).toBe(2); + expect(requestCalls(client, "workboard.cards.list").length).toBe(2); expect(state.cards).toEqual([reopenedCard]); expect(state.mutationReadiness).toBe("ready"); }); @@ -1849,7 +1902,7 @@ describe("workboard controller", () => { const createHost = {}; const createState = getWorkboardState(createHost); const createClientInstance = createClient({ - "workboard.cards.list": { cards: [], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([], ["todo", "done"]), }); createState.loaded = true; createState.draftOpen = true; @@ -1874,7 +1927,7 @@ describe("workboard controller", () => { return saveResponse; } if (method === "workboard.cards.list") { - return { cards: [sampleCard], statuses: ["todo", "done"] }; + return listResult([sampleCard], ["todo", "done"]); } if (method === "tasks.list") { return { tasks: [] }; @@ -1886,7 +1939,7 @@ describe("workboard controller", () => { state.editingCardId = sampleCard.id; state.draftTitle = "Unsaved edit"; - const save = saveWorkboardCardDraft({ host, client: client as never }); + const save = saveDraft(client); await waitForFast(() => { expect(client.request).toHaveBeenCalledWith("workboard.cards.update", expect.anything()); }); @@ -1909,7 +1962,7 @@ describe("workboard controller", () => { return dispatchResult.promise; } if (method === "workboard.cards.list") { - return { cards: [sampleCard], statuses: ["todo", "done"] }; + return listResult([sampleCard], ["todo", "done"]); } if (method === "tasks.list") { return { tasks: [] }; @@ -1918,7 +1971,7 @@ describe("workboard controller", () => { }); setLoadedCard(sampleCard); - const dispatch = dispatchWorkboard({ host, client: client as never }); + const dispatch = dispatchBoard(client); await waitForFast(() => { expect(client.request).toHaveBeenCalledWith("workboard.cards.dispatch", {}); }); @@ -1938,13 +1991,8 @@ describe("workboard controller", () => { const staleList = createDeferred(); const reopenedList = createDeferred(); const reopenedCard = { ...sampleCard, title: "Reopened board" }; - let cardListCalls = 0; - const client = createClient((method) => { - if (method === "workboard.cards.list") { - cardListCalls += 1; - return cardListCalls === 1 ? staleList.promise : reopenedList.promise; - } - return {}; + const client = createSequencedClient({ + "workboard.cards.list": [staleList.promise, reopenedList.promise], }); const initial = loadBoard(client, { taskRefresh: "linked" }); @@ -1959,24 +2007,15 @@ describe("workboard controller", () => { await Promise.all([forced, reopened]); expect(client.request).not.toHaveBeenCalledWith("workboard.cards.diagnostics.refresh", {}); - expect( - client.request.mock.calls.filter(([method]) => method === "workboard.cards.list"), - ).toHaveLength(2); + expect(requestCalls(client, "workboard.cards.list")).toHaveLength(2); expect(getWorkboardState(host).cards).toMatchObject([{ title: "Reopened board" }]); }); it("detaches a stalled initial load during lifecycle teardown", async () => { const initialList = createDeferred(); const reopenedCard = { ...sampleCard, title: "Reopened board" }; - let listCalls = 0; - const client = createClient((method) => { - if (method === "workboard.cards.list") { - listCalls += 1; - return listCalls === 1 - ? initialList.promise - : { cards: [reopenedCard], statuses: ["todo", "done"] }; - } - return {}; + const client = createSequencedClient({ + "workboard.cards.list": [initialList.promise, listResult([reopenedCard])], }); const initialLoad = loadWorkboard({ host, client: client as never }); @@ -1989,7 +2028,7 @@ describe("workboard controller", () => { expect(state.loading).toBe(false); expect(state.loadAttempted).toBe(false); await expect(loadWorkboard({ host, client: client as never })).resolves.toBe(true); - expect(listCalls).toBe(2); + expect(requestCalls(client, "workboard.cards.list").length).toBe(2); expect(state.cards).toMatchObject([{ title: "Reopened board" }]); initialList.resolve({ cards: [sampleCard], statuses: ["todo", "done"] }); @@ -2014,32 +2053,17 @@ describe("workboard controller", () => { await Promise.all([poll, forced]); expect(client.request).not.toHaveBeenCalledWith("workboard.cards.diagnostics.refresh", {}); - expect( - client.request.mock.calls.filter(([method]) => method === "workboard.cards.list"), - ).toHaveLength(1); + expect(requestCalls(client, "workboard.cards.list")).toHaveLength(1); }); it("does not mark a load successful when task enrichment is invalidated by a write", async () => { const taskList = createDeferred(); const movedCard = { ...sampleCard, title: "Moved during task enrichment" }; const reloadedCard = { ...sampleCard, title: "Reloaded after task invalidation" }; - let listCalls = 0; - let taskCalls = 0; - const client = createClient((method) => { - if (method === "workboard.cards.list") { - listCalls += 1; - return listCalls === 1 - ? { cards: [sampleCard], statuses: ["todo", "done"] } - : { cards: [reloadedCard], statuses: ["todo", "done"] }; - } - if (method === "tasks.list") { - taskCalls += 1; - return taskCalls === 1 ? taskList.promise : { tasks: [] }; - } - if (method === "workboard.cards.move") { - return { card: movedCard }; - } - return {}; + const client = createSequencedClient({ + "workboard.cards.list": [listResult(), listResult([reloadedCard])], + "tasks.list": [taskList.promise, { tasks: [] }], + "workboard.cards.move": [{ card: movedCard }], }); const initialLoad = loadWorkboard({ host, client: client as never }); @@ -2047,9 +2071,7 @@ describe("workboard controller", () => { await Promise.resolve(); expect(client.request).toHaveBeenCalledWith("tasks.list", { limit: 500 }); - await moveWorkboardCard({ - host, - client: client as never, + await moveCard(client, { cardId: sampleCard.id, status: "review", position: 2000, @@ -2073,7 +2095,7 @@ describe("workboard controller", () => { } satisfies WorkboardCard; const client = createClient((method, params) => { if (method === "workboard.cards.list") { - return { cards: [linked], statuses: ["todo", "done"] }; + return listResult([linked], ["todo", "done"]); } if (method === "tasks.list" && (params as { cursor?: string }).cursor === "page-2") { return { tasks: [sampleTask] }; @@ -2135,12 +2157,10 @@ describe("workboard controller", () => { }); it("summarizes health from card metadata, linked tasks, and sessions", () => { - const running = { - ...sampleCard, + const running = createSessionCard({ id: "running", status: "running", - sessionKey: sampleSession.key, - } satisfies WorkboardCard; + }); const blocked = { ...sampleCard, id: "blocked", status: "blocked" } satisfies WorkboardCard; const ready = { ...sampleCard, id: "ready", status: "ready" } satisfies WorkboardCard; const missingProof = { ...sampleCard, id: "done", status: "done" } satisfies WorkboardCard; @@ -2382,7 +2402,7 @@ describe("workboard controller", () => { runId: "run-1", } satisfies WorkboardCard; const client = createClient({ - "workboard.cards.list": { cards: [linked], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([linked], ["todo", "done"]), "tasks.list": { tasks: [ { @@ -2406,7 +2426,7 @@ describe("workboard controller", () => { runId: "run-1", } satisfies WorkboardCard; const client = createClient({ - "workboard.cards.list": { cards: [linked], statuses: ["todo", "done"] }, + "workboard.cards.list": listResult([linked], ["todo", "done"]), "tasks.list": { tasks: [ { @@ -2533,19 +2553,17 @@ describe("workboard controller", () => { state.draftLabels = "ui, polish"; state.draftAgentId = "dev"; state.draftSessionKey = sampleSession.key; - const updated = { - ...sampleCard, + const updated = createSessionCard({ title: "Updated board", notes: "New notes", status: "review", priority: "high", labels: ["ui", "polish"], agentId: "dev", - sessionKey: sampleSession.key, - }; + }); const client = createClient({ "workboard.cards.update": { card: updated } }); - await saveWorkboardCardDraft({ host, client: client as never }); + await saveDraft(client); expect(client.request).toHaveBeenCalledWith("workboard.cards.update", { id: "card-1", @@ -2576,7 +2594,7 @@ describe("workboard controller", () => { }; const client = createClient({ "workboard.cards.create": { card: created } }); - await saveWorkboardCardDraft({ host, client: client as never }); + await saveDraft(client); expect(client.request).toHaveBeenCalledWith("workboard.cards.create", { title: "Write tests", @@ -2604,7 +2622,7 @@ describe("workboard controller", () => { } satisfies WorkboardCard; const client = createClient({ "workboard.cards.create": { card: created } }); - await saveWorkboardCardDraft({ host, client: client as never }); + await saveDraft(client); expect(client.request).toHaveBeenCalledWith("workboard.cards.create", { title: "Investigate operations alert", @@ -2633,7 +2651,7 @@ describe("workboard controller", () => { } satisfies WorkboardCard; const client = createClient({ "workboard.cards.create": { card: created } }); - await saveWorkboardCardDraft({ host, client: client as never }); + await saveDraft(client); expect(client.request).toHaveBeenCalledWith( "workboard.cards.create", @@ -2682,7 +2700,7 @@ describe("workboard controller", () => { return {}; }); - await saveWorkboardCardDraft({ host, client: client as never }); + await saveDraft(client); await syncLifecycle(client, [ { ...sampleSession, hasActiveRun: false, status: "done", updatedAt: 1 }, ]); @@ -2848,7 +2866,7 @@ describe("workboard controller", () => { return {}; }); - const saving = saveWorkboardCardDraft({ host, client: client as never }); + const saving = saveDraft(client); await Promise.resolve(); await syncLifecycle(client, [ { ...sampleSession, hasActiveRun: false, status: "done", updatedAt: 1 }, @@ -2872,9 +2890,7 @@ describe("workboard controller", () => { } satisfies WorkboardCard; const client = createClient({ "workboard.cards.comment": { card: updated } }); - await addWorkboardCardComment({ - host, - client: client as never, + await commentCard(client, { cardId: sampleCard.id, body: state.detailCommentBody, }); @@ -2895,15 +2911,13 @@ describe("workboard controller", () => { status: "done", hasActiveRun: false, } as const; - const created = { - ...sampleCard, + const created = createSessionCard({ title: "Fix login", status: "review", - sessionKey: sampleSession.key, - } as const; + }); const client = createClient((method) => { if (method === "workboard.cards.list") { - return { cards: [], statuses: ["todo", "running", "review"] }; + return listResult([], ["todo", "running", "review"]); } if (method === "chat.history") { return { @@ -2992,17 +3006,13 @@ describe("workboard controller", () => { }); it("reuses an active captured session before an older archived match", async () => { - const archived = { - ...sampleCard, + const archived = createSessionCard({ id: "archived-session-card", - sessionKey: sampleSession.key, metadata: { archivedAt: 10 }, - } satisfies WorkboardCard; - const active = { - ...sampleCard, + }); + const active = createSessionCard({ id: "active-session-card", - sessionKey: sampleSession.key, - } satisfies WorkboardCard; + }); state.loaded = true; state.cards = [archived, active]; const client = createClient({}); @@ -3019,28 +3029,22 @@ describe("workboard controller", () => { inFlight: true, }, ])("$name", async ({ inFlight }) => { - const archived = { - ...sampleCard, + const archived = createSessionCard({ id: "archived-newest-session-card", - sessionKey: sampleSession.key, position: 0, updatedAt: 30, metadata: { archivedAt: 40 }, - } satisfies WorkboardCard; - const older = { - ...sampleCard, + }); + const older = createSessionCard({ id: "older-active-session-card", - sessionKey: sampleSession.key, position: 1000, updatedAt: 10, - } satisfies WorkboardCard; - const newest = { - ...sampleCard, + }); + const newest = createSessionCard({ id: "newest-active-session-card", - sessionKey: sampleSession.key, position: 2000, updatedAt: 20, - } satisfies WorkboardCard; + }); state.loaded = true; state.cards = [archived, older, newest]; if (inFlight) { @@ -3054,17 +3058,13 @@ describe("workboard controller", () => { }); it("returns the active captured session while a duplicate capture is in flight", async () => { - const archived = { - ...sampleCard, + const archived = createSessionCard({ id: "archived-inflight-session-card", - sessionKey: sampleSession.key, metadata: { archivedAt: 10 }, - } satisfies WorkboardCard; - const active = { - ...sampleCard, + }); + const active = createSessionCard({ id: "active-inflight-session-card", - sessionKey: sampleSession.key, - } satisfies WorkboardCard; + }); state.loaded = true; state.cards = [archived, active]; state.capturingSessionKeys.add(sampleSession.key); @@ -3076,11 +3076,9 @@ describe("workboard controller", () => { }); it("restores archived captured sessions instead of leaving them hidden", async () => { - const archived = { - ...sampleCard, - sessionKey: sampleSession.key, + const archived = createSessionCard({ metadata: { archivedAt: 10 }, - } satisfies WorkboardCard; + }); const restored = { ...archived, metadata: {}, @@ -3181,9 +3179,7 @@ describe("workboard controller", () => { ); }); - expect( - client.request.mock.calls.filter(([method]) => method === "workboard.cards.create"), - ).toHaveLength(1); + expect(requestCalls(client, "workboard.cards.create")).toHaveLength(1); create.resolve({ card: created }); const captures = await Promise.all([firstCapture, secondCapture]); @@ -3246,10 +3242,7 @@ describe("workboard controller", () => { }); it("waits for retained lifecycle writes before capturing after teardown", async () => { - const lifecycleCard = { - ...sampleCard, - sessionKey: sampleSession.key, - } satisfies WorkboardCard; + const lifecycleCard = createSessionCard(); const capturedSession = { ...sampleSession, key: "agent:main:dashboard:capture", @@ -3311,7 +3304,7 @@ describe("workboard controller", () => { const textPrefix = "y".repeat(696); const client = createClient((method) => { if (method === "workboard.cards.list") { - return { cards: [], statuses: ["todo"] }; + return listResult([], ["todo"]); } if (method === "chat.history") { return { @@ -3339,24 +3332,14 @@ describe("workboard controller", () => { }); it("starts a task run and links it back to the card", async () => { - const running = { - ...sampleCard, - status: "running", - sessionKey: sampleTaskSessionKey, - runId: "run-1", - taskId: "task-1", - }; + const running = createLinkedCard(); const client = createClient({ agent: { sessionKey: sampleTaskSessionKey, runId: "run-1" }, "tasks.list": { tasks: [sampleTask] }, "workboard.cards.update": { card: running }, }); - const sessionKey = await startWorkboardCard({ - host, - client: client as never, - card: sampleCard, - }); + const sessionKey = await startSampleCard(client); expect(sessionKey).toBe(sampleTaskSessionKey); expect(client.request).toHaveBeenNthCalledWith( @@ -3402,9 +3385,7 @@ describe("workboard controller", () => { "workboard.cards.update": { card: { ...sampleCard, title, status: "running" } }, }); - await startWorkboardCard({ - host, - client: client as never, + await startCard(client, { card: { ...sampleCard, title }, }); @@ -3439,9 +3420,7 @@ describe("workboard controller", () => { "workboard.cards.update": { card: running }, }); - const sessionKey = await startWorkboardCard({ - host, - client: client as never, + const sessionKey = await startCard(client, { card: staleLinked, }); @@ -3473,9 +3452,7 @@ describe("workboard controller", () => { "workboard.cards.update": { card: running }, }); - const sessionKey = await startWorkboardCard({ - host, - client: client as never, + const sessionKey = await startCard(client, { card: mixedCase, }); @@ -3489,35 +3466,21 @@ describe("workboard controller", () => { it("waits briefly for task ledger registration after a started run", async () => { vi.useFakeTimers(); - const running = { - ...sampleCard, - status: "running", - sessionKey: sampleTaskSessionKey, - runId: "run-1", - taskId: "task-1", - }; - let taskLists = 0; - const client = createClient((method) => { - if (method === "agent") { - return { sessionKey: sampleTaskSessionKey, runId: "run-1" }; - } - if (method === "tasks.list") { - taskLists += 1; - return { tasks: taskLists >= 3 ? [sampleTask] : [] }; - } - return { card: running }; - }); + const running = createLinkedCard(); + const client = createSequencedClient( + { + agent: [{ sessionKey: sampleTaskSessionKey, runId: "run-1" }], + "tasks.list": [{ tasks: [] }, { tasks: [] }, { tasks: [sampleTask] }], + }, + { card: running }, + ); - const started = startWorkboardCard({ - host, - client: client as never, - card: sampleCard, - }); + const started = startSampleCard(client); await vi.advanceTimersByTimeAsync(350); const sessionKey = await started; expect(sessionKey).toBe(sampleTaskSessionKey); - expect(taskLists).toBe(3); + expect(requestCalls(client, "tasks.list").length).toBe(3); expect(client.request).toHaveBeenLastCalledWith( "workboard.cards.update", expect.objectContaining({ @@ -3544,11 +3507,7 @@ describe("workboard controller", () => { return { card: running }; }); - const started = startWorkboardCard({ - host, - client: client as never, - card: sampleCard, - }); + const started = startSampleCard(client); await vi.advanceTimersByTimeAsync(1000); const sessionKey = await started; @@ -3598,9 +3557,7 @@ describe("workboard controller", () => { await loadBoard(client); client.request.mockClear(); - const sessionKey = await startWorkboardCard({ - host, - client: client as never, + const sessionKey = await startCard(client, { card: child, }); @@ -3618,18 +3575,16 @@ describe("workboard controller", () => { }); it("does not create a session when the gateway rejects start preflight", async () => { - const client = createClient((method) => { - if (method === "workboard.cards.update") { - throw new Error("Parent cards must be done before starting this card."); - } - return { key: "agent:main:dashboard:1" }; - }); + const client = createSequencedClient( + { + "workboard.cards.update": [ + new Error("Parent cards must be done before starting this card."), + ], + }, + { key: "agent:main:dashboard:1" }, + ); - const sessionKey = await startWorkboardCard({ - host, - client: client as never, - card: sampleCard, - }); + const sessionKey = await startSampleCard(client); expect(sessionKey).toBeNull(); expect(client.request).toHaveBeenCalledTimes(1); @@ -3644,23 +3599,12 @@ describe("workboard controller", () => { it("rolls back the running preflight when task run creation fails", async () => { const running = { ...sampleCard, status: "running" } satisfies WorkboardCard; - let updateCalls = 0; - const client = createClient((method) => { - if (method === "workboard.cards.update") { - updateCalls += 1; - return { card: updateCalls === 1 ? running : sampleCard }; - } - if (method === "agent") { - throw new Error("gateway disconnected"); - } - return {}; + const client = createSequencedClient({ + "workboard.cards.update": [{ card: running }, { card: sampleCard }], + agent: [new Error("gateway disconnected")], }); - const sessionKey = await startWorkboardCard({ - host, - client: client as never, - card: sampleCard, - }); + const sessionKey = await startSampleCard(client); expect(sessionKey).toBeNull(); expect(client.request).toHaveBeenNthCalledWith( @@ -3685,35 +3629,18 @@ describe("workboard controller", () => { it("rolls back the running preflight when final session link update fails", async () => { const running = { ...sampleCard, status: "running" } satisfies WorkboardCard; - let updateCalls = 0; - const client = createClient((method) => { - if (method === "workboard.cards.update") { - updateCalls += 1; - if (updateCalls === 1) { - return { card: running }; - } - if (updateCalls === 2) { - throw new Error("write conflict"); - } - return { card: sampleCard }; - } - if (method === "agent") { - return { sessionKey: sampleTaskSessionKey, runId: "run-1" }; - } - if (method === "tasks.list") { - return { tasks: [sampleTask] }; - } - if (method === "chat.abort") { - return { aborted: true, runIds: ["run-1"] }; - } - return {}; + const client = createSequencedClient({ + "workboard.cards.update": [ + { card: running }, + new Error("write conflict"), + { card: sampleCard }, + ], + agent: [{ sessionKey: sampleTaskSessionKey, runId: "run-1" }], + "tasks.list": [{ tasks: [sampleTask] }], + "chat.abort": [{ aborted: true, runIds: ["run-1"] }], }); - const sessionKey = await startWorkboardCard({ - host, - client: client as never, - card: sampleCard, - }); + const sessionKey = await startSampleCard(client); expect(sessionKey).toBeNull(); expect(client.request).toHaveBeenNthCalledWith(5, "chat.abort", { @@ -3743,14 +3670,12 @@ describe("workboard controller", () => { metadata: { automation: { scheduledAt: Date.now() + 60_000 } }, } satisfies WorkboardCard; const client = createClient({ - "workboard.cards.list": { cards: [scheduled], statuses: ["scheduled", "running", "done"] }, + "workboard.cards.list": listResult([scheduled], ["scheduled", "running", "done"]), }); await loadBoard(client); client.request.mockClear(); - const sessionKey = await startWorkboardCard({ - host, - client: client as never, + const sessionKey = await startCard(client, { card: scheduled, }); @@ -3782,9 +3707,7 @@ describe("workboard controller", () => { "sessions.create": { key: "agent:main:dashboard:manual" }, "workboard.cards.update": { card: manualLinked }, }); - const manualSessionKey = await startWorkboardCard({ - host, - client: manualClient as never, + const manualSessionKey = await startCard(manualClient, { card: manualScheduled, mode: "manual", }); @@ -3815,9 +3738,7 @@ describe("workboard controller", () => { card: { ...readyWithSchedule, sessionKey: "agent:main:dashboard:ready-manual" }, }, }); - await startWorkboardCard({ - host, - client: readyManualClient as never, + await startCard(readyManualClient, { card: readyWithSchedule, mode: "manual", }); @@ -3844,7 +3765,7 @@ describe("workboard controller", () => { } satisfies WorkboardCard; const dueClient = createClient((method) => { if (method === "workboard.cards.list") { - return { cards: [dueScheduled], statuses: ["scheduled", "running", "done"] }; + return listResult([dueScheduled], ["scheduled", "running", "done"]); } if (method === "agent") { return { @@ -3873,11 +3794,7 @@ describe("workboard controller", () => { await loadBoard(dueClient); dueClient.request.mockClear(); - const dueSessionKey = await startWorkboardCard({ - host, - client: dueClient as never, - card: dueScheduled, - }); + const dueSessionKey = await startCard(dueClient, { card: dueScheduled }); expect(dueSessionKey).toBe("subagent:workboard-default-scheduled-3"); expect(dueClient.request).toHaveBeenCalledWith( @@ -3902,25 +3819,13 @@ describe("workboard controller", () => { updatedAt: 10, }), }); - let updateCalls = 0; - const client = createClient((method) => { - if (method === "workboard.cards.update") { - updateCalls += 1; - return { card: updateCalls === 1 ? { ...sampleCard, status: "running" } : running }; - } - if (method === "agent") { - return { sessionKey: sampleTaskSessionKey, runId: "run-1" }; - } - if (method === "tasks.list") { - return { tasks: [sampleTask] }; - } - return {}; + const client = createSequencedClient({ + "workboard.cards.update": [{ card: { ...sampleCard, status: "running" } }, { card: running }], + agent: [{ sessionKey: sampleTaskSessionKey, runId: "run-1" }], + "tasks.list": [{ tasks: [sampleTask] }], }); - await startWorkboardCard({ - host, - client: client as never, - card: sampleCard, + await startSampleCard(client, { engine: "codex", }); @@ -3994,9 +3899,7 @@ describe("workboard controller", () => { "workboard.cards.update": { card: previous }, }); - await startWorkboardCard({ - host, - client: client as never, + await startCard(client, { card: previous, engine: "codex", }); @@ -4036,10 +3939,7 @@ describe("workboard controller", () => { "workboard.cards.update": { card: running }, }); - const sessionKey = await startWorkboardCard({ - host, - client: client as never, - card: sampleCard, + const sessionKey = await startSampleCard(client, { engine: "claude", mode: "manual", }); @@ -4105,9 +4005,7 @@ describe("workboard controller", () => { }); getWorkboardState(host).tasksByCardId.set("card-1", sampleTask); - await startWorkboardCard({ - host, - client: client as never, + await startCard(client, { card: staleLinkedCard, engine: "claude", mode: "manual", @@ -4129,27 +4027,21 @@ describe("workboard controller", () => { }); it("rolls back when the Gateway does not return a task run id", async () => { - let updateCalls = 0; - const client = createClient((method) => { - if (method === "agent") { - return { + const client = createSequencedClient({ + agent: [ + { sessionKey: sampleTaskSessionKey, runStarted: false, runError: { message: "provider unavailable" }, - }; - } - if (method === "workboard.cards.update") { - updateCalls += 1; - return { card: updateCalls === 1 ? { ...sampleCard, status: "running" } : sampleCard }; - } - return {}; + }, + ], + "workboard.cards.update": [ + { card: { ...sampleCard, status: "running" } }, + { card: sampleCard }, + ], }); - const sessionKey = await startWorkboardCard({ - host, - client: client as never, - card: sampleCard, - }); + const sessionKey = await startSampleCard(client); expect(sessionKey).toBeNull(); expect(client.request).toHaveBeenNthCalledWith(2, "agent", expect.any(Object)); @@ -4165,9 +4057,7 @@ describe("workboard controller", () => { const moved = { ...sampleCard, status: "blocked", position: 2000 }; const client = createClient({ "workboard.cards.move": { card: moved } }); - await moveWorkboardCard({ - host, - client: client as never, + await moveCard(client, { cardId: "card-1", status: "blocked", position: 2000, @@ -4210,9 +4100,7 @@ describe("workboard controller", () => { return {}; }); - await moveWorkboardCard({ - host, - client: client as never, + await moveCard(client, { cardId: "card-1", status: "running", position: 2000, @@ -4265,9 +4153,7 @@ describe("workboard controller", () => { return {}; }); - const moving = moveWorkboardCard({ - host, - client: client as never, + const moving = moveCard(client, { cardId: "card-1", status: "running", position: 2000, @@ -4322,9 +4208,7 @@ describe("workboard controller", () => { { ...sampleSession, hasActiveRun: false, status: "done", updatedAt: 1 }, ]); await Promise.resolve(); - await moveWorkboardCard({ - host, - client: client as never, + await moveCard(client, { cardId: "card-1", status: "running", position: 2000, @@ -4372,9 +4256,7 @@ describe("workboard controller", () => { { ...sampleSession, hasActiveRun: false, status: "done", updatedAt: 1 }, ]); await Promise.resolve(); - await addWorkboardCardComment({ - host, - client: client as never, + await commentCard(client, { cardId: linked.id, body: "Keep this", }); @@ -4430,9 +4312,7 @@ describe("workboard controller", () => { { ...sampleSession, hasActiveRun: false, status: "done", updatedAt: null }, ]); await Promise.resolve(); - await moveWorkboardCard({ - host, - client: client as never, + await moveCard(client, { cardId: "card-1", status: "running", position: 2000, @@ -4453,11 +4333,9 @@ describe("workboard controller", () => { }); it("keeps non-status edits following newer linked session lifecycle sync", async () => { - const edited = { - ...sampleCard, + const edited = createSessionCard({ title: "Renamed only", status: "running", - sessionKey: sampleSession.key, updatedAt: 5, events: [ { @@ -4469,7 +4347,7 @@ describe("workboard controller", () => { }, { id: "edit-1", kind: "edited", at: 5 }, ], - } satisfies WorkboardCard; + }); setLoadedCard(edited); const client = createClient({ "workboard.cards.update": { @@ -4489,10 +4367,8 @@ describe("workboard controller", () => { }); it("keeps lifecycle-created moves following newer linked session lifecycle sync", async () => { - const lifecycleMoved = { - ...sampleCard, + const lifecycleMoved = createSessionCard({ status: "running", - sessionKey: sampleSession.key, updatedAt: 5, metadata: { lifecycleStatusSourceUpdatedAt: 1 }, events: [ @@ -4504,7 +4380,7 @@ describe("workboard controller", () => { toStatus: "running", }, ], - } satisfies WorkboardCard; + }); setLoadedCard(lifecycleMoved); const client = createClient({ "workboard.cards.update": { @@ -4560,20 +4436,14 @@ describe("workboard controller", () => { }); getWorkboardState(host).cards = [parent, child]; - await deleteWorkboardCard({ - host, - client: client as never, - cardId: parent.id, - }); + await deleteCard(client, parent.id); const remaining = expectDefined(getWorkboardState(host).cards[0], "remaining child card"); expect(remaining).toMatchObject({ id: child.id }); expect(remaining.metadata?.links).toBeUndefined(); client.request.mockClear(); - await startWorkboardCard({ - host, - client: client as never, + await startCard(client, { card: remaining, }); @@ -4711,14 +4581,12 @@ describe("workboard controller", () => { it("does not sync stale linked-session status over a card creation status", async () => { state.loaded = true; state.cards = [ - { - ...sampleCard, + createSessionCard({ status: "running", - sessionKey: sampleSession.key, createdAt: 2000, updatedAt: 2000, events: [{ id: "event-created", kind: "created", at: 2000, toStatus: "running" }], - }, + }), ]; const client = createClient({ "workboard.cards.update": { @@ -4853,9 +4721,7 @@ describe("workboard controller", () => { await expect(loadWorkboard({ host, client: client as never })).resolves.toBe(true); expect(client.request).toHaveBeenCalledWith("workboard.cards.list", {}); - expect( - client.request.mock.calls.filter(([method]) => method === "workboard.cards.update"), - ).toHaveLength(1); + expect(requestCalls(client, "workboard.cards.update")).toHaveLength(1); }); it("reuses an in-flight lifecycle task refresh across render-driven syncs", async () => { @@ -4875,12 +4741,12 @@ describe("workboard controller", () => { const second = syncLifecycle(client); await Promise.resolve(); - expect(client.request.mock.calls.filter(([method]) => method === "tasks.list")).toHaveLength(1); + expect(requestCalls(client, "tasks.list")).toHaveLength(1); taskList.resolve({ tasks: [sampleTask] }); await Promise.all([first, second]); - expect(client.request.mock.calls.filter(([method]) => method === "tasks.list")).toHaveLength(1); + expect(requestCalls(client, "tasks.list")).toHaveLength(1); expect(state.lifecycleTasksPrepared).toBe(true); }); @@ -4893,19 +4759,10 @@ describe("workboard controller", () => { } satisfies WorkboardCard; const completedTask = { ...sampleTask, status: "completed" as const, updatedAt: 3 }; const firstTaskList = createDeferred(); - let taskListCalls = 0; - const client = createClient((method) => { - if (method === "tasks.list") { - taskListCalls += 1; - return taskListCalls === 1 ? firstTaskList.promise : { tasks: [completedTask] }; - } - if (method === "workboard.cards.comment") { - return { card: commented }; - } - if (method === "workboard.cards.update") { - return { card: { ...commented, status: "review", updatedAt: 4 } }; - } - return {}; + const client = createSequencedClient({ + "tasks.list": [firstTaskList.promise, { tasks: [completedTask] }], + "workboard.cards.comment": [{ card: commented }], + "workboard.cards.update": [{ card: { ...commented, status: "review", updatedAt: 4 } }], }); const requestUpdate = vi.fn(); @@ -4913,9 +4770,7 @@ describe("workboard controller", () => { await waitForFast(() => { expect(client.request).toHaveBeenCalledWith("tasks.list", { limit: 500 }); }); - await addWorkboardCardComment({ - host, - client: client as never, + await commentCard(client, { cardId: linked.id, body: "Keep this", requestUpdate, @@ -4961,12 +4816,10 @@ describe("workboard controller", () => { }); it("reconciles session-only cards when task discovery is unavailable", async () => { - const linked = { - ...sampleCard, + const linked = createSessionCard({ status: "running", - sessionKey: sampleSession.key, runId: "run-1", - } satisfies WorkboardCard; + }); setLoadedCard(linked); const client = createClient((method) => { if (method === "tasks.list") { @@ -4989,12 +4842,10 @@ describe("workboard controller", () => { }); it("honors task refresh backoff while reconciling session-only cards", async () => { - const linked = { - ...sampleCard, + const linked = createSessionCard({ status: "running", - sessionKey: sampleSession.key, runId: "run-1", - } satisfies WorkboardCard; + }); setLoadedCard(linked); state.lifecycleTaskRefreshFailed = true; state.lifecycleTaskRefreshRetryAt = Date.now() + 5000; @@ -5064,29 +4915,15 @@ describe("workboard controller", () => { it("rotates bounded exact confirmations before lifecycle writes", async () => { vi.useFakeTimers(); - const cards = Array.from({ length: 65 }, (_, index) => ({ - ...sampleCard, - id: `card-${index}`, - status: "running" as const, - taskId: `task-${index}`, - })); + const cards = createConfirmationCards(65); state.loaded = true; state.cards = cards; - const client = createClient((method, params) => { - if (method === "tasks.list") { - return { tasks: [] }; - } - if (method === "tasks.get") { - const taskId = (params as { taskId: string }).taskId; - return { task: { ...sampleTask, id: taskId, taskId } }; - } - return {}; - }); + const client = createConfirmationClient(); const requestUpdate = vi.fn(); await syncLifecycle(client, [], { requestUpdate }); - expect(client.request.mock.calls.filter(([method]) => method === "tasks.get")).toHaveLength(32); + expect(requestCalls(client, "tasks.get")).toHaveLength(32); expect(client.request).not.toHaveBeenCalledWith("workboard.cards.update", expect.anything()); expect(state.lifecycleTasksPrepared).toBe(false); @@ -5096,7 +4933,7 @@ describe("workboard controller", () => { vi.clearAllMocks(); await syncLifecycle(client, [], { requestUpdate }); - expect(client.request.mock.calls.filter(([method]) => method === "tasks.get")).toHaveLength(32); + expect(requestCalls(client, "tasks.get")).toHaveLength(32); expect(client.request).not.toHaveBeenCalledWith("workboard.cards.update", expect.anything()); expect(state.lifecycleTasksPrepared).toBe(false); @@ -5104,36 +4941,22 @@ describe("workboard controller", () => { await vi.advanceTimersByTimeAsync(100); await syncLifecycle(client, [], { requestUpdate }); - expect(client.request.mock.calls.filter(([method]) => method === "tasks.get")).toHaveLength(1); + expect(requestCalls(client, "tasks.get")).toHaveLength(1); expect(client.request).not.toHaveBeenCalledWith("workboard.cards.update", expect.anything()); expect(state.lifecycleTasksPrepared).toBe(true); }); it("fails closed when bounded confirmations exceed their freshness window", async () => { vi.useFakeTimers(); - const cards = Array.from({ length: 33 }, (_, index) => ({ - ...sampleCard, - id: `card-${index}`, - status: "running" as const, - taskId: `task-${index}`, - })); + const cards = createConfirmationCards(33); state.loaded = true; state.cards = cards; - const client = createClient((method, params) => { - if (method === "tasks.list") { - return { tasks: [] }; - } - if (method === "tasks.get") { - const taskId = (params as { taskId: string }).taskId; - return { task: { ...sampleTask, id: taskId, taskId } }; - } - return {}; - }); + const client = createConfirmationClient(); const requestUpdate = vi.fn(); await syncLifecycle(client, [], { requestUpdate }); - expect(client.request.mock.calls.filter(([method]) => method === "tasks.get")).toHaveLength(32); + expect(requestCalls(client, "tasks.get")).toHaveLength(32); expect(state.lifecycleTaskRefreshContinueAt).not.toBeNull(); vi.clearAllMocks(); @@ -5153,31 +4976,14 @@ describe("workboard controller", () => { }); it("stops bounded exact confirmations after a transient batch failure", async () => { - const cards = Array.from({ length: 33 }, (_, index) => ({ - ...sampleCard, - id: `card-${index}`, - status: "running" as const, - taskId: `task-${index}`, - })); + const cards = createConfirmationCards(33); state.loaded = true; state.cards = cards; - const client = createClient((method, params) => { - if (method === "tasks.list") { - return { tasks: [] }; - } - if (method === "tasks.get") { - const taskId = (params as { taskId: string }).taskId; - if (taskId === "task-0") { - throw new Error("task confirmation unavailable"); - } - return { task: { ...sampleTask, id: taskId, taskId } }; - } - return {}; - }); + const client = createConfirmationClient("task-0"); await syncLifecycle(client); - expect(client.request.mock.calls.filter(([method]) => method === "tasks.get")).toHaveLength(32); + expect(requestCalls(client, "tasks.get")).toHaveLength(32); expect(client.request).not.toHaveBeenCalledWith("workboard.cards.update", expect.anything()); expect(state.lifecycleTaskRefreshFailed).toBe(true); expect(state.lifecycleTasksPrepared).toBe(false); @@ -5232,13 +5038,7 @@ describe("workboard controller", () => { }); it("defers lifecycle writes when exact confirmation after task listing fails", async () => { - const linked = { - ...sampleCard, - status: "running", - sessionKey: sampleTaskSessionKey, - runId: "run-1", - taskId: sampleTask.taskId, - } satisfies WorkboardCard; + const linked = createLinkedCard(); setLoadedCard(linked, sampleTask); const client = createClient((method) => { if (method === "tasks.list") { @@ -5405,9 +5205,7 @@ describe("workboard controller", () => { const syncing = syncLifecycle(client, []); await Promise.resolve(); - await addWorkboardCardComment({ - host, - client: client as never, + await commentCard(client, { cardId: linked.id, body: "Keep this", }); @@ -5421,13 +5219,11 @@ describe("workboard controller", () => { it("moves stale running sessions into running while recording stale metadata", async () => { const staleUpdatedAt = Date.now() - 31 * 60 * 1000; - const linked = { - ...sampleCard, - sessionKey: sampleSession.key, + const linked = createSessionCard({ metadata: { comments: [{ id: "comment-1", body: "Keep me", createdAt: 1 }], }, - } satisfies WorkboardCard; + }); setLoadedCard(linked); const client = createClient({ "workboard.cards.update": { @@ -5465,19 +5261,11 @@ describe("workboard controller", () => { }); it("syncs stale session metadata and clears it when the session recovers", async () => { - const linked = { - ...sampleCard, - status: "running", - sessionKey: sampleSession.key, + const linked = createStaleSessionCard(1, { metadata: { comments: [{ id: "comment-1", body: "Keep me", createdAt: 1 }], - stale: { - detectedAt: 1, - lastSessionUpdatedAt: 1, - reason: "Linked thread has not reported recent activity.", - }, }, - } satisfies WorkboardCard; + }); setLoadedCard(linked); const client = createClient({ "workboard.cards.update": { @@ -5498,17 +5286,7 @@ describe("workboard controller", () => { }); it("clears stale metadata after a newer manual status move", async () => { - const linked = { - ...sampleCard, - status: "running", - sessionKey: sampleSession.key, - metadata: { - stale: { - detectedAt: 1, - lastSessionUpdatedAt: 1, - reason: "Linked thread has not reported recent activity.", - }, - }, + const linked = createStaleSessionCard(1, { events: [ { id: "move-1", @@ -5518,7 +5296,7 @@ describe("workboard controller", () => { toStatus: "running", }, ], - } satisfies WorkboardCard; + }); setLoadedCard(linked); const client = createClient({ "workboard.cards.update": { @@ -5544,18 +5322,7 @@ describe("workboard controller", () => { it("does not rewrite unchanged stale session metadata", async () => { const staleUpdatedAt = Date.now() - 31 * 60 * 1000; - const linked = { - ...sampleCard, - status: "running", - sessionKey: sampleSession.key, - metadata: { - stale: { - detectedAt: 1, - lastSessionUpdatedAt: staleUpdatedAt, - reason: "Linked thread has not reported recent activity.", - }, - }, - } satisfies WorkboardCard; + const linked = createStaleSessionCard(staleUpdatedAt); setLoadedCard(linked); const client = createClient({ "workboard.cards.update": { card: linked } }); @@ -5598,13 +5365,7 @@ describe("workboard controller", () => { }); it("recovers task refresh failures for read-only workboard clients", async () => { - const linked = { - ...sampleCard, - status: "running", - sessionKey: sampleTaskSessionKey, - runId: sampleTask.runId, - taskId: sampleTask.taskId, - } satisfies WorkboardCard; + const linked = createLinkedCard({ runId: sampleTask.runId }); setLoadedCard(linked); state.lifecycleTaskRefreshFailed = true; state.lifecycleTaskRefreshError = "tasks unavailable"; @@ -5623,12 +5384,10 @@ describe("workboard controller", () => { }); it("resyncs cards manually moved back to an active lifecycle column", async () => { - const linked = { - ...sampleCard, + const linked = createSessionCard({ status: "running", - sessionKey: sampleSession.key, updatedAt: 1000, - } as const; + }); const completedSession = { ...sampleSession, hasActiveRun: false, @@ -5651,12 +5410,10 @@ describe("workboard controller", () => { }); it("does not retry a failed lifecycle task refresh before backoff", async () => { - const linked = { - ...sampleCard, + const linked = createSessionCard({ status: "running", - sessionKey: sampleSession.key, updatedAt: 1000, - } as const; + }); const completedSession = { ...sampleSession, hasActiveRun: false, @@ -5677,10 +5434,8 @@ describe("workboard controller", () => { await syncLifecycle(client, [completedSession]); await syncLifecycle(client, [completedSession]); - expect(client.request.mock.calls.filter(([method]) => method === "tasks.list")).toHaveLength(1); - expect( - client.request.mock.calls.filter(([method]) => method === "workboard.cards.update"), - ).toHaveLength(1); + expect(requestCalls(client, "tasks.list")).toHaveLength(1); + expect(requestCalls(client, "workboard.cards.update")).toHaveLength(1); expect(state.error).toBeNull(); expect(state.lifecycleTaskRefreshError).toBe("tasks unavailable"); expect(state.cards[0]?.status).toBe("review"); @@ -5694,7 +5449,7 @@ describe("workboard controller", () => { "workboard.cards.update": { card: blocked }, }); - await stopWorkboardCard({ host, client: client as never, card: linked }); + await stopCard(client, linked); expect(client.request).toHaveBeenNthCalledWith(1, "chat.abort", { sessionKey: sampleSession.key, @@ -5708,12 +5463,7 @@ describe("workboard controller", () => { }); it("cancels active linked tasks and aborts the running session", async () => { - const linked = { - ...sampleCard, - sessionKey: sampleTaskSessionKey, - runId: "run-1", - taskId: "task-1", - }; + const linked = createLinkedCard({ status: sampleCard.status }); const blocked = { ...linked, status: "blocked" }; state.cards = [linked]; state.tasksByCardId.set("card-1", sampleTask); @@ -5723,7 +5473,7 @@ describe("workboard controller", () => { "workboard.cards.update": { card: blocked }, }); - await stopWorkboardCard({ host, client: client as never, card: linked }); + await stopCard(client, linked); expect(client.request).toHaveBeenNthCalledWith(1, "tasks.cancel", { taskId: "task-1", @@ -5745,29 +5495,17 @@ describe("workboard controller", () => { }); it("marks a cancelled task blocked when follow-up session abort fails", async () => { - const linked = { - ...sampleCard, - sessionKey: sampleTaskSessionKey, - runId: "run-1", - taskId: "task-1", - }; + const linked = createLinkedCard({ status: sampleCard.status }); const blocked = { ...linked, status: "blocked" }; state.cards = [linked]; state.tasksByCardId.set("card-1", sampleTask); - const client = createClient((method) => { - if (method === "tasks.cancel") { - return { cancelled: true }; - } - if (method === "chat.abort") { - throw new Error("run already removed"); - } - if (method === "workboard.cards.update") { - return { card: blocked }; - } - return {}; + const client = createSequencedClient({ + "tasks.cancel": [{ cancelled: true }], + "chat.abort": [new Error("run already removed")], + "workboard.cards.update": [{ card: blocked }], }); - await stopWorkboardCard({ host, client: client as never, card: linked }); + await stopCard(client, linked); expect(client.request).toHaveBeenCalledWith("workboard.cards.update", { id: "card-1", @@ -5784,12 +5522,7 @@ describe("workboard controller", () => { id: "task-replacement", taskId: "task-replacement", }; - const linked = { - ...sampleCard, - sessionKey: sampleTaskSessionKey, - runId: "run-1", - taskId: missingTaskId, - }; + const linked = createLinkedCard({ status: sampleCard.status, taskId: missingTaskId }); const blocked = { ...linked, status: "blocked" }; state.cards = [linked]; state.tasksByCardId.set("card-1", replacementTask); @@ -5800,7 +5533,7 @@ describe("workboard controller", () => { "workboard.cards.update": { card: blocked }, }); - await stopWorkboardCard({ host, client: client as never, card: linked }); + await stopCard(client, linked); expect(client.request).toHaveBeenNthCalledWith(1, "tasks.cancel", { taskId: replacementTask.taskId, @@ -5844,7 +5577,7 @@ describe("workboard controller", () => { method === "tasks.cancel" ? cancel() : { card: blocked }, ); - await stopWorkboardCard({ host, client: client as never, card: linked }); + await stopCard(client, linked); expect(client.request.mock.calls).toEqual([ ["tasks.cancel", { taskId, reason: "Stopped from Workboard." }], @@ -5860,26 +5593,18 @@ describe("workboard controller", () => { }); it("records found:false task cancellation before aborting its linked session", async () => { - const linked = { - ...sampleCard, - status: "running" as const, - sessionKey: sampleTaskSessionKey, - runId: "run-1", - taskId: "task-pruned", - }; + const linked = createLinkedCard({ taskId: "task-pruned" }); const blocked = { ...linked, status: "blocked" as const }; state.cards = [linked]; - const client = createClient((method) => { - if (method === "tasks.cancel") { - return { found: false, cancelled: false }; - } - if (method === "chat.abort") { - return { aborted: true, runIds: ["run-1"] }; - } - return { card: blocked }; - }); + const client = createSequencedClient( + { + "tasks.cancel": [{ found: false, cancelled: false }], + "chat.abort": [{ aborted: true, runIds: ["run-1"] }], + }, + { card: blocked }, + ); - await stopWorkboardCard({ host, client: client as never, card: linked }); + await stopCard(client, linked); expect(client.request).toHaveBeenNthCalledWith(1, "tasks.cancel", { taskId: "task-pruned", @@ -5899,28 +5624,22 @@ describe("workboard controller", () => { }); it("leaves linked cards unchanged when a missing task has no active session to abort", async () => { - const linked = { - ...sampleCard, - status: "running" as const, - sessionKey: sampleTaskSessionKey, - runId: "run-1", - taskId: "task-pruned", - }; + const linked = createLinkedCard({ taskId: "task-pruned" }); state.cards = [linked]; - const client = createClient((method) => { - if (method === "tasks.cancel") { - throw new GatewayRequestError({ - code: "INVALID_REQUEST", - message: "task not found: task-pruned", - }); - } - if (method === "chat.abort") { - return { aborted: false, runIds: [] }; - } - return { card: { ...linked, status: "blocked" } }; - }); + const client = createSequencedClient( + { + "tasks.cancel": [ + new GatewayRequestError({ + code: "INVALID_REQUEST", + message: "task not found: task-pruned", + }), + ], + "chat.abort": [{ aborted: false, runIds: [] }], + }, + { card: { ...linked, status: "blocked" } }, + ); - await stopWorkboardCard({ host, client: client as never, card: linked }); + await stopCard(client, linked); expect(client.request).toHaveBeenCalledTimes(3); expect(client.request).not.toHaveBeenCalledWith("workboard.cards.update", expect.anything()); @@ -5930,28 +5649,19 @@ describe("workboard controller", () => { }); it("reports linked session abort errors after a missing task cancellation", async () => { - const linked = { - ...sampleCard, - status: "running" as const, - sessionKey: sampleTaskSessionKey, - runId: "run-1", - taskId: "task-pruned", - }; + const linked = createLinkedCard({ taskId: "task-pruned" }); state.cards = [linked]; - const client = createClient((method) => { - if (method === "tasks.cancel") { - throw new GatewayRequestError({ + const client = createSequencedClient({ + "tasks.cancel": [ + new GatewayRequestError({ code: "INVALID_REQUEST", message: "task not found: task-pruned", - }); - } - if (method === "chat.abort") { - throw new Error("session abort unavailable"); - } - return { card: { ...linked, status: "blocked" } }; + }), + ], + "chat.abort": [new Error("session abort unavailable")], }); - await stopWorkboardCard({ host, client: client as never, card: linked }); + await stopCard(client, linked); expect(client.request).toHaveBeenCalledTimes(2); expect(client.request).not.toHaveBeenCalledWith("workboard.cards.update", expect.anything()); @@ -5961,26 +5671,14 @@ describe("workboard controller", () => { }); it("reports task cancellation errors without aborting the linked session", async () => { - const linked = { - ...sampleCard, - status: "running" as const, - sessionKey: sampleTaskSessionKey, - runId: "run-1", - taskId: "task-1", - }; + const linked = createLinkedCard(); state.cards = [linked]; state.tasksByCardId.set(linked.id, sampleTask); - const client = createClient((method) => { - if (method === "tasks.cancel") { - throw new Error("task ledger unavailable"); - } - if (method === "chat.abort") { - return { aborted: true, runIds: ["run-1"] }; - } - return { card: { ...linked, status: "blocked" } }; + const client = createSequencedClient({ + "tasks.cancel": [new Error("task ledger unavailable")], }); - await stopWorkboardCard({ host, client: client as never, card: linked }); + await stopCard(client, linked); expect(client.request).toHaveBeenCalledOnce(); expect(client.request).toHaveBeenCalledWith("tasks.cancel", { @@ -5992,12 +5690,7 @@ describe("workboard controller", () => { }); it("marks task-linked cards blocked when task cancellation already stopped the session", async () => { - const linked = { - ...sampleCard, - sessionKey: sampleTaskSessionKey, - runId: "run-1", - taskId: "task-1", - }; + const linked = createLinkedCard({ status: sampleCard.status }); state.cards = [linked]; state.tasksByCardId.set("card-1", sampleTask); const blocked = { ...linked, status: "blocked" as const }; @@ -6007,7 +5700,7 @@ describe("workboard controller", () => { "workboard.cards.update": { card: blocked }, }); - await stopWorkboardCard({ host, client: client as never, card: linked }); + await stopCard(client, linked); expect(client.request).toHaveBeenNthCalledWith(1, "tasks.cancel", { taskId: "task-1", @@ -6040,7 +5733,7 @@ describe("workboard controller", () => { "workboard.cards.update": { card: blocked }, }); - await stopWorkboardCard({ host, client: client as never, card: sampleCard }); + await stopCard(client, sampleCard); expect(client.request).toHaveBeenNthCalledWith(1, "tasks.cancel", { taskId: "task-1", @@ -6063,11 +5756,7 @@ describe("workboard controller", () => { } satisfies WorkboardCard; const client = createClient({ "workboard.cards.archive": { card: archived } }); - await archiveWorkboardCard({ - host, - client: client as never, - cardId: "card-1", - }); + await archiveCard(client, "card-1"); expect(client.request).toHaveBeenCalledWith("workboard.cards.archive", { id: "card-1", @@ -6079,17 +5768,17 @@ describe("workboard controller", () => { it("falls back to the active session abort when the stored run id is stale", async () => { const linked = { ...sampleCard, sessionKey: sampleSession.key, runId: "old-run" }; const blocked = { ...linked, status: "blocked" }; - const client = createClient((method, params) => { - if (method === "chat.abort" && (params as { runId?: string }).runId === "old-run") { - return { aborted: false, runIds: [] }; - } - if (method === "chat.abort") { - return { aborted: true, runIds: ["new-run"] }; - } - return { card: blocked }; - }); + const client = createSequencedClient( + { + "chat.abort": [ + { aborted: false, runIds: [] }, + { aborted: true, runIds: ["new-run"] }, + ], + }, + { card: blocked }, + ); - await stopWorkboardCard({ host, client: client as never, card: linked }); + await stopCard(client, linked); expect(client.request).toHaveBeenNthCalledWith(1, "chat.abort", { sessionKey: sampleSession.key, @@ -6112,7 +5801,7 @@ describe("workboard controller", () => { "chat.abort": { aborted: false, runIds: [] }, }); - await stopWorkboardCard({ host, client: client as never, card: linked }); + await stopCard(client, linked); expect(client.request).toHaveBeenCalledTimes(2); expect(client.request).toHaveBeenNthCalledWith(1, "chat.abort", {