返回 DeepSeek-Reasonix
hostCalls.test.ts
根目录 / desktop / electron / src / main / browser / hostCalls.test.ts
1 import assert from "node:assert/strict";
2 import { test } from "node:test";
3 import { RpcError } from "../rpc.js";
4 import { ActionExecutor } from "./actions.js";
5 import { DocumentRegistry } from "./documents.js";
6 import { DownloadTracker } from "./downloads.js";
7 import { BROWSER_ERR_NO_GRANT, BROWSER_ERR_STALE_REFERENCE, BROWSER_ERR_TAKEN_OVER } from "./errors.js";
8 import { FakeViewFactory, silentLog } from "./fakeGuestViews.js";
9 import { GrantRegistry } from "./grants.js";
10 import { buildBrowserHostCalls, type HostBrowserTab } from "./hostCalls.js";
11 import { BrowserSurfaceManager } from "./surfaceManager.js";
12
13 const code = (value: number) => (error: unknown) => error instanceof RpcError && error.code === value;
14
15 async function setup() {
16 const factory = new FakeViewFactory();
17 const surfaces = new BrowserSurfaceManager({ views: factory, contentSize: () => null, onTakeover() {}, onCrash() {}, log: silentLog, openWaitMs: 5 });
18 const grants = new GrantRegistry({ generation: () => "gen-1" });
19 let tokens = 0;
20 const documents = new DocumentRegistry(() => `tok-${++tokens}`);
21 const actions = new ActionExecutor({ surfaces, documents });
22 const directories = new Map<string, string>();
23 const downloads = new DownloadTracker({
24 tabForWebContents: () => undefined,
25 defaultDirectory: (taskId) => `/dl/${taskId}`,
26 onUpdate() {},
27 log: silentLog,
28 });
29 const table = buildBrowserHostCalls({
30 surfaces,
31 grants,
32 documents,
33 actions,
34 downloads,
35 snapshot: async (tab, selector) => ({ tabId: tab.id, selector }) as never,
36 screenshot: async (tab, request) => ({ tabId: tab.id, ...request }) as never,
37 });
38 const call = async (method: keyof typeof table, params: Record<string, unknown> = {}): Promise<unknown> => table[method](params);
39 const setDirs = downloads.setTaskDirectory.bind(downloads);
40 downloads.setTaskDirectory = (taskId, directory) => {
41 directories.set(taskId, directory);
42 setDirs(taskId, directory);
43 };
44 return { surfaces, grants, documents, downloads, directories, table, call };
45 }
46
47 test("grant, list and open are scoped to the grant's task", async () => {
48 const s = await setup();
49 await assert.rejects(s.call("host/browser.tabs.list", { grantId: "g" }), code(BROWSER_ERR_NO_GRANT));
50 await s.call("host/browser.grant", { grantId: "g", tabId: "task-1", sessionId: "s1" });
51 await s.call("host/browser.grant", { grantId: "h", tabId: "task-2", sessionId: "s2" });
52
53 const opened = (await s.call("host/browser.tabs.open", { grantId: "g", url: "https://a.test" })) as HostBrowserTab;
54 assert.equal(opened.url, "https://a.test/");
55 assert.equal(s.surfaces.require(opened.id).taskId, "task-1", "the tab belongs to the grant's task");
56 await s.surfaces.open("https://b.test", { taskId: "task-2", temporary: false });
57
58 const listed = (await s.call("host/browser.tabs.list", { grantId: "g" })) as { tabs: HostBrowserTab[] };
59 assert.deepEqual(listed.tabs.map((tab) => tab.id), [opened.id], "another task's tabs are invisible");
60 });
61
62 test("tab calls verify the grant and refuse tabs of another task", async () => {
63 const s = await setup();
64 await s.call("host/browser.grant", { grantId: "g", tabId: "task-1", sessionId: "" });
65 const other = await s.surfaces.open("https://b.test", { taskId: "task-2", temporary: false });
66 await assert.rejects(s.call("host/browser.tabs.navigate", { grantId: "g", tabId: other.id, url: "https://c.test" }), code(BROWSER_ERR_NO_GRANT));
67 await assert.rejects(s.call("host/browser.tabs.close", { grantId: "g", tabId: other.id }), code(BROWSER_ERR_NO_GRANT));
68 await assert.rejects(s.call("host/browser.tabs.navigate", { grantId: "g", tabId: "tab-99", url: "https://c.test" }), code(BROWSER_ERR_NO_GRANT));
69 });
70
71 test("reads and writes refuse a tab the user has taken over", async () => {
72 const s = await setup();
73 await s.call("host/browser.grant", { grantId: "g", tabId: "task-1", sessionId: "" });
74 const tab = await s.surfaces.open("https://a.test", { taskId: "task-1", temporary: false });
75 assert.deepEqual(await s.call("host/browser.snapshot", { grantId: "g", tabId: tab.id, selector: "" }), { tabId: tab.id, selector: "" });
76 s.surfaces.takeover(tab.id, "user mousedown");
77 await assert.rejects(s.call("host/browser.snapshot", { grantId: "g", tabId: tab.id }), code(BROWSER_ERR_TAKEN_OVER));
78 await assert.rejects(s.call("host/browser.tabs.navigate", { grantId: "g", tabId: tab.id, url: "https://c.test" }), code(BROWSER_ERR_TAKEN_OVER));
79 await assert.rejects(s.call("host/browser.screenshot", { grantId: "g", tabId: tab.id, ref: "", directory: "" }), code(BROWSER_ERR_TAKEN_OVER));
80 const closed = await s.call("host/browser.tabs.close", { grantId: "g", tabId: tab.id });
81 assert.deepEqual(closed, {}, "closing is still allowed so the task can clean up");
82 assert.equal(s.surfaces.get(tab.id), undefined);
83 });
84
85 test("act re-verifies the grant and rejects a stale document token", async () => {
86 const s = await setup();
87 await s.call("host/browser.grant", { grantId: "g", tabId: "task-1", sessionId: "" });
88 const tab = await s.surfaces.open("https://a.test", { taskId: "task-1", temporary: false });
89 await assert.rejects(
90 s.call("host/browser.act", { grantId: "g", tabId: tab.id, documentToken: "nope", action: "click", ref: "e1" }),
91 code(BROWSER_ERR_STALE_REFERENCE),
92 );
93 await s.call("host/browser.revoke", { grantId: "g" });
94 await assert.rejects(
95 s.call("host/browser.act", { grantId: "g", tabId: tab.id, documentToken: "nope", action: "click", ref: "e1" }),
96 code(BROWSER_ERR_NO_GRANT),
97 );
98 });
99
100 test("act and screenshot register the scratch directory for downloads", async () => {
101 const s = await setup();
102 await s.call("host/browser.grant", { grantId: "g", tabId: "task-1", sessionId: "" });
103 const tab = await s.surfaces.open("https://a.test", { taskId: "task-1", temporary: false });
104 await s.call("host/browser.screenshot", { grantId: "g", tabId: tab.id, ref: "", directory: "/scratch/one" });
105 assert.equal(s.directories.get("task-1"), "/scratch/one");
106 await s.call("host/browser.screenshot", { grantId: "g", tabId: tab.id, ref: "", directory: "" });
107 assert.equal(s.directories.get("task-1"), "/scratch/one", "an empty directory keeps the previous mapping");
108 });
109
110 test("downloads waits are served per tab and a zero wait answers at once", async () => {
111 const s = await setup();
112 await s.call("host/browser.grant", { grantId: "g", tabId: "task-1", sessionId: "" });
113 const tab = await s.surfaces.open("https://a.test", { taskId: "task-1", temporary: false });
114 assert.deepEqual(await s.call("host/browser.downloads", { grantId: "g", tabId: tab.id, waitForMs: 0 }), { downloads: [] });
115 });
116
117 test("revoke invalidates the task's document tokens", async () => {
118 const s = await setup();
119 await s.call("host/browser.grant", { grantId: "g", tabId: "task-1", sessionId: "" });
120 const tab = await s.surfaces.open("https://a.test", { taskId: "task-1", temporary: false });
121 const token = s.documents.issue({ tabId: tab.id, epoch: tab.epoch, snapshotId: "snap", frames: [] });
122 assert.ok(s.documents.lookup(token));
123 await s.call("host/browser.revoke", { grantId: "g" });
124 assert.equal(s.documents.lookup(token), undefined);
125 });
126
126 lines TYPESCRIPT