Files
harness-tools/extensions/fast-browser/config.test.ts
T
jb fd63b0ccec
fast-browser scenarios / Offline scenario contract tests (push) Failing after 44s
fast-browser scenarios / Local Chromium headless scenarios (push) Failing after 23s
fast-browser-with-jev
2026-09-20 12:24:08 -04:00

279 lines
12 KiB
TypeScript

import assert from "node:assert/strict";
import { mkdtemp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import {
FAST_BROWSER_ALLOWED_ORIGINS,
FAST_BROWSER_CDP_URL,
FAST_BROWSER_PYTHON,
FAST_BROWSER_TEXT_MODEL,
FastBrowserConfigError,
inspectFastBrowserConfigSettings,
loadFastBrowserConfig,
parseFastBrowserConfig,
writeFastBrowserConfigFile,
validateBrowserProfileName,
} from "./config.ts";
function env(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
return {
[FAST_BROWSER_CDP_URL]: "http://127.0.0.1:9222",
[FAST_BROWSER_ALLOWED_ORIGINS]: '["https://example.test"]',
...overrides,
};
}
test("parses explicit loopback endpoint and exact origin allowlist", () => {
assert.deepEqual(parseFastBrowserConfig(env()), {
cdpUrl: "http://127.0.0.1:9222",
allowedOrigins: ["https://example.test"],
});
});
test("keeps optional model and Python settings separate from credentials", () => {
const config = parseFastBrowserConfig(
env({
[FAST_BROWSER_TEXT_MODEL]: "anthropic/claude-haiku-4-5",
[FAST_BROWSER_PYTHON]: "/usr/bin/python3.12",
TYPESAFE_API_KEY: "must-not-be-read-into-config",
}),
);
assert.equal(config.textModel, "anthropic/claude-haiku-4-5");
assert.equal(config.python, "/usr/bin/python3.12");
assert.equal("apiKey" in config, false);
assert.equal(JSON.stringify(config).includes("must-not-be-read"), false);
});
test("requires CDP endpoint but defaults missing origins to unrestricted HTTP(S)", () => {
assert.throws(
() => parseFastBrowserConfig(env({ [FAST_BROWSER_CDP_URL]: "" })),
(error: unknown) => error instanceof FastBrowserConfigError && /CDP_URL/.test(error.message),
);
assert.equal(parseFastBrowserConfig({ [FAST_BROWSER_CDP_URL]: "http://127.0.0.1:9222" }).allowedOrigins, "*");
});
test("rejects non-loopback, non-root, and credential-bearing CDP URLs", () => {
for (const value of [
"https://127.0.0.1:9222",
"http://192.0.2.1:9222",
"http://user:pass@127.0.0.1:9222",
"http://127.0.0.1:9222/json/version",
"http://127.0.0.1:9222?query=1",
]) {
assert.throws(() => parseFastBrowserConfig(env({ [FAST_BROWSER_CDP_URL]: value })), FastBrowserConfigError);
}
});
test("supports explicit unrestricted policy and rejects malformed wildcard policies", () => {
assert.equal(parseFastBrowserConfig(env({ [FAST_BROWSER_ALLOWED_ORIGINS]: "*" })).allowedOrigins, "*");
assert.equal(parseFastBrowserConfig(env({ [FAST_BROWSER_ALLOWED_ORIGINS]: '"*"' })).allowedOrigins, "*");
for (const value of ["[]", "not-json", '"https://example.test"', '["*"]', '["https://*.example.test"]']) {
assert.throws(() => parseFastBrowserConfig(env({ [FAST_BROWSER_ALLOWED_ORIGINS]: value })), FastBrowserConfigError);
}
});
test("allows remote exact origins but rejects malformed, wildcard, and path-bearing entries", () => {
assert.deepEqual(
parseFastBrowserConfig(
env({
[FAST_BROWSER_ALLOWED_ORIGINS]:
'["https://example.test", "https://remote.example.org:8443"]',
}),
).allowedOrigins,
["https://example.test", "https://remote.example.org:8443"],
);
for (const value of [
"not-json",
'["*"]',
'["https://*.example.test"]',
'["https://example.test/path"]',
'["file:///tmp/fixture"]',
'["https://example.test", "https://example.test"]',
]) {
assert.throws(
() => parseFastBrowserConfig(env({ [FAST_BROWSER_ALLOWED_ORIGINS]: value })),
FastBrowserConfigError,
);
}
const tooMany = JSON.stringify(
Array.from({ length: 21 }, (_, index) => `https://host-${index}.example.test`),
);
assert.throws(
() => parseFastBrowserConfig(env({ [FAST_BROWSER_ALLOWED_ORIGINS]: tooMany })),
FastBrowserConfigError,
);
const tooLong = JSON.stringify([`https://${"a".repeat(4_001)}.example`]);
assert.throws(
() => parseFastBrowserConfig(env({ [FAST_BROWSER_ALLOWED_ORIGINS]: tooLong })),
FastBrowserConfigError,
);
});
test("merges env, global, project, and per-run settings in documented precedence", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-config-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
await mkdir(join(cwd, ".pi"), { recursive: true });
await writeFile(join(agentDir, "fast-browser.json"), JSON.stringify({
cdpUrl: "http://127.0.0.1:9333",
allowedOrigins: ["https://global.example"],
python: "/global/python",
textModel: "global/model",
}));
await writeFile(join(cwd, ".pi", "fast-browser.json"), JSON.stringify({
allowedOrigins: ["https://project.example"],
textModel: "project/model",
}));
const config = await loadFastBrowserConfig(cwd, { textModel: "override/model" }, {
agentDir,
env: env({ python: "/env/python" }),
});
assert.deepEqual(config, {
cdpUrl: "http://127.0.0.1:9333",
allowedOrigins: ["https://project.example"],
python: "/global/python",
textModel: "override/model",
textReasoning: "off",
});
const info = inspectFastBrowserConfigSettings(cwd, {}, { agentDir, env: env() });
assert.equal(info.sources.cdpUrl?.source, "global");
assert.equal(info.sources.allowedOrigins?.source, "project");
assert.equal(info.sources.textReasoning?.source, "default");
});
test("allows an inherited attach endpoint to coexist with launch mode", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-launch-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
await mkdir(join(cwd, ".pi"), { recursive: true });
await writeFile(join(cwd, ".pi", "fast-browser.json"), JSON.stringify({
browser: {
mode: "launch",
product: "vivaldi",
startUrl: "https://example.test/login",
headless: true,
},
}));
const config = await loadFastBrowserConfig(cwd, {}, {
agentDir,
env: env(),
});
assert.equal(config.browser?.mode, "launch");
assert.equal(config.browser?.product, "vivaldi");
assert.equal(config.cdpUrl, "http://127.0.0.1:9222");
});
test("rejects unsupported Firefox and launch URLs outside allowed origins", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-invalid-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
await mkdir(join(cwd, ".pi"), { recursive: true });
await writeFile(join(cwd, ".pi", "fast-browser.json"), JSON.stringify({
browser: { mode: "launch", product: "firefox", startUrl: "https://example.test" },
}));
await assert.rejects(
loadFastBrowserConfig(cwd, {}, { agentDir, env: env() }),
/Firefox is not supported/,
);
await writeFile(join(cwd, ".pi", "fast-browser.json"), JSON.stringify({
browser: { mode: "launch", product: "chrome", startUrl: "https://outside.test" },
}));
await assert.rejects(
loadFastBrowserConfig(cwd, {}, { agentDir, env: env() }),
/startUrl must use an origin/,
);
});
test("invalid JSON is sanitized and atomic writes keep restrictive permissions", async () => {
const directory = await mkdtemp(join(tmpdir(), "fast-browser-write-"));
const path = join(directory, "fast-browser.json");
await writeFile(path, '{"textModel":"secret-model",');
await assert.rejects(
loadFastBrowserConfig(directory, {}, { agentDir: directory, env: { HOME: directory } }),
(error: unknown) => error instanceof FastBrowserConfigError && !error.message.includes("secret-model"),
);
writeFastBrowserConfigFile(path, {
cdpUrl: "http://127.0.0.1:9222",
allowedOrigins: ["https://example.test"],
typesafeKey: { env: "TYPESAFE_API_KEY" },
});
const content = await readFile(path, "utf8");
assert.equal(content.includes("TYPESAFE_API_KEY"), true);
assert.equal(content.includes("secret-model"), false);
assert.equal((await stat(path)).mode & 0o777, 0o600);
});
test("launch mode may omit cdpUrl when startUrl is allowed", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-launch-only-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
await mkdir(join(cwd, ".pi"), { recursive: true });
await writeFile(join(cwd, ".pi", "fast-browser.json"), JSON.stringify({
allowedOrigins: ["https://example.test"],
browser: { mode: "launch", product: "chromium", startUrl: "https://example.test/start" },
}));
const config = await loadFastBrowserConfig(cwd, {}, { agentDir, env: {} });
assert.equal(config.cdpUrl, undefined);
assert.equal(config.browser?.mode, "launch");
});
test("omitted layered origins default to unrestricted policy", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-default-origins-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
await mkdir(join(cwd, ".pi"), { recursive: true });
await writeFile(join(cwd, ".pi", "fast-browser.json"), JSON.stringify({
browser: { mode: "launch", product: "chromium", startUrl: "https://any.example/start" },
}));
const config = await loadFastBrowserConfig(cwd, {}, { agentDir, env: {} });
assert.equal(config.allowedOrigins, "*");
const settings = inspectFastBrowserConfigSettings(cwd, {}, { agentDir, env: {} });
assert.equal(settings.values.allowedOrigins, "*");
assert.deepEqual(settings.sources.allowedOrigins, { source: "default" });
});
test("named profiles use bounded basenames, never arbitrary profile paths", () => {
for (const name of ["evebot", "project-agent-2", "Login_Profile.v1", "x".repeat(64)]) {
assert.equal(validateBrowserProfileName(name), name);
}
for (const name of ["", ".", "..", "../evebot", "a/b", "a\\b", "/tmp/profile", "~/.config/chromium", " name", "name\n", "x".repeat(65), null, 1]) {
assert.throws(() => validateBrowserProfileName(name), /browser.profile/);
}
});
test("named profile config roundtrips and an explicit disposable override does not inherit it", async (t) => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-named-config-"));
t.after(() => rm(cwd, { recursive: true, force: true }));
const agentDir = join(cwd, "agent");
const path = join(cwd, ".pi", "fast-browser.json");
writeFastBrowserConfigFile(path, {
allowedOrigins: ["https://example.test"],
browser: { mode: "launch", product: "chromium", profile: "evebot", startUrl: "https://example.test" },
});
const saved = await loadFastBrowserConfig(cwd, {}, { agentDir, env: {} });
assert.equal(saved.browser?.profile, "evebot");
const disposable = await loadFastBrowserConfig(cwd, {
browser: { mode: "launch", product: "chromium", startUrl: "https://example.test" },
}, { agentDir, env: {} });
assert.equal(disposable.browser?.profile, undefined);
await assert.rejects(loadFastBrowserConfig(cwd, {
cdpUrl: "http://127.0.0.1:9222", browser: { mode: "attach", profile: "evebot" },
}, { agentDir, env: {} }), /launch settings require mode launch/);
assert.equal(JSON.parse(await readFile(path, "utf8")).browser.profile, "evebot");
});
test("rejects nonregular and oversized configuration files before reading them", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-file-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
const path = join(cwd, ".pi", "fast-browser.json");
await mkdir(path, { recursive: true });
await assert.rejects(
loadFastBrowserConfig(cwd, {}, { agentDir, env: env() }),
/bounded regular file/,
);
await rm(path, { recursive: true });
await writeFile(path, "x".repeat(64 * 1024 + 1));
await assert.rejects(
loadFastBrowserConfig(cwd, {}, { agentDir, env: env() }),
/too large|bounded regular file/,
);
});