返回 DeepSeek-Reasonix
manual-creation-requests.test.ts
根目录 / desktop / frontend / src / __tests__ / manual-creation-requests.test.ts
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
80 lines TYPESCRIPT