599 lines
25 KiB
TypeScript
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);
|
|
});
|