返回 DeepSeek-Reasonix
diagnosticExport.test.ts
根目录 / desktop / electron / src / main / browser / diagnosticExport.test.ts
1 import assert from "node:assert/strict";
2 import { test } from "node:test";
3 import { BrowserDiagnosticExport } from "./diagnosticExport.js";
4 import { DiagnosticBuffer } from "./diagnostics.js";
5 import { FakeViewFactory, silentLog } from "./fakeGuestViews.js";
6 import { GrantRegistry } from "./grants.js";
7 import { BrowserSurfaceManager } from "./surfaceManager.js";
8 import { buildBrowserHostCalls } from "./hostCalls.js";
9 import { ActionExecutor } from "./actions.js";
10 import { DocumentRegistry } from "./documents.js";
11 import { DownloadTracker } from "./downloads.js";
12 import { acquireDebugger, disposeDebugger } from "./debuggerLease.js";
13
14 const a = "a".repeat(64), b = "b".repeat(64);
15 function setup(snapshot?: (tab: import("./surfaceManager.js").BrowserTab) => Promise<never>) {
16 const factory = new FakeViewFactory();
17 const surfaces = new BrowserSurfaceManager({ views: factory, contentSize: () => null, onTakeover() {}, onCrash() {}, log: silentLog });
18 const grants = new GrantRegistry({ generation: () => "generation" });
19 const diagnostics = new BrowserDiagnosticExport(surfaces, grants, { build: "test-build", version: "test", platform: "test" });
20 const documents = new DocumentRegistry();
21 const calls = buildBrowserHostCalls({ surfaces, grants, diagnosticExport: diagnostics, documents, actions: new ActionExecutor({ surfaces, documents }), downloads: new DownloadTracker({ tabForWebContents: () => undefined, defaultDirectory: () => "/tmp", onUpdate() {}, log: silentLog }), snapshot });
22 return { factory, surfaces, grants, diagnostics, calls };
23 }
24
25 test("export retains scoped closed-tab events, sanitizes pages and releases listeners", async () => {
26 const s = setup();
27 await s.calls["host/browser.grant"]({ grantId: "grant-a", tabId: "task-a", sessionId: "session", diagnosticScope: a });
28 await s.calls["host/browser.grant"]({ grantId: "grant-b", tabId: "task-b", sessionId: "session", diagnosticScope: b });
29 const tab = await s.surfaces.open("https://example.test/?token=SECRET", { taskId: "task-a", sessionId: "session", temporary: true });
30 const other = await s.surfaces.open("https://other.test", { taskId: "task-b", sessionId: "session", temporary: false });
31 const page = tab.view.diagnostics = new DiagnosticBuffer();
32 const otherPage = other.view.diagnostics = new DiagnosticBuffer();
33 s.diagnostics.read(a); // Attach newly materialized page buffers.
34 page.add({ kind: "console", message: "token=SECRET cookie: SECRET", url: "https://user:SECRET@example.test/path?token=SECRET#SECRET" });
35 otherPage.add({ kind: "exception", message: "FOREIGN-PAGE" });
36 s.surfaces.takeover(tab.id, "user");
37 s.grants.revoke("grant-a");
38 s.surfaces.close(tab.id);
39 page.add({ kind: "console", message: "LATE-CLOSED-PAGE" });
40 const result = s.diagnostics.read(a);
41 const data = JSON.stringify(result);
42 for (const text of ["SECRET", "grant-a", "FOREIGN-PAGE", "LATE-CLOSED-PAGE"]) assert.equal(data.includes(text), false, text);
43 assert.ok(result.entries.some(row => row.event === "tab_closed"));
44 assert.ok(result.entries.some(row => row.event === "grant_revoked"));
45 assert.ok(result.entries.some(row => row.mode === "human"));
46 assert.ok(result.entries.some(row => row.kind === "page"));
47 assert.equal(result.tabs.length, 0);
48 s.diagnostics.dispose();
49 const restarted = new BrowserDiagnosticExport(s.surfaces, s.grants, { build: "new-build", version: "test", platform: "test" });
50 assert.equal(restarted.read(a).entries.length, 0, "unattributed history must not be guessed after restart");
51 restarted.dispose();
52 s.surfaces.destroyAll();
53 });
54
55 test("queued requests retain CDP request and operation identity without arguments", async () => {
56 const releases: (() => void)[] = [];
57 let enteredSecond!: () => void;
58 const secondStarted = new Promise<void>(resolve => { enteredSecond = resolve; });
59 const s = setup(async tab => {
60 const release = acquireDebugger(tab.view.page.debugger);
61 try { await release.send("Page.createIsolatedWorld", { expression: "PRIVATE-PAGE" }, "private-session"); throw new Error("script failure token=SECRET"); }
62 finally { release(); disposeDebugger(tab.view.page.debugger); }
63 });
64 for (const [scope, task] of [[a, "a"], [b, "b"]]) {
65 await s.calls["host/browser.grant"]({ grantId: task, tabId: task, sessionId: "session", diagnosticScope: scope });
66 const tab = await s.surfaces.open("https://example.test", { taskId: task, sessionId: "session", temporary: false });
67 s.factory.views.at(-1)!.page.debugger.respond = () => new Promise(resolve => { releases.push(() => resolve({})); if (releases.length === 2) enteredSecond(); });
68 // Start both requests before delivering either CDP response.
69 const pending = s.calls["host/browser.snapshot"]({ grantId: task, tabId: tab.id, requestId: `request-${task}`, operationId: `operation-${task}` });
70 (tab as typeof tab & { pending: Promise<unknown> }).pending = Promise.resolve(pending).then(() => assert.fail("expected failure"), () => {});
71 }
72 assert.equal(releases.length, 1, "shared capture surface must serialize requests");
73 releases[0]();
74 await secondStarted;
75 releases[1]();
76 await Promise.all(s.surfaces.all().map(tab => (tab as typeof tab & { pending: Promise<unknown> }).pending));
77 for (const [scope, task] of [[a, "a"], [b, "b"]]) {
78 const result = s.diagnostics.read(scope);
79 const commands = result.entries.filter(row => row.command);
80 assert.deepEqual(commands.map(row => row.phase), ["cdp_started", "cdp_completed"]);
81 assert.ok(commands.every(row => row.requestId === `request-${task}` && row.operationId === `operation-${task}` && row.target === "child"));
82 assert.ok(result.entries.some(row => row.phase === "failed" && row.requestId === `request-${task}`));
83 assert.equal(JSON.stringify(result).includes("PRIVATE-PAGE"), false);
84 assert.equal(JSON.stringify(result).includes("SECRET"), false);
85 }
86 s.surfaces.destroyAll(); s.diagnostics.dispose();
87 });
88
89 test("export has honest global eviction and no page credentials in bounded payload", () => {
90 const s = setup();
91 for (let i = 0; i < 700; i++) s.diagnostics.trace(a, { requestId: `r-${i}`, method: "host/browser.snapshot", phase: "reading", elapsedMs: i });
92 let result = s.diagnostics.read(a);
93 assert.equal(result.truncated, true); assert.equal(result.entries.length, 512);
94 assert.ok(Buffer.byteLength(JSON.stringify(result.entries)) <= 256 * 1024);
95 assert.equal(s.diagnostics.read(b).entries.length, 0);
96 s.diagnostics.dispose();
97 });
98
99 test("live grant attribution survives more than 256 bindings and page reopen", async () => {
100 const s = setup();
101 s.diagnostics.bind(s.grants.install({ grantId: "first", taskId: "first", sessionId: "s", diagnosticScope: a }));
102 const closed = await s.surfaces.open("https://example.test", { taskId: "first", sessionId: "s", temporary: false });
103 s.surfaces.close(closed.id);
104 for (let i = 0; i < 256; i++) s.diagnostics.bind(s.grants.install({ grantId: `g-${i}`, taskId: `task-${i}`, sessionId: "s", diagnosticScope: b }));
105 assert.equal(s.grants.verify("first").diagnosticScope, a);
106 const tab = await s.surfaces.open("https://example.test", { taskId: "first", sessionId: "s", temporary: false });
107 const buffer = tab.view.diagnostics = new DiagnosticBuffer();
108 s.diagnostics.read(a);
109 buffer.add({ kind: "exception", message: "retained after 257 grants" });
110 const result = s.diagnostics.read(a);
111 assert.equal(result.truncated, false);
112 assert.ok(result.entries.some(row => row.kind === "page" && row.message === "retained after 257 grants"));
113 assert.equal(result.tabs[0]?.pageDiagnosticsAvailable, true);
114 assert.equal(s.diagnostics.read(b).entries.some(row => row.tabId === tab.id), false);
115 s.grants.revoke("first");
116 buffer.add({ kind: "console", message: "existing page remains attributable after revoke" });
117 assert.ok(s.diagnostics.read(a).entries.some(row => row.message === "existing page remains attributable after revoke"));
118 s.surfaces.destroyAll(); s.diagnostics.dispose();
119 });
120
121 test("host export serializes sanitized console URLs for every protocol casing", async () => {
122 const s = setup();
123 await s.calls["host/browser.grant"]({ grantId: "a", tabId: "a", sessionId: "s", diagnosticScope: a });
124 const tab = await s.surfaces.open("https://example.test", { taskId: "a", sessionId: "s", temporary: false });
125 const buffer = tab.view.diagnostics = new DiagnosticBuffer();
126 s.diagnostics.read(a);
127 for (const scheme of ["https", "HTTPS", "HtTpS", "HTTP"]) {
128 buffer.add({ kind: "console", message: `Request failed ${scheme}://review-user:REVIEW-PASSWORD@example.test/callback?code=REVIEW-OAUTH-CODE&signature=REVIEW-SIGNATURE#REVIEW-FRAGMENT`, url: `${scheme}://example.test/source?code=REVIEW-OAUTH-CODE` });
129 }
130 const exported = await s.calls["host/browser.exportDiagnostics"]({ scope: a });
131 const json = JSON.stringify(exported);
132 for (const secret of ["review-user", "REVIEW-PASSWORD", "REVIEW-OAUTH-CODE", "REVIEW-SIGNATURE", "REVIEW-FRAGMENT"]) assert.equal(json.includes(secret), false, secret);
133 const entries = (exported as ReturnType<BrowserDiagnosticExport["read"]>).entries;
134 assert.deepEqual(entries.filter(row => row.kind === "page").map(row => ({ message: row.message, url: row.url })), [
135 ...Array.from({ length: 3 }, () => ({ message: "Request failed https://example.test/callback", url: "https://example.test/source" })),
136 { message: "Request failed http://example.test/callback", url: "http://example.test/source" },
137 ]);
138 s.surfaces.destroyAll(); s.diagnostics.dispose();
139 });
140
140 lines TYPESCRIPT