返回 DeepSeek-Reasonix
skills-settings-refresh-race.test.tsx
根目录 / desktop / frontend / src / __tests__ / skills-settings-refresh-race.test.tsx
1 import { JSDOM } from "jsdom";
2 import React, { act } from "react";
3 import { createRoot } from "react-dom/client";
4 import { SkillsSettingsPage } from "../components/CapabilitiesPanel";
5 import { LocaleProvider } from "../lib/i18n";
6 import type { AppBindings } from "../lib/bridge";
7 import { installDesktopHostStub } from "./desktopHostStub";
8
9 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
10 pretendToBeVisual: true,
11 url: "http://localhost/",
12 });
13 Object.defineProperty(dom.window.HTMLElement.prototype, "attachEvent", { configurable: true, value: () => {} });
14 Object.defineProperty(dom.window.HTMLElement.prototype, "detachEvent", { configurable: true, value: () => {} });
15 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
16 globalThis.window = dom.window as unknown as Window & typeof globalThis;
17 globalThis.document = dom.window.document;
18 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
19 globalThis.HTMLElement = dom.window.HTMLElement;
20 globalThis.Event = dom.window.Event;
21 globalThis.MouseEvent = dom.window.MouseEvent;
22 globalThis.localStorage = dom.window.localStorage;
23 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
24 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
25
26 type SkillsView = {
27 skills: Array<{ name: string; description: string; scope: string; runAs: string; enabled: boolean }>;
28 skillRoots: [];
29 allowImplicitInvocation: boolean;
30 };
31
32 const pending: Record<string, Array<(view: SkillsView) => void>> = { a: [], b: [] };
33 let activeWorkspace = "a";
34
35 function view(name: string): SkillsView {
36 return {
37 skills: [{ name, description: name, scope: "project", runAs: "direct", enabled: true }],
38 skillRoots: [],
39 allowImplicitInvocation: true,
40 };
41 }
42
43 function flushPromises(): Promise<void> {
44 return new Promise((resolve) => setTimeout(resolve, 0));
45 }
46
47 async function waitFor(label: string, predicate: () => boolean) {
48 for (let attempt = 0; attempt < 30; attempt += 1) {
49 await act(async () => {
50 await flushPromises();
51 });
52 if (predicate()) return;
53 }
54 throw new Error(`timed out waiting for ${label}`);
55 }
56
57 const appBindings = {
58 Meta: async () => ({ eventChannel: "agent:event", cwd: `/workspace-${activeWorkspace}` }),
59 ListTabs: async () => [{ id: `tab-${activeWorkspace}`, scope: "project", workspaceRoot: `/workspace-${activeWorkspace}`, active: true }],
60 SkillsSettings: async () => new Promise<SkillsView>((resolve) => {
61 pending[activeWorkspace].push(resolve);
62 }),
63 } as unknown as AppBindings;
64
65 installDesktopHostStub(({ main: { App: appBindings } }).main.App);
66
67 const rootEl = document.getElementById("root");
68 if (!rootEl) throw new Error("missing root");
69 const root = createRoot(rootEl);
70
71 await act(async () => {
72 root.render(
73 <LocaleProvider>
74 <SkillsSettingsPage activeWorkspaceKey="tab-a\u0000/workspace-a" />
75 </LocaleProvider>,
76 );
77 await flushPromises();
78 });
79 await waitFor("workspace A request", () => pending.a.length === 1);
80
81 activeWorkspace = "b";
82 await act(async () => {
83 root.render(
84 <LocaleProvider>
85 <SkillsSettingsPage activeWorkspaceKey="tab-b\u0000/workspace-b" />
86 </LocaleProvider>,
87 );
88 await flushPromises();
89 });
90 await waitFor("workspace B request", () => pending.b.length === 1);
91
92 await act(async () => {
93 pending.b.shift()?.(view("skill-b"));
94 await flushPromises();
95 });
96 await waitFor("workspace B render", () => rootEl.textContent?.includes("skill-b") === true);
97
98 await act(async () => {
99 pending.a.shift()?.(view("skill-a"));
100 await flushPromises();
101 });
102 if (rootEl.textContent?.includes("skill-a")) {
103 throw new Error("stale workspace A response overwrote workspace B skills");
104 }
105 if (!rootEl.textContent?.includes("skill-b")) {
106 throw new Error("workspace B skills disappeared after stale response");
107 }
108
109 await act(async () => {
110 root.unmount();
111 await flushPromises();
112 });
113 console.log("skills settings refresh race: passed");
114
114 lines Plain Text