| 1 | import { noteSessionObservation, sessionObservationDiagnostics } from "../lib/sessionObservationDiagnostics"; |
| 2 | import { notePromptSubmission } from "../lib/promptSubmissionDiagnostics"; |
| 3 | import assert from "node:assert/strict"; |
| 4 | import { JSDOM } from "jsdom"; |
| 5 | import { installDesktopHostStub } from "./desktopHostStub"; |
| 6 | import { exportChunks, runSessionExport } from "../lib/sessionExportOperation"; |
| 7 | import { cancelSessionExport, sessionExportProgress } from "../lib/sessionExportProgress"; |
| 8 | import type { SessionExportHandle } from "../generated/desktopContract.generated"; |
| 9 | const dom = new JSDOM("<!doctype html><html><body></body></html>", { url: "http://localhost" }); |
| 10 | Object.assign(globalThis, { window: dom.window, document: dom.window.document }); |
| 11 | const body = Buffer.from("第一条问题\n工具参数和完整输出✓\n最终回答", "utf8"); |
| 12 | let cancelled = 0, finished = 0; |
| 13 | let exportedObservation = ""; |
| 14 | let blocked = false; |
| 15 | let release!: () => void; |
| 16 | let started!: () => void; |
| 17 | const readStarted = new Promise<void>(resolve => { started = resolve; }); |
| 18 | const gate = new Promise<void>(resolve => { release = resolve; }); |
| 19 | const stub = installDesktopHostStub({ |
| 20 | BeginSessionExportForTarget: async (_selector: unknown, _tab: string, _format: string, _title: string, observation: string) => { |
| 21 | exportedObservation = observation; |
| 22 | return { exportId: "fixed-A", format: "clipboard", snapshot: {} }; |
| 23 | }, |
| 24 | ReadSessionExportChunk: async (_id: string, offset: number) => { |
| 25 | if (blocked) { started(); await gate; } |
| 26 | const end = Math.min(offset + 1, body.length); |
| 27 | return { data: body.subarray(offset, end).toString("base64"), nextOffset: end, done: end === body.length }; |
| 28 | }, |
| 29 | CancelSessionExport: async () => { cancelled++; }, |
| 30 | FinishSessionExport: async () => { finished++; return { paths: [], records: 137, pages: 0 }; }, |
| 31 | CancelSession: async () => { throw Error("export must not cancel execution"); }, |
| 32 | }); |
| 33 | try { |
| 34 | noteSessionObservation("session-id:shared", { action: "unsubscribe", tabId: "remote-A", generation: 1, sequence: 7 }); |
| 35 | noteSessionObservation("session-id:shared", { action: "terminal", tabId: "remote-B", generation: 2, sequence: 9 }); |
| 36 | assert.equal(sessionObservationDiagnostics("session-id:shared", "remote-A").events[0]?.action, "unsubscribe"); |
| 37 | assert.equal(sessionObservationDiagnostics("session-id:shared", "remote-B").events.length, 1, "equal remote routes do not mix renderer bindings"); |
| 38 | let text = ""; |
| 39 | for await (const part of exportChunks({ exportId: "fixed-A" } as SessionExportHandle)) text += part; |
| 40 | assert.equal(text, body.toString("utf8"), "UTF-8 characters crossing every RPC boundary survive"); |
| 41 | const input = { selector: { ref: { hostId: "local", sessionId: "A" } }, tabId: "tab-A", format: "clipboard", title: "A", remote: false, residentItems: 0, runningStream: false, unresolvedTools: 0 }; |
| 42 | notePromptSubmission({ tabId: "tab-A", sessionId: "A", sessionGeneration: 0, promptId: "1", turnId: "turn-A", runtimeEpoch: "runtime-A", kind: "approval" }, "transport", "accepted"); |
| 43 | const result = await runSessionExport(input); |
| 44 | const promptEvent = JSON.parse(exportedObservation).lifecycleDiagnostics.events.find((event: { action: string }) => event.action === "prompt-transport"); |
| 45 | assert.equal(promptEvent.status, "accepted", "session exports contain the actual approval transport outcome"); |
| 46 | assert.equal(promptEvent.prompt.bindingGeneration, 0, "export preserves the valid initial generation"); |
| 47 | assert.equal(result.text, text); |
| 48 | assert.equal(finished, 1); |
| 49 | blocked = true; |
| 50 | const pending = runSessionExport(input); |
| 51 | await readStarted; |
| 52 | await cancelSessionExport("fixed-A"); |
| 53 | release(); |
| 54 | assert.equal((await pending).cancelled, true); |
| 55 | assert.equal(finished, 1, "cancelled export is never published"); |
| 56 | assert.ok(cancelled >= 1); |
| 57 | assert.equal(sessionExportProgress.getSnapshot().length, 0); |
| 58 | console.log("session export operations: UTF-8 chunk boundaries, full clipboard and independent cancellation passed"); |
| 59 | } finally { stub.uninstall(); dom.window.close(); } |
| 60 |