Skip to content

Commit 0a0fe0c

Browse files
mattzcareyMatt Carey
andauthored
Use real Workers resources in browser tools tests (#1811)
Co-authored-by: Matt Carey <matt@cloudflare.com>
1 parent 7f367d8 commit 0a0fe0c

4 files changed

Lines changed: 145 additions & 63 deletions

File tree

Lines changed: 31 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,59 @@
1+
import { env } from "cloudflare:workers";
12
import { describe, expect, it } from "vitest";
2-
import { createBrowserRuntime, createBrowserTools } from "../tools/browser";
3+
import type { BrowserToolsHost } from "./worker";
34

4-
// Shape-only tests: nothing here executes against the facet, storage, or the
5-
// browser binding — those paths are exercised by the agents browser e2e
6-
// suite. The facet stub satisfies the runtime's eager `ctx.facets.get`, and
7-
// the export mirrors a Worker entry that re-exports CodemodeRuntime.
8-
const fakeCtx = {
9-
storage: {},
10-
facets: { get: () => ({}) },
11-
exports: { CodemodeRuntime: class MockCodemodeRuntime {} }
12-
} as unknown as DurableObjectState;
5+
function host() {
6+
const id = env.BrowserToolsHost.idFromName("browser-tools");
7+
return env.BrowserToolsHost.get(id) as DurableObjectStub<BrowserToolsHost>;
8+
}
139

1410
describe("createBrowserTools", () => {
15-
it("returns browser_execute plus the default Quick Action tools when a binding is present", () => {
16-
const tools = createBrowserTools({
17-
ctx: fakeCtx,
18-
browser: {} as Fetcher,
19-
loader: {} as WorkerLoader
20-
});
11+
it("returns browser_execute plus the default Quick Action tools when a binding is present", async () => {
12+
const tools = await host().toolsWithBinding();
2113

22-
expect(Object.keys(tools).sort()).toEqual([
14+
expect(tools.keys).toEqual([
2315
"browser_execute",
2416
"browser_extract",
2517
"browser_links",
2618
"browser_markdown",
2719
"browser_scrape"
2820
]);
29-
expect(typeof tools.browser_execute.execute).toBe("function");
21+
expect(tools.hasExecute).toBe(true);
3022
// The runtime tool description lists the cdp connector namespace.
31-
expect(tools.browser_execute.description).toContain("`cdp`");
23+
expect(tools.description).toContain("`cdp`");
3224
});
3325

34-
it("omits Quick Action tools when quickActions is false", () => {
35-
const tools = createBrowserTools({
36-
ctx: fakeCtx,
37-
browser: {} as Fetcher,
38-
loader: {} as WorkerLoader,
39-
quickActions: false
26+
it("omits Quick Action tools when quickActions is false", async () => {
27+
await expect(host().toolsWithoutQuickActions()).resolves.toMatchObject({
28+
keys: ["browser_execute"]
4029
});
41-
42-
expect(Object.keys(tools)).toEqual(["browser_execute"]);
4330
});
4431

45-
it("accepts cdpUrl instead of browser binding (Quick Actions skipped without a binding)", () => {
46-
const tools = createBrowserTools({
47-
ctx: fakeCtx,
48-
cdpUrl: "http://localhost:9222",
49-
loader: {} as WorkerLoader
32+
it("accepts cdpUrl instead of browser binding (Quick Actions skipped without a binding)", async () => {
33+
await expect(host().toolsWithCdpUrl()).resolves.toMatchObject({
34+
keys: ["browser_execute"]
5035
});
51-
52-
expect(Object.keys(tools)).toEqual(["browser_execute"]);
5336
});
5437

55-
it("accepts optional timeout and session mode", () => {
56-
const tools = createBrowserTools({
57-
ctx: fakeCtx,
58-
browser: {} as Fetcher,
59-
loader: {} as WorkerLoader,
60-
timeout: 60_000,
61-
session: { mode: "dynamic" }
38+
it("accepts optional timeout and session mode", async () => {
39+
await expect(host().toolsWithOptions()).resolves.toMatchObject({
40+
keys: expect.arrayContaining(["browser_execute"])
6241
});
63-
64-
expect(tools).toHaveProperty("browser_execute");
6542
});
6643

67-
it("requires a browser binding or cdpUrl", () => {
68-
expect(() =>
69-
createBrowserTools({
70-
ctx: fakeCtx,
71-
loader: {} as WorkerLoader
72-
})
73-
).toThrow("must be provided");
44+
it("requires a browser binding or cdpUrl", async () => {
45+
await expect(host().missingBrowserOrCdpUrlError()).resolves.toContain(
46+
"must be provided"
47+
);
7448
});
7549

76-
it("exposes the runtime handle and connector via createBrowserRuntime", () => {
77-
const { runtime, connector, tools } = createBrowserRuntime({
78-
ctx: fakeCtx,
79-
browser: {} as Fetcher,
80-
loader: {} as WorkerLoader
50+
it("exposes the runtime handle and connector via createBrowserRuntime", async () => {
51+
await expect(host().runtimeShape()).resolves.toEqual({
52+
connectorName: "cdp",
53+
runtimeApprove: "function",
54+
runtimeExpirePaused: "function",
55+
connectorSweep: "function",
56+
hasExecute: true
8157
});
82-
83-
expect(connector.name()).toBe("cdp");
84-
expect(typeof runtime.approve).toBe("function");
85-
expect(typeof runtime.expirePaused).toBe("function");
86-
expect(typeof connector.sweep).toBe("function");
87-
expect(tools).toHaveProperty("browser_execute");
8858
});
8959
});

packages/think/src/tests/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"extends": "agents/tsconfig",
33
"compilerOptions": {
44
"types": [
5-
"@cloudflare/workers-types",
5+
"@cloudflare/workers-types/latest",
66
"@cloudflare/vitest-pool-workers/types"
77
]
88
},

packages/think/src/tests/worker.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
12
import { routeAgentRequest } from "agents";
3+
import { createBrowserRuntime, createBrowserTools } from "../tools/browser";
24

35
export { HostBridgeLoopback } from "../extensions";
46

@@ -75,6 +77,110 @@ import type {
7577
ThinkMediaEvictionAutoAgent
7678
} from "./agents";
7779

80+
type BrowserRunTestBinding = Fetcher & {
81+
quickAction(action: string, options: unknown): Promise<Response>;
82+
};
83+
84+
export class TestBrowserRunBinding extends WorkerEntrypoint<Env> {
85+
fetch(): Response {
86+
return Response.json({ ok: true });
87+
}
88+
89+
quickAction(action: string): Response {
90+
const result =
91+
action === "links" || action === "scrape" || action === "json" ? [] : "";
92+
return Response.json({ success: true, result });
93+
}
94+
}
95+
96+
function browserToolSummary(tools: Record<string, unknown>) {
97+
const execute = tools.browser_execute as
98+
| { execute?: unknown; description?: string }
99+
| undefined;
100+
return {
101+
keys: Object.keys(tools).sort(),
102+
hasExecute: typeof execute?.execute === "function",
103+
description: execute?.description ?? ""
104+
};
105+
}
106+
107+
export class BrowserToolsHost extends DurableObject<Env> {
108+
#browser(): BrowserRunTestBinding {
109+
return this.ctx.exports.TestBrowserRunBinding as BrowserRunTestBinding;
110+
}
111+
112+
toolsWithBinding() {
113+
return browserToolSummary(
114+
createBrowserTools({
115+
ctx: this.ctx,
116+
browser: this.#browser(),
117+
loader: this.env.LOADER
118+
})
119+
);
120+
}
121+
122+
toolsWithoutQuickActions() {
123+
return browserToolSummary(
124+
createBrowserTools({
125+
ctx: this.ctx,
126+
browser: this.#browser(),
127+
loader: this.env.LOADER,
128+
quickActions: false
129+
})
130+
);
131+
}
132+
133+
toolsWithCdpUrl() {
134+
return browserToolSummary(
135+
createBrowserTools({
136+
ctx: this.ctx,
137+
cdpUrl: "http://localhost:9222",
138+
loader: this.env.LOADER
139+
})
140+
);
141+
}
142+
143+
toolsWithOptions() {
144+
return browserToolSummary(
145+
createBrowserTools({
146+
ctx: this.ctx,
147+
browser: this.#browser(),
148+
loader: this.env.LOADER,
149+
timeout: 60_000,
150+
session: { mode: "dynamic" }
151+
})
152+
);
153+
}
154+
155+
missingBrowserOrCdpUrlError() {
156+
try {
157+
createBrowserTools({
158+
ctx: this.ctx,
159+
loader: this.env.LOADER
160+
});
161+
return "";
162+
} catch (error) {
163+
return error instanceof Error ? error.message : String(error);
164+
}
165+
}
166+
167+
runtimeShape() {
168+
const { runtime, connector, tools } = createBrowserRuntime({
169+
ctx: this.ctx,
170+
browser: this.#browser(),
171+
loader: this.env.LOADER
172+
});
173+
174+
return {
175+
connectorName: connector.name(),
176+
runtimeApprove: typeof runtime.approve,
177+
runtimeExpirePaused: typeof runtime.expirePaused,
178+
connectorSweep: typeof connector.sweep,
179+
hasExecute: "browser_execute" in tools
180+
};
181+
}
182+
}
183+
78184
export type Env = {
79185
TestAssistantToolsAgent: DurableObjectNamespace<TestAssistantToolsAgent>;
80186
TestAssistantAgentAgent: DurableObjectNamespace<TestAssistantAgentAgent>;
@@ -108,6 +214,7 @@ export type Env = {
108214
ThinkWindowedHydrationAgent: DurableObjectNamespace<ThinkWindowedHydrationAgent>;
109215
ThinkMediaEvictionAgent: DurableObjectNamespace<ThinkMediaEvictionAgent>;
110216
ThinkMediaEvictionAutoAgent: DurableObjectNamespace<ThinkMediaEvictionAutoAgent>;
217+
BrowserToolsHost: DurableObjectNamespace<BrowserToolsHost>;
111218
LOADER: WorkerLoader;
112219
};
113220

packages/think/src/tests/wrangler.jsonc

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@
139139
"class_name": "ThinkMediaEvictionAutoAgent",
140140
"name": "ThinkMediaEvictionAutoAgent"
141141
},
142+
{
143+
"class_name": "BrowserToolsHost",
144+
"name": "BrowserToolsHost"
145+
},
142146
// Facet-only class. vitest-pool-workers needs it listed as a binding so
143147
// a facet-compatible class value resolves; it stays out of
144148
// new_sqlite_classes below (the host agent owns the SQLite store).
@@ -184,7 +188,8 @@
184188
"ThinkOnStartHydrationFailureAgent",
185189
"ThinkWindowedHydrationAgent",
186190
"ThinkMediaEvictionAgent",
187-
"ThinkMediaEvictionAutoAgent"
191+
"ThinkMediaEvictionAutoAgent",
192+
"BrowserToolsHost"
188193
],
189194
"tag": "v1"
190195
}

0 commit comments

Comments
 (0)