| 1 | import assert from "node:assert/strict"; |
| 2 | import { JSDOM } from "jsdom"; |
| 3 | import { installDesktopHostStub } from "./desktopHostStub"; |
| 4 | import { createManualSession } from "../lib/manualCreationRequests"; |
| 5 | import type { ManualSessionCreationRequest, ManualSessionCreationView } from "../generated/desktopContract.generated"; |
| 6 | |
| 7 | const dom = new JSDOM("", { url: "http://localhost" }); |
| 8 | Object.assign(globalThis, { window: dom.window, document: dom.window.document }); |
| 9 | const request: ManualSessionCreationRequest = { operationId: "lost-response", workspaceId: "", scope: "project", workspaceRoot: "/workspace" }; |
| 10 | const operation: ManualSessionCreationView = { ...request, workspaceId: "canonical", scope: "project", workspaceRoot: "/workspace", |
| 11 | ref: { hostId: "local", sessionId: "same-session" }, topicId: "same-topic", phase: "ready", |
| 12 | settings: { model: "stub", mode: "normal", toolApprovalMode: "default", disabledMcp: {}, mcpOrder: [] } }; |
| 13 | const begins: string[] = []; |
| 14 | const reads: string[] = []; |
| 15 | const opened: string[] = []; |
| 16 | const stub = installDesktopHostStub({ |
| 17 | BeginManualSessionCreation: async (input: ManualSessionCreationRequest) => { |
| 18 | begins.push(input.operationId); |
| 19 | throw new Error("connection closed after persistence"); |
| 20 | }, |
| 21 | GetManualSessionCreation: async (id: string) => { reads.push(id); return operation; }, |
| 22 | }); |
| 23 | try { |
| 24 | const result = await createManualSession(request, { onSurfaceReady: async view => { opened.push(view.ref.sessionId); } }); |
| 25 | assert.equal(result.ref.sessionId, "same-session"); |
| 26 | assert.deepEqual(begins, [request.operationId], "lost acknowledgement never allocates another creation request"); |
| 27 | assert.deepEqual(reads, [request.operationId], "recovery reads the durable original request"); |
| 28 | assert.deepEqual(opened, ["same-session"], "older hosts without surfaceReady still open at ready"); |
| 29 | stub.commands.BeginManualSessionCreation = async () => ({ ...operation, phase: "starting", progress: { |
| 30 | status: "blocked", stage: "preparing_storage", errorCode: "unsupported_ui_schema", stageStartedAt: 1, elapsedMs: 0, slow: false, |
| 31 | } }); |
| 32 | const blocked = await createManualSession(request); |
| 33 | assert.equal(blocked.progress?.status, "blocked", "terminal host progress ends observation even if the last durable phase was starting"); |
| 34 | assert.equal(reads.length, 1, "a blocked operation does not poll indefinitely"); |
| 35 | for (const status of ["queued", "running", "waiting_lock", "waiting_workspace", "retrying_storage"]) { |
| 36 | stub.commands.BeginManualSessionCreation = async () => ({ ...operation, phase: "failed", error: "session_operation:target_changed:previous failure", progress: { |
| 37 | status, stage: "queued", stageStartedAt: 1, elapsedMs: 0, slow: false, |
| 38 | } }); |
| 39 | const recovered = await createManualSession(request); |
| 40 | assert.equal(recovered.phase, "ready", `${status} observes automatic recovery despite the older durable failed phase`); |
| 41 | } |
| 42 | assert.equal(reads.length, 6, "all active recovery statuses read the eventual result instead of reporting stale failure"); |
| 43 | stub.commands.BeginManualSessionCreation = async (input: ManualSessionCreationRequest) => { |
| 44 | begins.push(input.operationId); |
| 45 | return { ...operation, phase: "starting", surfaceReady: true }; |
| 46 | }; |
| 47 | const abandoned = await createManualSession(request, { |
| 48 | isObservationCurrent: () => false, |
| 49 | onSurfaceReady: async view => { opened.push(view.ref.sessionId); }, |
| 50 | }); |
| 51 | assert.equal(abandoned.phase, "starting", "stale observation leaves the acknowledged host operation running"); |
| 52 | assert.equal(begins.length, 2, "the requested creation is accepted even when navigation becomes stale"); |
| 53 | assert.equal(reads.length, 6, "a stale observer does not start a poll"); |
| 54 | assert.equal(opened.length, 1, "a stale observer never opens its target"); |
| 55 | let observing = true; |
| 56 | await createManualSession(request, { |
| 57 | isObservationCurrent: () => observing, |
| 58 | onSurfaceReady: async () => { observing = false; }, |
| 59 | }); |
| 60 | assert.equal(reads.length, 6, "leaving during surface activation stops observation without cancelling creation"); |
| 61 | const retried: string[] = []; |
| 62 | const updates: string[] = []; |
| 63 | stub.commands.BeginManualSessionCreation = async () => ({ ...operation, phase: "failed" }); |
| 64 | stub.commands.RetryManualSessionCreation = async (id: string) => { |
| 65 | retried.push(id); |
| 66 | return { ...operation, phase: "starting", surfaceReady: true, progress: { status: "running", stage: "building_runtime", elapsedMs: 1, stageStartedAt: 1, slow: false } }; |
| 67 | }; |
| 68 | let poll = 0; |
| 69 | stub.commands.GetManualSessionCreation = async () => ++poll === 1 |
| 70 | ? { ...operation, phase: "starting", surfaceReady: true, progress: { status: "running", stage: "building_runtime", elapsedMs: 500, stageStartedAt: 1, slow: false } } |
| 71 | : { ...operation, surfaceReady: true }; |
| 72 | await createManualSession(request, { retry: true, onSurfaceReady: async () => {}, onProgress: view => { updates.push(view.phase); } }); |
| 73 | assert.deepEqual(retried, [request.operationId], "retry keeps the original durable request identity"); |
| 74 | assert.deepEqual(updates, ["starting", "ready"], "elapsed time alone does not repaint the app or duplicate notices"); |
| 75 | console.log("PASS manual creation recovers lost acknowledgements with the original identity and older-host fallback"); |
| 76 | } finally { |
| 77 | stub.uninstall(); |
| 78 | dom.window.close(); |
| 79 | } |
| 80 |