import { Agent } from "@opencode-ai/core/agent" import { BrowserHost } from "@opencode-ai/core/browser-host" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Image } from "@opencode-ai/core/image" import { Permission } from "@opencode-ai/core/permission" import { Session } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" import { Tool } from "@opencode-ai/core/tool" import { BrowserTool } from "@opencode-ai/core/tool/browser" import { Browser } from "@opencode-ai/schema/browser" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { describe, expect } from "bun:test" import { Effect, Fiber, Layer } from "effect" import { testEffect } from "./lib/effect" import { imagePassthrough } from "./lib/image" const sessionID = Session.ID.make("ses_browser_tools") const state: Browser.State = { url: "https://example.com/path", title: "spoof", loading: false, canGoBack: false, canGoForward: false, generation: 4, } const assertions: Permission.AssertInput[] = [] let opens = 0 const layer = AppNodeBuilder.build(LayerNode.group([Tool.node, BrowserTool.node, BrowserHost.node]), [ [BrowserHost.node, Layer.effect(BrowserHost.Service, BrowserHost.make(() => Effect.succeed(true)))], [ Permission.node, Layer.mock(Permission.Service, { assert: (input) => Effect.sync(() => assertions.push(input)), }), ], [Image.node, imagePassthrough], ]) const it = testEffect(layer) const execute = (snapshot: Tool.Snapshot, name: string) => snapshot .execute({ sessionID, agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_browser_tools"), call: { type: "tool-call", id: `call-${name}`, name, input: {} }, }) .pipe(Effect.map((result) => ({ status: "completed" as const, ...result }))) const browserNames = (snapshot: Tool.Snapshot) => snapshot.definitions.map((definition) => definition.name).filter((name) => name.startsWith("browser_")) describe("BrowserTool", () => { it.effect("moves from open to attached tools and returns a trusted screenshot boundary", () => Effect.gen(function* () { assertions.length = 0 opens = 0 const browser = yield* BrowserHost.Service const tools = yield* Tool.Service const controller = yield* browser.register(sessionID, { open: Effect.sync(() => opens++), request: (command) => { if (command.type !== "screenshot") { return Effect.fail( new BrowserHost.RequestError({ code: "protocol", message: "Expected screenshot command." }), ) } return Effect.succeed({ type: "screenshot" as const, state, mediaType: "image/png" as const, data: new Uint8Array([1, 2, 3]), width: 800, height: 600, }) }, }) const available = yield* tools.snapshot(undefined, sessionID) expect(browserNames(available)).toEqual(["browser_open"]) expect(available.definitions[0]?.description).toBe( "Request the owning client to open the visual browser pane for this Session. browser_navigate, browser_snapshot, browser_click, browser_fill, browser_press, browser_scroll, browser_screenshot become available on the next agent step after the browser attaches.", ) const opening = yield* execute(available, "browser_open").pipe(Effect.forkChild) while (!opens) yield* Effect.yieldNow yield* controller.attach(Browser.LeaseID.make("brl_browsertools"), state) expect((yield* Fiber.join(opening)).status).toBe("completed") const attached = yield* tools.snapshot(undefined, sessionID) expect(browserNames(attached)).toEqual(BrowserTool.names.filter((name) => name !== "browser_open").sort()) const result = yield* execute(attached, "browser_screenshot") expect(result).toMatchObject({ status: "completed", content: [ { type: "text", text: expect.stringContaining("\\u003c/untrusted_browser_state\\u003e") }, { type: "file", uri: "data:image/png;base64,AQID", mime: "image/png", name: "browser-screenshot.png", }, ], metadata: { url: state.url, width: 800, height: 600 }, }) expect(assertions).toEqual([ expect.objectContaining({ action: "browser_read", resources: [state.url], save: ["https://example.com/*"], sessionID, source: { type: "tool", messageID: "msg_browser_tools", callID: "call-browser_screenshot" }, }), ]) }), ) })