返回 DeepSeek-Reasonix
ipc.test.ts
根目录 / desktop / electron / src / main / ipc.test.ts
1 import assert from "node:assert/strict";
2 import type { IpcMain } from "electron";
3 import { test } from "node:test";
4 import { IPC, type IpcResult } from "../shared/ipc.js";
5 import { parseContract } from "./contract.js";
6 import { isOpenableExternalURL, parseRendererDiagnostic, registerRendererIpc } from "./ipc.js";
7 import { RpcError } from "./rpc.js";
8
9 function fakeIpc() {
10 const handlers = new Map<string, (event: unknown, ...args: unknown[]) => Promise<IpcResult>>();
11 const listeners = new Map<string, (event: { sender: unknown; senderFrame: unknown; returnValue?: unknown }) => void>();
12 const ipcMain = {
13 handle: (channel: string, fn: (event: unknown, ...args: unknown[]) => Promise<IpcResult>) => handlers.set(channel, fn),
14 on: (channel: string, fn: (event: { sender: unknown; senderFrame: unknown; returnValue?: unknown }) => void) => listeners.set(channel, fn),
15 } as unknown as IpcMain;
16 return { ipcMain, handlers, listeners };
17 }
18
19 test("external URLs are limited to http, https and mailto", () => {
20 assert.equal(isOpenableExternalURL("https://example.com/x"), true);
21 assert.equal(isOpenableExternalURL("http://example.com"), true);
22 assert.equal(isOpenableExternalURL("mailto:a@b.c"), true);
23 assert.equal(isOpenableExternalURL("file:///etc/passwd"), false);
24 assert.equal(isOpenableExternalURL("javascript:alert(1)"), false);
25 assert.equal(isOpenableExternalURL("reasonix://app/"), false);
26 assert.equal(isOpenableExternalURL("not a url"), false);
27 assert.equal(isOpenableExternalURL(42), false);
28 });
29
30 test("renderer transcript diagnostics accept only the bounded stable schema", () => {
31 const valid = {
32 kind: "transcript", event: "failure", stage: "delta_validate", reason: "revision_gap", transport: "local", errorType: "classified",
33 revision: 12, commit: 8, attempts: 1, failures: 2, durationMs: 1500,
34 };
35 assert.deepEqual(parseRendererDiagnostic(valid), valid);
36 assert.throws(() => parseRendererDiagnostic({ ...valid, reason: "raw server response" }), /invalid renderer diagnostic value/);
37 assert.throws(() => parseRendererDiagnostic({ ...valid, prompt: "secret" }), /invalid renderer diagnostic field/);
38 assert.throws(() => parseRendererDiagnostic({ ...valid, failures: -1 }), /invalid renderer diagnostic failures/);
39 assert.throws(() => parseRendererDiagnostic({ ...valid, prompt: "x".repeat(2100) }), /payload too large/);
40 });
41
42 test("renderer invokes are gated by sender identity and the contract allowlist", async (t) => {
43 let now = 1_000;
44 t.mock.method(Date, "now", () => now);
45 const { ipcMain, handlers, listeners } = fakeIpc();
46 const trustedSender = { id: 1 };
47 const trustedFrame = {};
48 const invoked: Array<{ method: string; args: unknown[] }> = [];
49 const nativeCalls: string[] = [];
50 const logMessages: string[] = [];
51 const warningMessages: string[] = [];
52 let diagnosticReads = 0;
53 const performanceCalls: string[] = [];
54 registerRendererIpc({
55 ipcMain,
56 contract: parseContract({ digest: "sha256:a", commands: ["OpenProjectTab", "MinimiseMainWindow", "ToggleMaximiseMainWindow", "IsMainWindowMaximised", "CloseMainWindow"] }),
57 window: {
58 isTrustedSender: (sender, frame) => sender === trustedSender && frame === trustedFrame,
59 minimise() { nativeCalls.push("minimise"); },
60 toggleMaximise() { nativeCalls.push("toggle"); },
61 isMaximised: () => true,
62 close() { nativeCalls.push("close"); },
63 bounds: () => ({ x: 1, y: 2, width: 3, height: 4, maximised: false }),
64 setTheme() {},
65 setBackgroundColour() {},
66 getAppZoom: async () => 1,
67 setAppZoom: async () => 1,
68 resetAppZoom: async () => 1,
69 },
70 serviceState: () => ({ phase: "ready" as const, generation: "g-test" }),
71 processDiagnostics: () => { diagnosticReads++; return { scope: "electron", samples: [] }; },
72 performance: {
73 captureRendererProfile: async () => { performanceCalls.push("capture"); return { status: "captured" }; },
74 cancelRendererProfile: () => { performanceCalls.push("cancel"); },
75 exportHeapSnapshot: async () => { performanceCalls.push("heap"); return { status: "cancelled" }; },
76 dispose() {},
77 },
78 invoke: async (method, args) => {
79 invoked.push({ method, args });
80 if (method === "OpenProjectTab" && args[0] === "/missing") throw new Error("workspace not found");
81 if (args[0] === "/pending-settings") throw new RpcError(-32000,"settings pending",{submissionOutcome:"not_accepted",modelApplication:{code:"model_settings_pending"}});
82 return { opened: args[0] };
83 },
84 clipboard: { writeText: async () => undefined, readText: async () => "clip" },
85 openExternal: async () => undefined,
86 log: { info: (message) => logMessages.push(message), warn: (message) => warningMessages.push(message), error() {} },
87 });
88 const invoke = handlers.get(IPC.invoke);
89 assert.ok(invoke);
90 const trusted = { sender: trustedSender, senderFrame: trustedFrame };
91 const diagnostics = handlers.get(IPC.processDiagnostics)!;
92 assert.deepEqual(await diagnostics({ sender: trustedSender, senderFrame: {} }), { ok: false, message: "untrusted sender" });
93 assert.equal(diagnosticReads, 0);
94 assert.deepEqual(await diagnostics(trusted), { ok: true, value: { scope: "electron", samples: [] } });
95 assert.equal(diagnosticReads, 1);
96 for (const channel of [IPC.captureRendererProfile, IPC.cancelRendererProfile, IPC.exportHeapSnapshot]) {
97 assert.deepEqual(await handlers.get(channel)!({ sender: trustedSender, senderFrame: {} }), { ok: false, message: "untrusted sender" });
98 }
99 assert.deepEqual(performanceCalls, []);
100 await handlers.get(IPC.cancelRendererProfile)!(trusted);
101 assert.deepEqual(performanceCalls, [], "unscoped IPC cancellation is ignored");
102 for (const channel of [IPC.captureRendererProfile, IPC.cancelRendererProfile, IPC.exportHeapSnapshot]) await handlers.get(channel)!(trusted, "test-request");
103 assert.deepEqual(performanceCalls, ["capture", "cancel", "heap"]);
104 assert.deepEqual(await invoke({ sender: { id: 9 }, senderFrame: trustedFrame }, "OpenProjectTab", ["/p"]), { ok: false, message: "untrusted sender" });
105 assert.deepEqual(await invoke({ sender: trustedSender, senderFrame: {} }, "OpenProjectTab", ["/p"]), { ok: false, message: "untrusted sender" });
106 assert.deepEqual(await invoke(trusted, "OpenProjectTab", ["/p"]), { ok: true, value: { opened: "/p" } });
107 assert.deepEqual(await invoke(trusted, "OpenProjectTab", ["/missing"]), { ok: false, message: "workspace not found" });
108 assert.deepEqual(await invoke(trusted,"OpenProjectTab",["/pending-settings"]),{ok:false,message:"settings pending",code:-32000,data:{submissionOutcome:"not_accepted",modelApplication:{code:"model_settings_pending"}}});
109 assert.deepEqual(await invoke(trusted, "MinimiseMainWindow", []), { ok: true, value: undefined });
110 assert.deepEqual(await invoke(trusted, "ToggleMaximiseMainWindow", []), { ok: true, value: undefined });
111 assert.deepEqual(await invoke(trusted, "IsMainWindowMaximised", []), { ok: true, value: true });
112 assert.deepEqual(await invoke(trusted, "CloseMainWindow", []), { ok: true, value: undefined });
113 assert.deepEqual(nativeCalls, ["minimise", "toggle", "close"]);
114 assert.deepEqual(await invoke(trusted, "DeleteEverything", []), { ok: false, message: "-32601 method not found: DeleteEverything",code:-32601,data:undefined });
115 assert.deepEqual(await invoke(trusted, "__proto__", []), { ok: false, message: "-32601 method not found: __proto__",code:-32601,data:undefined });
116 assert.deepEqual(invoked.map((call) => call.method), ["OpenProjectTab", "OpenProjectTab", "OpenProjectTab"]);
117
118 const contract = listeners.get(IPC.contract);
119 assert.ok(contract);
120 const event: { sender: unknown; senderFrame: unknown; returnValue?: unknown } = { ...trusted };
121 contract(event);
122 assert.deepEqual(event.returnValue, {
123 protocolVersion: 1,
124 digest: "sha256:a",
125 commands: ["OpenProjectTab", "MinimiseMainWindow", "ToggleMaximiseMainWindow", "IsMainWindowMaximised", "CloseMainWindow"],
126 });
127 const untrusted: { sender: unknown; senderFrame: unknown; returnValue?: unknown } = { sender: { id: 9 }, senderFrame: trustedFrame };
128 contract(untrusted);
129 assert.equal(untrusted.returnValue, null);
130
131 const open = handlers.get(IPC.openExternal);
132 assert.ok(open);
133 assert.deepEqual(await open(trusted, "file:///etc/passwd"), { ok: false, message: "refusing to open file:///etc/passwd" });
134 assert.deepEqual(await open(trusted, "https://example.com"), { ok: true, value: undefined });
135 const bounds = handlers.get(IPC.windowGetBounds);
136 assert.ok(bounds);
137 assert.deepEqual(await bounds(trusted), { ok: true, value: { x: 1, y: 2, width: 3, height: 4, maximised: false } });
138 const read = handlers.get(IPC.clipboardRead);
139 assert.ok(read);
140 assert.deepEqual(await read(trusted), { ok: true, value: "clip" });
141
142 const rendererDiagnostic = handlers.get(IPC.rendererDiagnostic)!;
143 const diagnostic = {
144 kind: "transcript", event: "failure", stage: "delta_validate", reason: "revision_gap", transport: "local", errorType: "classified",
145 revision: 12, commit: 8, attempts: 1, failures: 2, durationMs: 1500,
146 };
147 assert.deepEqual(await rendererDiagnostic({ sender: { id: 9 }, senderFrame: trustedFrame }, diagnostic), { ok: false, message: "untrusted sender" });
148 assert.deepEqual(await rendererDiagnostic(trusted, diagnostic), { ok: true, value: undefined });
149 assert.match(logMessages.at(-1) ?? "", /stage=delta_validate reason=revision_gap type=classified.*service=ready generation=g-test/);
150 assert.deepEqual(await rendererDiagnostic(trusted, { ...diagnostic, response: "raw" }), { ok: false, message: "invalid renderer diagnostic field" });
151 for (let index = 0; index < 10; index++) await rendererDiagnostic(trusted, diagnostic);
152 assert.equal(logMessages.length, 10, "only ten renderer diagnostics are accepted in one second");
153 now = 2_000;
154 await rendererDiagnostic(trusted, diagnostic);
155 assert.ok(warningMessages.some(message => message === "renderer diagnostics rate limited dropped=1"));
156 assert.equal(logMessages.length, 11, "the next rate window accepts diagnostics again");
157 });
158
158 lines TYPESCRIPT