188 lines
6.2 KiB
TypeScript
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();
|
|
});
|
|
});
|