openclaw/src/agents/sandbox/browser.create.test.ts
2026-03-15 21:35:30 -07:00

217 lines
7.1 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { BROWSER_BRIDGES } from "./browser-bridges.js";
import { ensureSandboxBrowser } from "./browser.js";
import { resetNoVncObserverTokensForTests } from "./novnc-auth.js";
import { collectDockerFlagValues, findDockerArgsCall } from "./test-args.js";
import type { SandboxConfig } from "./types.js";
const dockerMocks = vi.hoisted(() => ({
dockerContainerState: vi.fn(),
execDocker: vi.fn(),
readDockerContainerEnvVar: vi.fn(),
readDockerContainerLabel: vi.fn(),
readDockerPort: vi.fn(),
}));
const registryMocks = vi.hoisted(() => ({
readBrowserRegistry: vi.fn(),
updateBrowserRegistry: vi.fn(),
}));
const bridgeMocks = vi.hoisted(() => ({
startBrowserBridgeServer: vi.fn(),
stopBrowserBridgeServer: vi.fn(),
}));
vi.mock("./docker.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./docker.js")>();
return {
...actual,
dockerContainerState: dockerMocks.dockerContainerState,
execDocker: dockerMocks.execDocker,
readDockerContainerEnvVar: dockerMocks.readDockerContainerEnvVar,
readDockerContainerLabel: dockerMocks.readDockerContainerLabel,
readDockerPort: dockerMocks.readDockerPort,
};
});
vi.mock("./registry.js", () => ({
readBrowserRegistry: registryMocks.readBrowserRegistry,
updateBrowserRegistry: registryMocks.updateBrowserRegistry,
}));
vi.mock("../../browser/bridge-server.js", () => ({
startBrowserBridgeServer: bridgeMocks.startBrowserBridgeServer,
stopBrowserBridgeServer: bridgeMocks.stopBrowserBridgeServer,
}));
function buildConfig(enableNoVnc: boolean): SandboxConfig {
return {
mode: "all",
backend: "docker",
scope: "session",
workspaceAccess: "none",
workspaceRoot: "/tmp/openclaw-sandboxes",
docker: {
image: "openclaw-sandbox:bookworm-slim",
containerPrefix: "openclaw-sbx-",
workdir: "/workspace",
readOnlyRoot: true,
tmpfs: ["/tmp", "/var/tmp", "/run"],
network: "none",
capDrop: ["ALL"],
env: { LANG: "C.UTF-8" },
},
ssh: {
command: "ssh",
workspaceRoot: "/tmp/openclaw-sandboxes",
strictHostKeyChecking: true,
updateHostKeys: true,
},
browser: {
enabled: true,
image: "openclaw-sandbox-browser:bookworm-slim",
containerPrefix: "openclaw-sbx-browser-",
network: "openclaw-sandbox-browser",
cdpPort: 9222,
vncPort: 5900,
noVncPort: 6080,
headless: false,
enableNoVnc,
allowHostControl: false,
autoStart: true,
autoStartTimeoutMs: 12_000,
},
tools: {
allow: ["browser"],
deny: [],
},
prune: {
idleHours: 24,
maxAgeDays: 7,
},
};
}
describe("ensureSandboxBrowser create args", () => {
beforeEach(() => {
BROWSER_BRIDGES.clear();
resetNoVncObserverTokensForTests();
dockerMocks.dockerContainerState.mockClear();
dockerMocks.execDocker.mockClear();
dockerMocks.readDockerContainerEnvVar.mockClear();
dockerMocks.readDockerContainerLabel.mockClear();
dockerMocks.readDockerPort.mockClear();
registryMocks.readBrowserRegistry.mockClear();
registryMocks.updateBrowserRegistry.mockClear();
bridgeMocks.startBrowserBridgeServer.mockClear();
bridgeMocks.stopBrowserBridgeServer.mockClear();
dockerMocks.dockerContainerState.mockResolvedValue({ exists: false, running: false });
dockerMocks.execDocker.mockImplementation(async (args: string[]) => {
if (args[0] === "image" && args[1] === "inspect") {
return { stdout: "[]", stderr: "", code: 0 };
}
return { stdout: "", stderr: "", code: 0 };
});
dockerMocks.readDockerContainerLabel.mockResolvedValue(null);
dockerMocks.readDockerContainerEnvVar.mockResolvedValue(null);
dockerMocks.readDockerPort.mockImplementation(async (_containerName: string, port: number) => {
if (port === 9222) {
return 49100;
}
if (port === 6080) {
return 49101;
}
return null;
});
registryMocks.readBrowserRegistry.mockResolvedValue({ entries: [] });
registryMocks.updateBrowserRegistry.mockResolvedValue(undefined);
bridgeMocks.startBrowserBridgeServer.mockResolvedValue({
server: {} as never,
port: 19000,
baseUrl: "http://127.0.0.1:19000",
state: {
server: null,
port: 19000,
resolved: { profiles: {} },
profiles: new Map(),
},
});
bridgeMocks.stopBrowserBridgeServer.mockResolvedValue(undefined);
});
it("publishes noVNC on loopback and injects noVNC password env", async () => {
const result = await ensureSandboxBrowser({
scopeKey: "session:test",
workspaceDir: "/tmp/workspace",
agentWorkspaceDir: "/tmp/workspace",
cfg: buildConfig(true),
});
const createArgs = findDockerArgsCall(dockerMocks.execDocker.mock.calls, "create");
expect(createArgs).toBeDefined();
expect(createArgs).toContain("127.0.0.1::6080");
const envEntries = collectDockerFlagValues(createArgs ?? [], "-e");
expect(envEntries).toContain("OPENCLAW_BROWSER_NO_SANDBOX=1");
const passwordEntry = envEntries.find((entry) =>
entry.startsWith("OPENCLAW_BROWSER_NOVNC_PASSWORD="),
);
expect(passwordEntry).toMatch(/^OPENCLAW_BROWSER_NOVNC_PASSWORD=[A-Za-z0-9]{8}$/);
expect(result?.noVncUrl).toMatch(/^http:\/\/127\.0\.0\.1:19000\/sandbox\/novnc\?token=/);
expect(result?.noVncUrl).not.toContain("password=");
});
it("does not inject noVNC password env when noVNC is disabled", async () => {
const result = await ensureSandboxBrowser({
scopeKey: "session:test",
workspaceDir: "/tmp/workspace",
agentWorkspaceDir: "/tmp/workspace",
cfg: buildConfig(false),
});
const createArgs = findDockerArgsCall(dockerMocks.execDocker.mock.calls, "create");
const envEntries = collectDockerFlagValues(createArgs ?? [], "-e");
expect(envEntries.some((entry) => entry.startsWith("OPENCLAW_BROWSER_NOVNC_PASSWORD="))).toBe(
false,
);
expect(result?.noVncUrl).toBeUndefined();
});
it("mounts the main workspace read-only when workspaceAccess is none", async () => {
const cfg = buildConfig(false);
cfg.workspaceAccess = "none";
await ensureSandboxBrowser({
scopeKey: "session:test",
workspaceDir: "/tmp/workspace",
agentWorkspaceDir: "/tmp/workspace",
cfg,
});
const createArgs = findDockerArgsCall(dockerMocks.execDocker.mock.calls, "create");
expect(createArgs).toBeDefined();
expect(createArgs).toContain("/tmp/workspace:/workspace:ro");
});
it("keeps the main workspace writable when workspaceAccess is rw", async () => {
const cfg = buildConfig(false);
cfg.workspaceAccess = "rw";
await ensureSandboxBrowser({
scopeKey: "session:test",
workspaceDir: "/tmp/workspace",
agentWorkspaceDir: "/tmp/workspace",
cfg,
});
const createArgs = findDockerArgsCall(dockerMocks.execDocker.mock.calls, "create");
expect(createArgs).toBeDefined();
expect(createArgs).toContain("/tmp/workspace:/workspace");
expect(createArgs).not.toContain("/tmp/workspace:/workspace:ro");
});
});