| 1 | import { act } from "react"; |
| 2 | import type { useController } from "../lib/useController"; |
| 3 | import { runtimeStateStore, type RuntimeState } from "../lib/runtimeStateStore"; |
| 4 | import type { Meta } from "../lib/types"; |
| 5 | import type { DesktopHostStub } from "../__tests__/desktopHostStub"; |
| 6 | |
| 7 | type Controller = ReturnType<typeof useController>; |
| 8 | |
| 9 | export async function runTodoSessionSwitchScenario(options: { |
| 10 | controller: () => Controller | undefined; |
| 11 | desktopStub: DesktopHostStub; |
| 12 | currentSessionPath: () => string; |
| 13 | meta: (overrides: Partial<Meta>) => Meta; |
| 14 | equal: (actual: unknown, expected: unknown, label: string) => void; |
| 15 | }): Promise<void> { |
| 16 | const { controller, desktopStub, currentSessionPath, meta, equal } = options; |
| 17 | const pausedTodos = [{ content: "Finish session A", status: "in_progress" }]; |
| 18 | const sessionA = "/sessions/todo-a.jsonl", sessionB = "/sessions/todo-b.jsonl"; |
| 19 | desktopStub.commands.MetaForTab = async () => meta({ |
| 20 | sessionPath: currentSessionPath(), sessionGeneration: 1, |
| 21 | canonicalTodos: currentSessionPath() === sessionA ? pausedTodos : [], |
| 22 | }); |
| 23 | let revision = 0; |
| 24 | const publish = (sessionPath: string, todos: typeof pausedTodos) => { |
| 25 | const state: RuntimeState = { |
| 26 | schemaVersion: 1, runtimeEpoch: "todo-runtime", activityRevision: 1, revision: ++revision, |
| 27 | phase: "idle", running: false, turnId: "paused-turn", turnStatus: "interrupted", turnEventSeq: 1, |
| 28 | pendingPrompt: false, cancelRequested: false, cancellable: false, backgroundJobs: 0, activity: "", todos, |
| 29 | }; |
| 30 | runtimeStateStore.commit({ epoch: "todo-app", revision, topics: [], sessions: [{ |
| 31 | tabId: "tab-switch", scope: "project", workspaceRoot: "/repo", topicId: "topic-a", |
| 32 | sessionPath, sessionGeneration: 1, open: true, remote: false, freshness: "synced", state, |
| 33 | }] }); |
| 34 | }; |
| 35 | await act(async () => { const navigation = controller()?.resumeSession(sessionA, "tab-switch"); await navigation?.surfaceReady; publish(sessionA, pausedTodos); }); |
| 36 | equal(controller()?.state.meta?.canonicalTodos?.[0]?.content, "Finish session A", "paused A owns its unfinished todo"); |
| 37 | await act(async () => { const navigation = controller()?.resumeSession(sessionB, "tab-switch"); await navigation?.surfaceReady; }); |
| 38 | equal(controller()?.state.meta?.canonicalTodos?.length, 0, "B stays empty while A's runtime snapshot is still current in the feed"); |
| 39 | await act(async () => { publish(sessionB, []); const navigation = controller()?.resumeSession(sessionA, "tab-switch"); await navigation?.surfaceReady; }); |
| 40 | equal(controller()?.state.meta?.canonicalTodos?.[0]?.content, "Finish session A", "returning to A cannot be cleared by B's delayed empty snapshot"); |
| 41 | await act(async () => { publish(sessionB, []); }); |
| 42 | equal(controller()?.state.meta?.canonicalTodos?.[0]?.content, "Finish session A", "late B publication after the return to A cannot erase A's todo"); |
| 43 | await act(async () => { publish(sessionA, [{ content: "Finish session A", status: "completed" }]); }); |
| 44 | equal(controller()?.state.meta?.canonicalTodos?.[0]?.status, "completed", "a matching runtime snapshot still publishes live todo progress"); |
| 45 | await act(async () => { publish(sessionA, []); }); |
| 46 | equal(controller()?.state.meta?.canonicalTodos?.length, 0, "an authoritative empty list for the same session still clears todos"); |
| 47 | } |
| 48 |