返回 DeepSeek-Reasonix
session-draft-startup-navigation.test.tsx
根目录 / desktop / frontend / src / __tests__ / session-draft-startup-navigation.test.tsx
1 import assert from "node:assert/strict";
2 import React, { act } from "react";
3 import { createRoot } from "react-dom/client";
4 import { JSDOM } from "jsdom";
5
6 import { useSessionDraftSurface } from "../app-runtime/useSessionDraftSurface";
7 import type { SessionDraftView } from "../generated/desktopContract.generated";
8 import { installDesktopHostStub } from "./desktopHostStub";
9
10 function deferred<T>() {
11 let resolve!: (value: T) => void;
12 const promise = new Promise<T>((next) => { resolve = next; });
13 return { promise, resolve };
14 }
15
16 const dom = new JSDOM("<div id='root'></div>", { url: "http://localhost" });
17 Object.assign(globalThis, {
18 window: dom.window,
19 document: dom.window.document,
20 IS_REACT_ACT_ENVIRONMENT: true,
21 requestAnimationFrame: (callback: FrameRequestCallback) => setTimeout(() => callback(0), 0),
22 cancelAnimationFrame: (id: number) => clearTimeout(id),
23 });
24 dom.window.requestAnimationFrame = globalThis.requestAnimationFrame;
25 dom.window.cancelAnimationFrame = globalThis.cancelAnimationFrame;
26
27 const settings = {
28 model: "fixture/model",
29 mode: "normal",
30 toolApprovalMode: "ask",
31 disabledMcp: {},
32 mcpOrder: [],
33 };
34 const restoredDraft: SessionDraftView = {
35 id: "draft-restored",
36 workspaceId: "workspace-restored",
37 scope: "project",
38 workspaceRoot: "/workspace/restored",
39 revision: 1,
40 contentJson: "{}",
41 settings,
42 status: "active",
43 updatedAt: 1,
44 };
45 const globalDraft: SessionDraftView = {
46 ...restoredDraft,
47 id: "draft-global",
48 workspaceId: "workspace-global",
49 scope: "global",
50 workspaceRoot: "",
51 };
52
53 let restore = async (): Promise<SessionDraftView | null> => null;
54 let tabs: Array<{ id: string }> = [];
55 let globalOpenCalls = 0;
56 const restoreTargets: string[] = [];
57 const stub = installDesktopHostStub({
58 ListSessionDraftSummaries: async () => [],
59 RestoreSessionDraft: () => restore(),
60 ListTabs: async () => tabs,
61 OpenSessionDraftForTarget: async () => {
62 globalOpenCalls++;
63 return structuredClone(globalDraft);
64 },
65 GetDraftContext: async (id: string) => ({
66 draft: structuredClone(id === globalDraft.id ? globalDraft : restoredDraft),
67 commands: [],
68 servers: [],
69 models: [],
70 }),
71 SetSessionDraftRestoreTarget: async (id: string) => { restoreTargets.push(id); },
72 DismissSessionDraft: async () => {},
73 AttachmentDataURLForTarget: async () => "",
74 });
75
76 let navigationIntent = 0;
77 let claimCalls = 0;
78 let owner!: ReturnType<typeof useSessionDraftSurface>;
79 function Probe() {
80 owner = useSessionDraftSurface({
81 onAccepted: () => {},
82 onChanged: () => {},
83 claimNavigationIntent: () => {
84 claimCalls++;
85 return ++navigationIntent;
86 },
87 currentNavigationIntent: () => navigationIntent,
88 isNavigationIntentCurrent: (intent) => intent === navigationIntent,
89 });
90 return null;
91 }
92
93 const root = createRoot(document.getElementById("root")!);
94 try {
95 await act(async () => { root.render(<Probe />); });
96
97 const lateRestore = deferred<SessionDraftView | null>();
98 restore = () => lateRestore.promise;
99 let pending!: Promise<void>;
100 act(() => { pending = owner.initializeEmptySurface(); });
101 navigationIntent++;
102 lateRestore.resolve(structuredClone(restoredDraft));
103 await act(async () => { await pending; });
104 assert.equal(owner.surface, null, "a late restore read cannot override newer user navigation");
105 assert.equal(claimCalls, 0, "a stale startup probe never claims a fresh navigation intent");
106
107 restore = async () => null;
108 tabs = [{ id: "formal" }];
109 await act(async () => { await owner.initializeEmptySurface(); });
110 assert.equal(owner.surface, null, "an existing formal session remains the visible startup surface");
111 assert.equal(claimCalls, 0, "probing startup state does not invalidate formal-session hydration");
112 assert.equal(globalOpenCalls, 0, "an existing formal session does not create a global draft");
113
114 restore = async () => structuredClone(restoredDraft);
115 await act(async () => { await owner.initializeEmptySurface(); });
116 assert.equal(owner.surface?.draft.id, restoredDraft.id, "a saved draft restore target wins over passive formal hydration");
117 assert.equal(claimCalls, 1, "a confirmed draft restore claims exactly one navigation intent");
118 assert.equal(restoreTargets.at(-1), restoredDraft.id, "the installed draft remains the durable restore target");
119
120 await act(async () => { await owner.dismiss(); });
121 restore = async () => null;
122 tabs = [];
123 await act(async () => { await owner.initializeEmptySurface(); });
124 assert.equal(owner.surface?.draft.id, globalDraft.id, "startup without a formal or restored surface opens the global draft");
125 assert.equal(claimCalls, 2, "only the confirmed empty startup path claims another navigation intent");
126 assert.equal(globalOpenCalls, 1, "the empty startup path creates or restores one global draft");
127
128 await act(async () => { root.unmount(); });
129 console.log("session draft startup navigation: passive hydration, restore precedence and stale reads passed");
130 } finally {
131 stub.uninstall();
132 dom.window.close();
133 }
134
134 lines Plain Text