返回 DeepSeek-Reasonix
session-navigation-draft-fence.test.tsx
根目录 / desktop / frontend / src / __tests__ / session-navigation-draft-fence.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 { useSessionNavigationCommands, type SessionNavigationCommandsInput } from "../app-runtime/useSessionNavigationCommands";
7
8 function deferred() {
9 let resolve!: () => void;
10 const promise = new Promise<void>((done) => { resolve = done; });
11 return { promise, resolve };
12 }
13
14 const dom = new JSDOM("<div id='root'></div>");
15 Object.assign(globalThis, {
16 window: dom.window,
17 document: dom.window.document,
18 IS_REACT_ACT_ENVIRONMENT: true,
19 });
20
21 let intent = 0;
22 let commands!: ReturnType<typeof useSessionNavigationCommands>;
23 const firstDismiss = deferred();
24 const secondDismiss = deferred();
25 const dismissals = [firstDismiss, secondDismiss];
26 const enqueued: Array<{ request: unknown; intent: number }> = [];
27
28 function Probe() {
29 commands = useSessionNavigationCommands({
30 activeTab: { id: "fixture", scope: "project", workspaceRoot: "/workspace" },
31 showToast: () => {},
32 closeTransientOverlays: () => {},
33 clearImDetail: () => {},
34 prepareBlankWorkspace: () => {},
35 navigation: {
36 enqueueNavigation: async () => {},
37 enqueueNavigationWithIntent: async (request, navigationIntent) => {
38 enqueued.push({ request, intent: navigationIntent });
39 },
40 openRemoteProject: async () => ({ status: "cancelled", reason: "superseded" }),
41 },
42 noteNavigationIntent: () => ++intent,
43 beginNavigationSurface: () => {},
44 settleNavigationSurface: () => {},
45 isNavigationIntentCurrent: (candidate) => candidate === intent,
46 markProjectChanged: () => {},
47 refreshTabMetas: async () => {},
48 refreshHistoryView: () => {},
49 enterConversation: () => {},
50 pickWorkspace: async () => "",
51 switchWorkspace: async () => {},
52 draft: {
53 open: async () => {},
54 dismiss: () => dismissals.shift()!.promise,
55 },
56 ports: {
57 openTaskSessionForTab: async () => ({ ok: false }),
58 listSessionsForTab: async () => [],
59 },
60 } as SessionNavigationCommandsInput);
61 return null;
62 }
63
64 const root = createRoot(document.getElementById("root")!);
65 try {
66 await act(async () => { root.render(<Probe />); });
67
68 let stale!: Promise<void>;
69 let latest!: Promise<void>;
70 act(() => {
71 stale = commands.handleOpenTopic("project", "/workspace", "topic-a");
72 latest = commands.openCanonicalSession({ hostId: "local", sessionId: "session-b" });
73 });
74
75 firstDismiss.resolve();
76 await act(async () => { await stale; });
77 assert.deepEqual(enqueued, [], "navigation superseded during draft cleanup cannot enqueue afterward");
78
79 secondDismiss.resolve();
80 await act(async () => { await latest; });
81 assert.equal(enqueued.length, 1);
82 assert.equal(enqueued[0]?.intent, 2, "the winning request keeps the intent captured before its first await");
83 assert.deepEqual(enqueued[0]?.request, {
84 kind: "canonical-session",
85 ref: { hostId: "local", sessionId: "session-b" },
86 });
87
88 await act(async () => { root.unmount(); });
89 console.log("session navigation draft fence: stale cleanup completion cannot override the latest target");
90 } finally {
91 dom.window.close();
92 }
93
93 lines Plain Text