fast-browser-with-jev
This commit is contained in:
@@ -0,0 +1,598 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user