Files
openclaw/extensions/browser/src/attached-browser-tool-runtime.test.ts
Peter Steinberger b5d5ec340f feat(cloud-workers): add desktop apps and browser autonomy (#121475)
* feat(cloud-workers): add desktop apps and browser autonomy

provider-attested Browser/Terminal launchers, shared visible loopback CDP Browser tool, no MCP/cookies/generic command.

* feat(ui): add cloud desktop app launcher

* docs(gateway): document cloud desktop apps and browser autonomy

* perf(ui): trim desktop launcher startup copy

* refactor(ui): simplify desktop launch feedback

* perf(ui): reuse desktop app labels

* fix(ui): keep desktop launch failures actionable

* fix(crabbox): allow browser bootstrap to finish

* fix(cloud-workers): honor provider provision budgets

* fix(cloud-workers): persist browser screenshot receipts

* fix(cloud-workers): bound browser screenshot lifecycle

* fix(cloud-workers): avoid replaying desktop launches

* test(cloud-workers): isolate browser runtime integration

* refactor(cloud-workers): tighten desktop runtime boundaries

* test(cloud-workers): keep browser runtime mock synchronous

* fix(cloud-workers): break gateway type import cycle

* fix(ci): settle admitted setup sessions in tests

* build(plugin-sdk): refresh desktop app contract

* ci: refresh merge-tree validation

* build(plugin-sdk): regenerate desktop app baseline

* style(gateway): format merged method order test
2026-08-10 20:31:07 -07:00

142 lines
5.3 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
createBrowserTool: vi.fn(),
startBrowserBridgeServer: vi.fn(),
stopBrowserBridgeServer: vi.fn(),
}));
vi.mock("./browser-tool.js", () => ({
createBrowserTool: mocks.createBrowserTool,
}));
vi.mock("./browser/bridge-server.js", () => ({
startBrowserBridgeServer: mocks.startBrowserBridgeServer,
stopBrowserBridgeServer: mocks.stopBrowserBridgeServer,
}));
import { createAttachedBrowserToolRuntime } from "./attached-browser-tool-runtime.js";
describe("attached Browser tool runtime", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.createBrowserTool.mockReturnValue({ name: "browser" });
mocks.startBrowserBridgeServer.mockResolvedValue({
baseUrl: "http://127.0.0.1:18443",
server: { marker: "bridge-server" },
});
mocks.stopBrowserBridgeServer.mockResolvedValue(undefined);
});
it("exposes only one raw attach-only CDP profile through an authenticated loopback bridge", async () => {
const ensureAttachTarget = vi.fn().mockResolvedValue(undefined);
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "attached-browser-workspace-"));
const runtime = await createAttachedBrowserToolRuntime({
cdpUrl: "http://127.0.0.1:9222",
ensureAttachTarget,
agentSessionKey: "worker:session-1",
agentDir: "/tmp/worker-state",
workspaceDir,
});
expect(mocks.startBrowserBridgeServer).toHaveBeenCalledOnce();
const bridgeParams = mocks.startBrowserBridgeServer.mock.calls[0]?.[0];
expect(bridgeParams).toMatchObject({
host: "127.0.0.1",
port: 0,
authToken: expect.stringMatching(/^[A-Za-z0-9_-]{40,}$/u),
resolved: {
enabled: true,
attachOnly: true,
defaultProfile: "worker",
profiles: {
worker: {
driver: "openclaw",
attachOnly: true,
cdpUrl: "http://127.0.0.1:9222",
},
},
extensionRelayPorts: {},
extensionRelayInternalTokens: {},
},
});
expect(Object.keys(bridgeParams.resolved.profiles)).toEqual(["worker"]);
await bridgeParams.onEnsureAttachTarget();
expect(ensureAttachTarget).toHaveBeenCalledOnce();
expect(mocks.createBrowserTool).toHaveBeenCalledWith({
sandboxBridgeUrl: "http://127.0.0.1:18443",
allowHostControl: false,
agentSessionKey: "worker:session-1",
agentDir: "/tmp/worker-state",
workspaceDir,
screenshotResultMode: "path",
persistScreenshot: expect.any(Function),
});
const toolParams = mocks.createBrowserTool.mock.calls[0]?.[0];
const sourcePath = path.join(workspaceDir, "source.png");
await fs.writeFile(sourcePath, "screenshot-bytes");
const artifactPath = await toolParams.persistScreenshot({ sourcePath, type: "png" });
expect(artifactPath).toMatch(
/\.artifacts\/cloud-worker-browser\/screenshot-[a-f0-9]{16}\.png$/u,
);
expect(await fs.readFile(artifactPath, "utf8")).toBe("screenshot-bytes");
expect((await fs.stat(artifactPath)).mode & 0o777).toBe(0o600);
const filesBeforeFailure = await fs.readdir(path.dirname(artifactPath));
await expect(
toolParams.persistScreenshot({
sourcePath: path.join(workspaceDir, "missing.png"),
type: "png",
}),
).rejects.toThrow();
expect(await fs.readdir(path.dirname(artifactPath))).toEqual(filesBeforeFailure);
expect(runtime.tool).toEqual({ name: "browser" });
await runtime.dispose();
expect(mocks.stopBrowserBridgeServer).toHaveBeenCalledWith({ marker: "bridge-server" });
expect(ensureAttachTarget).toHaveBeenCalledOnce();
await fs.rm(workspaceDir, { recursive: true, force: true });
});
it.runIf(process.platform !== "win32")(
"rejects screenshot artifact symlink escapes",
async () => {
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "attached-browser-workspace-"));
const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), "attached-browser-outside-"));
await fs.symlink(outsideDir, path.join(workspaceDir, ".artifacts"));
await createAttachedBrowserToolRuntime({
cdpUrl: "http://127.0.0.1:9222",
ensureAttachTarget: async () => {},
workspaceDir,
});
const toolParams = mocks.createBrowserTool.mock.calls.at(-1)?.[0];
const sourcePath = path.join(workspaceDir, "source.png");
await fs.writeFile(sourcePath, "screenshot-bytes");
await expect(toolParams.persistScreenshot({ sourcePath, type: "png" })).rejects.toThrow();
expect(await fs.readdir(outsideDir)).toEqual([]);
await fs.rm(workspaceDir, { recursive: true, force: true });
await fs.rm(outsideDir, { recursive: true, force: true });
},
);
it.each([
"http://localhost:9222",
"https://127.0.0.1:9222",
"http://127.0.0.1",
"http://127.0.0.1:9222/devtools/browser/target",
])("rejects non-canonical raw CDP URL %s before starting a bridge", async (cdpUrl) => {
await expect(
createAttachedBrowserToolRuntime({
cdpUrl,
ensureAttachTarget: async () => {},
workspaceDir: "/tmp/workspace",
}),
).rejects.toThrow("loopback HTTP URL with an explicit port");
expect(mocks.startBrowserBridgeServer).not.toHaveBeenCalled();
});
});