Files
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

599 lines
25 KiB
TypeScript

import assert from "node:assert/strict";
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { initTheme } from "@earendil-works/pi-coding-agent";
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import {
registerFastBrowserCommand,
resetFastBrowserModelChoicesCacheForTests,
} from "./command.ts";
import { FAST_BROWSER_SETTING_IDS, FAST_BROWSER_SETTING_LABELS } from "./settings-view.ts";
import { loadFastBrowserConfig } from "./config.ts";
interface RegisteredCommand {
handler: (args: string, ctx: ExtensionContext) => Promise<void>;
}
function captureCommand(actions?: {
stopBrowser?: () => Promise<void>;
launchBrowser?: (ctx: ExtensionContext, purpose: "setup" | "automation") => Promise<{ sessionId: string; profile?: string }>;
}): RegisteredCommand {
let command: RegisteredCommand | undefined;
registerFastBrowserCommand({
registerCommand(_name: string, definition: unknown) {
command = definition as RegisteredCommand;
},
sendMessage() {},
} as unknown as ExtensionAPI, actions);
assert.ok(command);
return command;
}
async function withEnvironment<T>(values: Record<string, string | undefined>, action: () => Promise<T>): Promise<T> {
const temporaryAgentDir = values.PI_CODING_AGENT_DIR === undefined
? await mkdtemp(join(tmpdir(), "fast-browser-command-agent-"))
: undefined;
const effectiveValues: Record<string, string | undefined> = {
...values,
...(temporaryAgentDir ? { PI_CODING_AGENT_DIR: temporaryAgentDir } : {}),
};
const previous = new Map<string, string | undefined>();
const names = new Set(["PI_CODING_AGENT_DIR", ...Object.keys(effectiveValues)]);
for (const key of names) {
previous.set(key, process.env[key]);
const value = effectiveValues[key];
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
try {
return await action();
} finally {
for (const [key, value] of previous) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
if (temporaryAgentDir) await rm(temporaryAgentDir, { recursive: true, force: true });
}
}
interface ContextHooks {
afterInput?: (title: string, value: string | undefined) => Promise<void> | void;
}
function context(
cwd: string,
selects: string[],
inputs: Array<string | undefined>,
notices: string[],
models: Array<{ provider: string; id: string }> = [],
rendered: string[] = [],
hooks: ContextHooks = {},
): ExtensionContext {
const settingLabels = [
FAST_BROWSER_SETTING_LABELS[FAST_BROWSER_SETTING_IDS.cdpUrl],
FAST_BROWSER_SETTING_LABELS[FAST_BROWSER_SETTING_IDS.allowedOrigins],
FAST_BROWSER_SETTING_LABELS[FAST_BROWSER_SETTING_IDS.python],
FAST_BROWSER_SETTING_LABELS[FAST_BROWSER_SETTING_IDS.textModel],
FAST_BROWSER_SETTING_LABELS[FAST_BROWSER_SETTING_IDS.textReasoning],
FAST_BROWSER_SETTING_LABELS[FAST_BROWSER_SETTING_IDS.typesafeKey],
FAST_BROWSER_SETTING_LABELS[FAST_BROWSER_SETTING_IDS.browserMode],
FAST_BROWSER_SETTING_LABELS[FAST_BROWSER_SETTING_IDS.browserProduct],
FAST_BROWSER_SETTING_LABELS[FAST_BROWSER_SETTING_IDS.browserExecutable],
FAST_BROWSER_SETTING_LABELS[FAST_BROWSER_SETTING_IDS.browserHeadless],
FAST_BROWSER_SETTING_LABELS[FAST_BROWSER_SETTING_IDS.browserStartUrl],
FAST_BROWSER_SETTING_LABELS[FAST_BROWSER_SETTING_IDS.browserProfile],
];
return {
mode: "tui",
hasUI: true,
cwd,
ui: {
async select(_title: string, _items: string[]) {
return selects.shift();
},
async input(title: string, _placeholder?: string) {
const value = inputs.shift();
await hooks.afterInput?.(title, value);
return value;
},
async custom<T>(factory: (tui: { requestRender(): void }, theme: unknown, keybindings: unknown, done: (value: T) => void) => unknown): Promise<T> {
let completed = false;
let result: T;
const component = factory({ requestRender() {} }, {}, {}, (value: T) => {
completed = true;
result = value;
}) as { handleInput?(data: string): void };
const renderedComponent = component as { render?(width: number): string[] };
if (renderedComponent.render) rendered.push(renderedComponent.render(80).join("\n"));
const selected = selects.shift();
const normalized = selected?.startsWith("Edit ")
? selected.slice("Edit ".length)
: selected;
if (selected === "Cancel") {
component.handleInput?.("\u001b");
} else {
const index = settingLabels.findIndex((label) => label.toLowerCase() === normalized?.toLowerCase());
assert(index >= 0, `unknown settings row: ${selected}`);
for (let i = 0; i < index; i++) component.handleInput?.("\u001b[B");
component.handleInput?.("\r");
}
assert.equal(completed, true, `settings row did not dispatch: ${selected}`);
return result!;
},
notify(message: string) {
notices.push(message);
},
},
modelRegistry: {
getAvailable: () => models,
},
} as unknown as ExtensionContext;
}
test.before(() => initTheme("dark", false));
test.afterEach(() => resetFastBrowserModelChoicesCacheForTests());
test("close-all action explicitly describes this Pi instance's session scope", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-close-"));
const notices: string[] = [];
let closed = 0;
try {
await withEnvironment({ PI_CODING_AGENT_DIR: join(cwd, "agent") }, async () => {
await captureCommand({ stopBrowser: async () => { closed++; } }).handler("", context(
cwd, ["Close all browser sessions in this Pi instance", "Close"], [], notices,
));
});
assert.equal(closed, 1);
assert(notices.some((message) => message.includes("external browser processes were left running")));
} finally { await rm(cwd, { recursive: true, force: true }); }
});
test("launch menu actions use explicit setup and automation purposes", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-launch-menu-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
const notices: string[] = [];
const purposes: string[] = [];
try {
await withEnvironment({
PI_CODING_AGENT_DIR: agentDir,
FAST_BROWSER_CDP_URL: "http://127.0.0.1:9222",
FAST_BROWSER_ALLOWED_ORIGINS: '["https://example.test"]',
}, async () => {
await captureCommand({
launchBrowser: async (_ctx, purpose) => {
purposes.push(purpose);
return { sessionId: `session-${purpose}`, profile: "evebot" };
},
}).handler("", context(cwd, [
"Launch named profile for manual setup",
"Launch automated browser session",
"Close",
], [], notices));
});
} finally {
await rm(cwd, { recursive: true, force: true });
await rm(agentDir, { recursive: true, force: true });
}
assert.deepEqual(purposes, ["setup", "automation"]);
assert.match(notices[0]!, /session-setup/);
assert.match(notices[1]!, /session-automation/);
assert.match(notices[0]!, /evebot/);
});
test("publishes launch metadata without triggering a turn or exposing secrets", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-message-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
const notices: string[] = [];
const messages: Array<{ message: any; options: any }> = [];
let command: RegisteredCommand | undefined;
try {
registerFastBrowserCommand({
registerCommand(_name: string, definition: unknown) {
command = definition as RegisteredCommand;
},
sendMessage(message: unknown, options: unknown) {
messages.push({ message, options });
},
} as unknown as ExtensionAPI, {
launchBrowser: async (_ctx, purpose) => ({
sessionId: `session-${purpose}`,
profile: "evebot",
}),
});
assert.ok(command);
await withEnvironment({ PI_CODING_AGENT_DIR: agentDir }, async () => {
await command!.handler("", context(cwd, [
"Launch named profile for manual setup",
"Launch automated browser session",
"Close",
], [], notices));
});
} finally {
await rm(cwd, { recursive: true, force: true });
await rm(agentDir, { recursive: true, force: true });
}
assert.equal(messages.length, 2);
assert.deepEqual(messages.map(({ message }) => message.details), [
{ sessionId: "session-setup", purpose: "setup", profile: "evebot" },
{ sessionId: "session-automation", purpose: "automation", profile: "evebot" },
]);
for (const { message, options } of messages) {
assert.equal(message.customType, "fast-browser-session");
assert.equal(message.display, true);
assert.equal(options.triggerTurn, false);
assert.equal(options.deliverAs, "nextTurn");
assert.doesNotMatch(message.content, /secret|credential|password/i);
}
assert.match(messages[0]!.message.content, /not automatable.*close/i);
assert.match(notices[0]!, /not automatable.*closed/i);
});
test("sanitizes non-config launch failures with recovery guidance", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-launch-error-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
const notices: string[] = [];
try {
await withEnvironment({ PI_CODING_AGENT_DIR: agentDir }, async () => {
await captureCommand({
launchBrowser: async () => {
throw new Error("profile secret should not be shown");
},
}).handler("", context(cwd, ["Launch automated browser session", "Close"], [], notices));
});
} finally {
await rm(cwd, { recursive: true, force: true });
await rm(agentDir, { recursive: true, force: true });
}
assert.equal(notices[0], "Unable to launch fast-browser session; close other users of the named profile and check browser configuration");
assert.doesNotMatch(notices[0]!, /profile secret/);
});
test("edits initially partial settings and autosaves a complete project config", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
const notices: string[] = [];
const ctx = context(
cwd,
["Edit project settings", "Edit CDP endpoint", "Edit allowed origins", "Specific origins", "Cancel", "Close"],
["http://127.0.0.1:9333", '["https://project.example"]'],
notices,
);
await withEnvironment({
PI_CODING_AGENT_DIR: agentDir,
FAST_BROWSER_CDP_URL: undefined,
FAST_BROWSER_ALLOWED_ORIGINS: undefined,
}, async () => {
await captureCommand().handler("", ctx);
});
const saved = JSON.parse(await readFile(join(cwd, ".pi", "fast-browser.json"), "utf8")) as Record<string, unknown>;
assert.equal(saved.cdpUrl, "http://127.0.0.1:9333");
assert.deepEqual(saved.allowedOrigins, ["https://project.example"]);
assert.equal(saved.textReasoning, undefined);
assert.equal(notices.some((message) => message.includes("Saved project")), false);
});
test("view displays the unrestricted default and its default source", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-default-view-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
const notices: string[] = [];
await withEnvironment({
PI_CODING_AGENT_DIR: agentDir,
FAST_BROWSER_CDP_URL: "http://127.0.0.1:9222",
FAST_BROWSER_ALLOWED_ORIGINS: undefined,
}, async () => {
await captureCommand().handler("", context(cwd, ["View effective configuration", "Close"], [], notices));
});
assert.match(notices.join("\n"), /allowedOrigins: All web origins \[default\]/);
});
test("origin policy editor autosaves unrestricted and inherited choices", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-origin-policy-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
await mkdir(join(cwd, ".pi"), { recursive: true });
const path = join(cwd, ".pi", "fast-browser.json");
await writeFile(path, JSON.stringify({ allowedOrigins: ["https://example.test"] }));
const notices: string[] = [];
const ctx = context(cwd, [
"Edit project settings", "Edit allowed origins", "All web origins (*)", "Cancel",
"Edit project settings", "Edit allowed origins", "(unset override — inherit)", "Cancel", "Close",
], [], notices);
await withEnvironment({ PI_CODING_AGENT_DIR: agentDir }, async () => {
await captureCommand().handler("", ctx);
});
assert.deepEqual(JSON.parse(await readFile(path, "utf8")), {});
});
test("cancel and malformed input preserve the existing file", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-cancel-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
await mkdir(join(cwd, ".pi"), { recursive: true });
await writeFile(join(cwd, ".pi", "fast-browser.json"), "{\"textModel\":\"keep-me\",");
const before = await readFile(join(cwd, ".pi", "fast-browser.json"), "utf8");
const notices: string[] = [];
const ctx = context(cwd, [], [], notices);
await withEnvironment({ PI_CODING_AGENT_DIR: agentDir }, async () => {
await captureCommand().handler("", ctx);
});
assert.equal(await readFile(join(cwd, ".pi", "fast-browser.json"), "utf8"), before);
assert.equal(notices.length > 0, true);
});
test("field cancellation returns to settings without undoing earlier autosaves", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-scope-cancel-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
await mkdir(join(cwd, ".pi"), { recursive: true });
const path = join(cwd, ".pi", "fast-browser.json");
await writeFile(path, JSON.stringify({ textModel: "keep-me" }));
const before = await readFile(path, "utf8");
const notices: string[] = [];
const ctx = context(cwd, [
"Edit project settings", "Edit CDP endpoint", "Edit Python executable", "Cancel", "Close",
], ["http://127.0.0.1:9333", undefined], notices);
await withEnvironment({ PI_CODING_AGENT_DIR: agentDir }, async () => {
await captureCommand().handler("", ctx);
});
const after = await readFile(path, "utf8");
assert.notEqual(after, before);
assert.deepEqual(JSON.parse(after), {
cdpUrl: "http://127.0.0.1:9333",
textModel: "keep-me",
});
});
test("invalid field input preserves the committed file and draft", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-invalid-edit-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
await mkdir(join(cwd, ".pi"), { recursive: true });
const path = join(cwd, ".pi", "fast-browser.json");
await writeFile(path, JSON.stringify({ allowedOrigins: ["https://keep.example"] }));
const before = await readFile(path, "utf8");
const notices: string[] = [];
const ctx = context(cwd, [
"Edit project settings", "Edit allowed origins", "Specific origins", "Cancel", "Close",
], ["not-json"], notices);
await withEnvironment({ PI_CODING_AGENT_DIR: agentDir }, async () => {
await captureCommand().handler("", ctx);
});
assert.equal(await readFile(path, "utf8"), before);
assert(notices.length > 0);
});
test("write failures preserve the committed file and displayed state", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-write-failure-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
await mkdir(join(cwd, ".pi"), { recursive: true });
const path = join(cwd, ".pi", "fast-browser.json");
await writeFile(path, JSON.stringify({ textModel: "keep-me" }));
const notices: string[] = [];
const ctx = context(
cwd,
["Edit project settings", "Edit Python executable", "Cancel", "Close"],
["/tmp/python"],
notices,
[],
[],
{ afterInput: async () => { await rm(path); await mkdir(path); } },
);
await withEnvironment({ PI_CODING_AGENT_DIR: agentDir }, async () => {
await captureCommand().handler("", ctx);
});
assert.equal((await stat(path)).isDirectory(), true);
assert(notices.length > 0);
});
test("inherited browser edits create a complete project browser override", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-inherited-browser-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
await writeFile(join(agentDir, "fast-browser.json"), JSON.stringify({
allowedOrigins: ["https://example.test"],
browser: {
mode: "launch", product: "chromium", startUrl: "https://example.test/start",
headless: true, profile: "global-profile",
},
}));
const notices: string[] = [];
const ctx = context(cwd, [
"Edit project settings", "Edit browser profile", "Cancel", "Close",
], ["project-profile"], notices);
await withEnvironment({ PI_CODING_AGENT_DIR: agentDir }, async () => {
await captureCommand().handler("", ctx);
});
const saved = JSON.parse(await readFile(join(cwd, ".pi", "fast-browser.json"), "utf8")) as any;
assert.deepEqual(saved.browser, {
mode: "launch", product: "chromium", startUrl: "https://example.test/start",
headless: true, profile: "project-profile",
});
});
test("reselecting browser mode preserves all existing browser attributes", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-browser-mode-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
await mkdir(join(cwd, ".pi"), { recursive: true });
const browser = {
mode: "launch", product: "chromium", startUrl: "https://example.test/start",
headless: true, profile: "evebot",
};
const path = join(cwd, ".pi", "fast-browser.json");
await writeFile(path, JSON.stringify({ browser }));
const ctx = context(cwd, [
"Edit project settings", "Edit browser mode", "launch", "Cancel", "Close",
], [], []);
await withEnvironment({ PI_CODING_AGENT_DIR: agentDir }, async () => {
await captureCommand().handler("", ctx);
});
assert.deepEqual(JSON.parse(await readFile(path, "utf8")).browser, browser);
});
test("view includes precedence and paths without exposing credential contents", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-view-"));
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({
cdpUrl: "http://127.0.0.1:9333",
allowedOrigins: ["https://example.test"],
typesafeKey: { env: "TYPESAFE_API_KEY" },
}));
const notices: string[] = [];
const ctx = context(cwd, ["View effective configuration", "Close"], [], notices);
await withEnvironment({ PI_CODING_AGENT_DIR: agentDir }, async () => {
await captureCommand().handler("", ctx);
});
const view = notices.join("\n");
assert.match(view, /Precedence:/);
assert.match(view, /Project path:/);
assert.match(view, /project/);
assert.doesNotMatch(view, /secret|apiKey:/i);
});
test("model choices are cached and optional stop action is exposed", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-model-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
let calls = 0;
let stopped = 0;
const notices: string[] = [];
const ctx = context(cwd, ["Edit project settings", "Edit auxiliary text model", "fixture/small", "Cancel", "Close"], [], notices, [
{ provider: "fixture", id: "small" },
]);
(ctx.modelRegistry as any).getAvailable = () => {
calls++;
return [{ provider: "fixture", id: "small" }];
};
await withEnvironment({
PI_CODING_AGENT_DIR: agentDir,
FAST_BROWSER_CDP_URL: "http://127.0.0.1:9222",
FAST_BROWSER_ALLOWED_ORIGINS: '["https://example.test"]',
}, async () => {
const command = captureCommand({ stopBrowser: async () => { stopped++; } });
await command.handler("", ctx);
});
assert.equal(calls, 1);
assert.equal(stopped, 0);
});
test("partial global defaults autosave without endpoint or origins", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-partial-global-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
const notices: string[] = [];
const ctx = context(cwd, [
"Edit global settings", "Edit auxiliary text model", "fixture/small", "Cancel", "Close",
], [], notices, [{ provider: "fixture", id: "small" }]);
await withEnvironment({
PI_CODING_AGENT_DIR: agentDir,
FAST_BROWSER_CDP_URL: undefined,
FAST_BROWSER_ALLOWED_ORIGINS: undefined,
}, async () => {
await captureCommand().handler("", ctx);
});
const saved = JSON.parse(await readFile(join(agentDir, "fast-browser.json"), "utf8")) as Record<string, unknown>;
assert.deepEqual(saved, { textModel: "fixture/small" });
});
test("global edits preserve project-only values", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-global-"));
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:9222",
textModel: "global/model",
}));
await writeFile(join(cwd, ".pi", "fast-browser.json"), JSON.stringify({
allowedOrigins: ["https://project.example"],
}));
const notices: string[] = [];
const ctx = context(cwd, [
"Edit global settings", "Edit Python executable", "Cancel", "Close",
], ["/global/python"], notices);
await withEnvironment({ PI_CODING_AGENT_DIR: agentDir }, async () => {
await captureCommand().handler("", ctx);
});
const globalConfig = JSON.parse(await readFile(join(agentDir, "fast-browser.json"), "utf8")) as Record<string, unknown>;
const projectConfig = JSON.parse(await readFile(join(cwd, ".pi", "fast-browser.json"), "utf8")) as Record<string, unknown>;
assert.equal(globalConfig.allowedOrigins, undefined);
assert.deepEqual(projectConfig.allowedOrigins, ["https://project.example"]);
assert.equal(globalConfig.python, "/global/python");
});
test("global settings do not display project overrides as inherited defaults", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-global-view-"));
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({ textModel: "project/model" }));
const notices: string[] = [];
const rendered: string[] = [];
const ctx = context(cwd, ["Edit global settings", "Cancel", "Close"], [], notices, [], rendered);
await withEnvironment({
PI_CODING_AGENT_DIR: agentDir,
FAST_BROWSER_TEXT_MODEL: undefined,
FAST_BROWSER_CDP_URL: undefined,
FAST_BROWSER_ALLOWED_ORIGINS: undefined,
}, async () => {
await captureCommand().handler("", ctx);
});
assert(rendered.length > 0);
assert.doesNotMatch(rendered[0]!, /project\/model/);
assert.match(rendered[0]!, /\(unset\)/);
});
test("unsetting a project model override reveals the inherited model", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-inherit-"));
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:9222",
allowedOrigins: ["https://example.test"],
textModel: "global/model",
}));
await writeFile(join(cwd, ".pi", "fast-browser.json"), JSON.stringify({ textModel: "project/model" }));
const notices: string[] = [];
const ctx = context(cwd, [
"Edit project settings", "Edit auxiliary text model", "(unset override — inherit)", "Cancel", "Close",
], [], notices);
await withEnvironment({ PI_CODING_AGENT_DIR: agentDir }, async () => {
await captureCommand().handler("", ctx);
const config = await loadFastBrowserConfig(cwd, {}, { agentDir, env: {} });
assert.equal(config.textModel, "global/model");
});
assert.equal(notices.some((message) => message.includes("Saved project")), false);
});
test("edits launch product, start URL, and headless settings", async () => {
const cwd = await mkdtemp(join(tmpdir(), "fast-browser-command-launch-"));
const agentDir = await mkdtemp(join(tmpdir(), "fast-browser-agent-"));
const notices: string[] = [];
const ctx = context(
cwd,
[
"Edit project settings",
"Edit browser mode", "launch",
"Edit browser product", "vivaldi",
"Edit browser start URL",
"Edit browser profile",
"Edit browser headless", "true",
"Cancel",
"Close",
],
["https://example.test/start", "evebot"],
notices,
);
await withEnvironment({
PI_CODING_AGENT_DIR: agentDir,
FAST_BROWSER_CDP_URL: "http://127.0.0.1:9222",
FAST_BROWSER_ALLOWED_ORIGINS: '["https://example.test"]',
}, async () => {
await captureCommand().handler("", ctx);
});
const saved = JSON.parse(await readFile(join(cwd, ".pi", "fast-browser.json"), "utf8")) as any;
assert.deepEqual(saved.browser, {
mode: "launch",
product: "vivaldi",
startUrl: "https://example.test/start",
headless: true,
profile: "evebot",
});
assert.equal(saved.cdpUrl, undefined);
assert.equal(notices.some((message) => message.includes("Saved project")), false);
});