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; } function captureCommand(actions?: { stopBrowser?: () => Promise; 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(values: Record, action: () => Promise): Promise { const temporaryAgentDir = values.PI_CODING_AGENT_DIR === undefined ? await mkdtemp(join(tmpdir(), "fast-browser-command-agent-")) : undefined; const effectiveValues: Record = { ...values, ...(temporaryAgentDir ? { PI_CODING_AGENT_DIR: temporaryAgentDir } : {}), }; const previous = new Map(); 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; } function context( cwd: string, selects: string[], inputs: Array, 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(factory: (tui: { requestRender(): void }, theme: unknown, keybindings: unknown, done: (value: T) => void) => unknown): Promise { 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; 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; 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; const projectConfig = JSON.parse(await readFile(join(cwd, ".pi", "fast-browser.json"), "utf8")) as Record; 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); });