返回 DeepSeek-Reasonix
diagnostics-settings.test.tsx
根目录 / desktop / frontend / src / __tests__ / diagnostics-settings.test.tsx
1 import { JSDOM } from "jsdom";
2 import React from "react";
3 import { act } from "react";
4 import { createRoot } from "react-dom/client";
5 import { DiagnosticsSettingsPage } from "../components/DiagnosticsSettingsPage";
6 import type { AppBindings } from "../lib/bridge";
7 import { LocaleProvider } from "../lib/i18n";
8 import type { CapabilityDiagnosticsReport, SettingsTab } from "../lib/types";
9 import { installDesktopHostStub } from "./desktopHostStub";
10
11 function ok(value: unknown, message: string) {
12 if (!value) throw new Error(message);
13 }
14
15 function flush(): Promise<void> {
16 return new Promise((resolve) => setTimeout(resolve, 0));
17 }
18
19 async function waitFor(label: string, predicate: () => boolean) {
20 for (let attempt = 0; attempt < 40; attempt += 1) {
21 await act(async () => {
22 await flush();
23 });
24 if (predicate()) return;
25 }
26 throw new Error(`timed out waiting for ${label}: ${document.body?.textContent?.slice(0, 400) ?? ""}`);
27 }
28
29 function installDom() {
30 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
31 pretendToBeVisual: true,
32 url: "http://localhost/",
33 });
34 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
35 globalThis.window = dom.window as unknown as Window & typeof globalThis;
36 globalThis.document = dom.window.document;
37 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
38 globalThis.Node = dom.window.Node;
39 globalThis.HTMLElement = dom.window.HTMLElement;
40 globalThis.HTMLButtonElement = dom.window.HTMLButtonElement;
41 globalThis.HTMLInputElement = dom.window.HTMLInputElement;
42 globalThis.Event = dom.window.Event;
43 globalThis.MouseEvent = dom.window.MouseEvent;
44 globalThis.localStorage = dom.window.localStorage;
45 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
46 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
47 Object.defineProperty(window, "matchMedia", {
48 configurable: true,
49 value: () => ({
50 matches: true,
51 media: "(prefers-reduced-motion: reduce)",
52 onchange: null,
53 addEventListener() {},
54 removeEventListener() {},
55 addListener() {},
56 removeListener() {},
57 dispatchEvent: () => false,
58 }),
59 });
60 return dom;
61 }
62
63 function baseReport(runtime: boolean): CapabilityDiagnosticsReport {
64 return {
65 schema_version: 1,
66 root: "<workspace>",
67 live: false,
68 summary: {
69 errors: 1,
70 warnings: 1,
71 infos: runtime ? 1 : 0,
72 instructions: 1,
73 skills: 1,
74 commands: 0,
75 hooks: 0,
76 plugins: 0,
77 mcp_servers: 1,
78 },
79 instructions: { docs: [{ path: "<workspace>/AGENTS.md", scope: "project", directory: "<workspace>", depth: 0, order: 1 }] },
80 skills: {
81 roots: [{ path: "<workspace>/.reasonix/skills", scope: "project", status: "ok" }],
82 entries: [{ name: "demo", path: "<workspace>/.reasonix/skills/demo/SKILL.md", status: "winner" }],
83 winners: 1,
84 shadowed: 0,
85 },
86 commands: { roots: [], entries: [], winners: 0, shadowed: 0 },
87 hooks: { trusted_project: false, project_defines_hooks: true, sources: [], entries: [] },
88 plugins: { packages: [] },
89 mcp: {
90 servers: [{
91 name: "demo-mcp",
92 transport: "stdio",
93 start_intent: "automatic",
94 source: "toml",
95 runtime_status: runtime ? "connected" : undefined,
96 tool_count: runtime ? 2 : undefined,
97 }],
98 },
99 issues: [
100 {
101 severity: "error",
102 code: "mcp.command_not_found",
103 subsystem: "mcp",
104 name: "demo-mcp",
105 message: "command missing",
106 remediation: "install binary",
107 settings_tab: "mcp",
108 },
109 {
110 severity: "warning",
111 code: "skill.missing_description",
112 subsystem: "skills",
113 name: "nodesc",
114 message: "no description",
115 settings_tab: "skills",
116 },
117 ],
118 };
119 }
120
121 console.log("diagnostics settings page");
122
123 {
124 const calls: boolean[] = [];
125 const navigations: SettingsTab[] = [];
126 installDom();
127 // Prefer English labels for stable button text assertions.
128 window.localStorage.setItem("reasonix-lang", "en");
129
130 const desktopStub = installDesktopHostStub(({
131 main: {
132 App: {
133 CapabilityDiagnostics: async (includeSessionRuntime: boolean) => {
134 calls.push(includeSessionRuntime);
135 return baseReport(includeSessionRuntime);
136 },
137 RuntimeDoctor: async () => ({
138 text: "runtime ok", publishedGeneration: 2, allowResume: true, cleanRollback: true,
139 hasIrreversible: false, noOpRebuilds: 1, fullRebuilds: 0, subgraphRebuilds: 1,
140 staleDrops: 0, admissionRejected: 0, runtimeOwnerFallbacks: 0,
141 skillWatch: { physicalWatches: 3, logicalSubscriptions: 4, scans: 0, scannedEntries: 0, eventsReceived: 2, notifications: 1, degradedRoots: 0, helperRestarts: 0 },
142 }),
143 } as Partial<AppBindings> as AppBindings,
144 },
145 }).main.App);
146
147 const rootEl = document.getElementById("root");
148 if (!rootEl) throw new Error("missing root");
149 const root = createRoot(rootEl);
150
151 await act(async () => {
152 root.render(
153 React.createElement(
154 LocaleProvider,
155 null,
156 React.createElement(DiagnosticsSettingsPage, {
157 onNavigate: (tab: SettingsTab) => {
158 navigations.push(tab);
159 },
160 }),
161 ),
162 );
163 await flush();
164 });
165
166 await waitFor("static report", () => (rootEl.textContent || "").includes("mcp.command_not_found"));
167 ok(calls[0] === false, "initial load must request static report (includeSessionRuntime=false)");
168 ok((rootEl.textContent || "").includes("skill.missing_description"), "warnings must render");
169 ok(rootEl.querySelector(".diag-summary"), "health summary must render");
170 ok(rootEl.querySelector('[data-testid="skill-watch-diagnostics"]')?.textContent?.includes("physical watches"), "skill watcher resource counters render in the doctor panel");
171 const frontendToggle = rootEl.querySelector('[data-testid="frontend-diagnostics-settings"] [role="switch"]');
172 ok(frontendToggle, "frontend diagnostics switch must be visible in the production diagnostics settings page");
173 ok(frontendToggle?.getAttribute("aria-checked") === "false", "frontend diagnostics switch starts off");
174
175 const runtimeToggle = rootEl.querySelector('input[type="checkbox"]') as HTMLInputElement | null;
176 ok(runtimeToggle, "runtime toggle must exist");
177 ok(runtimeToggle!.checked === false, "runtime toggle starts unchecked");
178 await act(async () => {
179 // Use click so React's controlled onChange sees the flipped checked state.
180 runtimeToggle!.click();
181 await flush();
182 });
183 await waitFor("runtime reload", () => calls.length >= 2 && calls[calls.length - 1] === true);
184
185 const gotoBtn = Array.from(rootEl.querySelectorAll("button")).find((b) =>
186 (b.textContent || "").includes("Open settings"),
187 );
188 ok(gotoBtn, "goto settings button must exist on issue with settings_tab");
189 await act(async () => {
190 gotoBtn!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
191 await flush();
192 });
193 ok(navigations.includes("mcp"), `goto settings must navigate to mcp, got ${JSON.stringify(navigations)}`);
194
195 const paths = rootEl.querySelectorAll(".diag-path");
196 ok(paths.length > 0, "redacted paths must render");
197
198 await act(async () => {
199 root.unmount();
200 });
201 }
202
203 {
204 installDom();
205 window.localStorage.setItem("reasonix-lang", "en");
206
207 const nullArrays = baseReport(false) as unknown as Record<string, unknown>;
208 nullArrays.summary = {
209 errors: 0,
210 warnings: 0,
211 infos: 0,
212 instructions: 0,
213 skills: 0,
214 commands: 0,
215 hooks: 0,
216 plugins: 0,
217 mcp_servers: 0,
218 };
219 nullArrays.issues = null;
220 nullArrays.instructions = { docs: null };
221 nullArrays.skills = { roots: null, entries: null, winners: 0, shadowed: 0 };
222 nullArrays.commands = { roots: null, entries: null, winners: 0, shadowed: 0 };
223 nullArrays.hooks = { trusted_project: false, project_defines_hooks: false, sources: null, entries: null };
224 nullArrays.plugins = { packages: null };
225 nullArrays.mcp = { servers: null };
226
227 const desktopStub = installDesktopHostStub(({
228 main: {
229 App: {
230 CapabilityDiagnostics: async () => nullArrays as unknown as CapabilityDiagnosticsReport,
231 } as Partial<AppBindings> as AppBindings,
232 },
233 }).main.App);
234
235 const rootEl = document.getElementById("root");
236 if (!rootEl) throw new Error("missing root");
237 const root = createRoot(rootEl);
238
239 await act(async () => {
240 root.render(
241 React.createElement(
242 LocaleProvider,
243 null,
244 React.createElement(DiagnosticsSettingsPage),
245 ),
246 );
247 await flush();
248 });
249
250 await waitFor("null arrays to render as empty", () => (rootEl.textContent || "").includes("No issues found"));
251 ok(rootEl.querySelector(".diag-summary"), "null arrays must not crash the diagnostics page");
252
253 await act(async () => {
254 root.unmount();
255 });
256 }
257
258 console.log("diagnostics-settings: ok");
259
259 lines Plain Text