| 1 | import assert from "node:assert/strict"; |
| 2 | import { register } from "node:module"; |
| 3 | import React, { act } from "react"; |
| 4 | import { createRoot } from "react-dom/client"; |
| 5 | import { JSDOM } from "jsdom"; |
| 6 | import { initialState } from "../lib/useController"; |
| 7 | import { applyHydrateErrorState } from "../lib/hydrateErrorState"; |
| 8 | import { projectSessionAvailability, type SessionAvailability } from "../lib/sessionAvailability"; |
| 9 | import { useTranscriptSurfaceProjection, type TranscriptSurfaceProjectionInput } from "../app-runtime/useTranscriptSurfaceProjection"; |
| 10 | import { projectNavigationSurfaceTarget } from "../app-runtime/conversationProjection"; |
| 11 | import { SessionRecoveryBanner } from "../components/SessionRecoveryBanner"; |
| 12 | import { LocaleProvider, type Translator } from "../lib/i18n"; |
| 13 | |
| 14 | register(new URL("../../scripts/svg-loader.mjs", import.meta.url)); |
| 15 | const { ChatPaneRegion } = await import("../app-shell/ChatPaneRegion"); |
| 16 | |
| 17 | const dom = new JSDOM("<div id='root'></div>", { url: "http://localhost" }); |
| 18 | Object.assign(globalThis, { window: dom.window, document: dom.window.document, localStorage: dom.window.localStorage, |
| 19 | IS_REACT_ACT_ENVIRONMENT: true }); |
| 20 | const root = createRoot(document.getElementById("root")!); |
| 21 | const readyLocal = { ...initialState, meta: { ready: true, eventChannel: "fixture" } }; |
| 22 | const failedLocal = applyHydrateErrorState(readyLocal, "startup", "history read failed"); |
| 23 | assert.deepEqual(projectSessionAvailability({ local: { ...failedLocal, meta: { |
| 24 | ...readyLocal.meta, ready: false, historicalSource: { hostId: "local", path: "/fixture/legacy.jsonl" }, |
| 25 | } } }), { kind: "pending", source: "runtime" }, "unprepared history is a user choice, not a failed recovery"); |
| 26 | const noop = () => {}; |
| 27 | const common: TranscriptSurfaceProjectionInput = { |
| 28 | hydrating: false, hydrateHistoryLoaded: undefined, hydratePlaceholderItems: undefined, hydratePlaceholderActive: false, |
| 29 | items: [], remote: false, remoteItems: [], activeTabId: "A", geometrySessionKey: "A", transitioning: false, |
| 30 | navigationDataReady: true, preserved: null, controllerReady: true, |
| 31 | availability: projectSessionAvailability({ local: readyLocal }), sessionActivity: false, imDetailActive: false, |
| 32 | sessionHasContent: false, commitRendered: noop, commitPaint: () => null, commitSingleSurface: noop, |
| 33 | ports: { loadOlderHistory: async () => false, commitThenSend: async () => {} }, |
| 34 | }; |
| 35 | let projection!: ReturnType<typeof useTranscriptSurfaceProjection>; |
| 36 | function ProjectionProbe({ input }: { input: TranscriptSurfaceProjectionInput }) { |
| 37 | projection = useTranscriptSurfaceProjection(input); |
| 38 | return null; |
| 39 | } |
| 40 | const paintProjection = (input: TranscriptSurfaceProjectionInput) => act(async () => root.render(<ProjectionProbe input={input} />)); |
| 41 | try { |
| 42 | await paintProjection(common); |
| 43 | assert.equal(projection.emptyHero, true, "successful empty history shows the welcome"); |
| 44 | await paintProjection({ ...common, availability: projectSessionAvailability({ local: failedLocal }) }); |
| 45 | assert.equal(projection.emptyHero, false, "failed history is not a successful empty session"); |
| 46 | for (const state of ["connecting", "reconnecting", "serve_down", "error", "disconnected", "ready"] as const) { |
| 47 | for (const hydrated of [false, true]) { |
| 48 | const remote = { state, hydrated, error: "", surfaceGeneration: 1 }; |
| 49 | await paintProjection({ ...common, remote: true, availability: projectSessionAvailability({ local: readyLocal, remote }) }); |
| 50 | assert.equal(projection.emptyHero, state === "ready" && hydrated, `${state}/${hydrated} uses remote readiness`); |
| 51 | } |
| 52 | } |
| 53 | const remoteHistoryFailure = { state: "ready" as const, hydrated: false, error: "snapshot failed", surfaceGeneration: 1 }; |
| 54 | const failedTarget = projectNavigationSurfaceTarget({ activeTabId: "remote", sessionKey: "remote", local: readyLocal, remote: remoteHistoryFailure }); |
| 55 | assert.equal(failedTarget.hydrating, false, "exhausted remote hydration releases the navigation overlay"); |
| 56 | assert.equal(failedTarget.hydrateError, "snapshot failed"); |
| 57 | await paintProjection({ ...common, remote: true, availability: projectSessionAvailability({ remote: remoteHistoryFailure }) }); |
| 58 | assert.equal(projection.emptyHero, false); |
| 59 | for (const partial of [{ transitioning: true }, { sessionActivity: true }, { hydratePlaceholderActive: true, hydratePlaceholderItems: [] }, { sessionHasContent: true }]) { |
| 60 | await paintProjection({ ...common, ...partial }); |
| 61 | assert.equal(projection.emptyHero, false, "navigation, decisions, running turns and content keep the main surface visible"); |
| 62 | } |
| 63 | |
| 64 | let retries = 0; |
| 65 | let rejectRetry!: (error: Error) => void; |
| 66 | const onRetry = () => { retries++; return new Promise<void>((_resolve, reject) => { rejectRetry = reject; }); }; |
| 67 | const availability: SessionAvailability = { kind: "error", source: "connection", detail: "tunnel closed" }; |
| 68 | const paintBanner = (identity: string, value = availability) => act(async () => root.render( |
| 69 | <LocaleProvider><SessionRecoveryBanner key={identity} availability={value} onRetry={onRetry} /></LocaleProvider>, |
| 70 | )); |
| 71 | await paintBanner("A"); |
| 72 | const button = document.querySelector<HTMLButtonElement>(".session-recovery .btn--primary")!; |
| 73 | await act(async () => { button.click(); button.click(); }); |
| 74 | assert.equal(retries, 1, "rapid retries share one in-flight action"); |
| 75 | assert.equal(button.disabled, true); |
| 76 | assert.equal(document.querySelector(".session-recovery")?.getAttribute("role"), "status"); |
| 77 | await act(async () => { rejectRetry(new Error("retry failed")); }); |
| 78 | assert.equal(button.disabled, false, "failed recovery restores the action"); |
| 79 | await act(async () => document.querySelector<HTMLButtonElement>("button[aria-controls]")!.click()); |
| 80 | assert.equal(document.querySelector(".session-recovery__detail")?.textContent, "retry failed"); |
| 81 | await act(async () => button.click()); |
| 82 | await paintBanner("B"); |
| 83 | await act(async () => { rejectRetry(new Error("old session failure")); }); |
| 84 | assert.equal(document.body.textContent?.includes("old session failure"), false, "late recovery cannot overwrite another session's error"); |
| 85 | await paintBanner("B", { kind: "ready", source: "history" }); |
| 86 | assert.equal(document.querySelector(".session-recovery"), null); |
| 87 | |
| 88 | await act(async () => root.render(<LocaleProvider><ChatPaneRegion transitioning={false} t={((key: string) => key) as Translator} |
| 89 | imDetail={null} transcript={{ state: failedLocal, items: [], tabId: "local", geometrySessionKey: "local", footerHeight: 140, |
| 90 | transcriptHydrating: false, navigationDataReady: true, readOnly: false, controllerReady: true, hydratePlaceholderActive: false, |
| 91 | clearContextPending: false, availability: projectSessionAvailability({ local: failedLocal }), |
| 92 | rewind: { stateActive: false, committing: false, signal: undefined }, revealSignal: 0, invocationMetadata: undefined, |
| 93 | surfaceCommitToken: undefined, liveStore: undefined }} onRetryHistory={async () => { retries++; }} |
| 94 | commands={{ onPrompt: noop, onFork: noop, onLoadOlderHistory: async () => false, onSurfacePaintReady: noop }} /></LocaleProvider>)); |
| 95 | const recovery = document.querySelector(".session-recovery")!; |
| 96 | assert.ok(recovery, "actual local chat region renders history recovery"); |
| 97 | assert.equal(recovery.closest("main"), null, "history retry is outside the main transcript collapse"); |
| 98 | assert.ok(document.querySelector("main .session-recovery-placeholder")); |
| 99 | const beforeRetry = retries; |
| 100 | await act(async () => recovery.querySelector<HTMLButtonElement>(".btn--primary")!.click()); |
| 101 | assert.equal(retries, beforeRetry + 1, "local history recovery calls the owning retry command"); |
| 102 | await act(async () => root.unmount()); |
| 103 | console.log("PASS session recovery: source readiness, navigation errors, visible controls, retry coalescing and stale completion isolation"); |
| 104 | } finally { dom.window.close(); } |
| 105 |