Files
Moltbot/src/agents/sandbox/browser.create.test.ts

188 lines
6.2 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 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",
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" },
},
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,
},
};
}
function envEntriesFromDockerArgs(args: string[]): string[] {
const values: string[] = [];
for (let i = 0; i < args.length; i += 1) {
if (args[i] === "-e" && typeof args[i + 1] === "string") {
values.push(args[i + 1]);
}
}
return values;
}
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 = dockerMocks.execDocker.mock.calls.find(
(call: unknown[]) => Array.isArray(call[0]) && call[0][0] === "create",
)?.[0] as string[] | undefined;
expect(createArgs).toBeDefined();
expect(createArgs).toContain("127.0.0.1::6080");
const envEntries = envEntriesFromDockerArgs(createArgs ?? []);
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 = dockerMocks.execDocker.mock.calls.find(
(call: unknown[]) => Array.isArray(call[0]) && call[0][0] === "create",
)?.[0] as string[] | undefined;
const envEntries = envEntriesFromDockerArgs(createArgs ?? []);
expect(envEntries.some((entry) => entry.startsWith("OPENCLAW_BROWSER_NOVNC_PASSWORD="))).toBe(
false,
);
expect(result?.noVncUrl).toBeUndefined();
});
});