mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fada067277
* feat(browser): add zero-click extension bootstrap Pre-register deterministic path-derived extension IDs and install a strict native messaging host. Keep the popup and options UI minimal while removing the obsolete copilot and page-share flows. * fix(browser): satisfy native bootstrap CI guards * test(browser): isolate native bootstrap Chrome roots * test(browser): flush native bootstrap profile before status * test(browser): seed Linux native bootstrap identity * fix(browser): preserve native bootstrap upgrade safety Allow immutable root-owned package inputs while keeping mutable state, manifests, and launchers user-owned. Preserve all retired copilot keys whenever active or unrecognized recovery custody remains. * fix(browser): preserve pending copilot custody Retired cleanup now removes copilot state only when the durable registry is exactly empty. Any session, archive, malformed value, future shape, or read failure preserves every retired key. * fix(browser): guard native bootstrap upgrades Fail closed while retired copilot custody remains and make discard durable across partial failures. Require exact launcher-embedded origins and repair full launcher drift without accepting mismatched registrations. * fix(browser): remove stale layout export * chore(release): leave changelog to release flow
300 lines
11 KiB
TypeScript
300 lines
11 KiB
TypeScript
// Browser tests cover browser cli.lazy plugin behavior.
|
|
import { Command } from "commander";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { isBrowserMachineOutput } from "../../cli-output-mode.js";
|
|
|
|
const manageMocks = vi.hoisted(() => {
|
|
const doctorAction = vi.fn();
|
|
const openAction = vi.fn();
|
|
const startAction = vi.fn();
|
|
const statusAction = vi.fn();
|
|
const tabNewAction = vi.fn();
|
|
const tabsAction = vi.fn();
|
|
const registerBrowserManageCommands = vi.fn((browser: Command) => {
|
|
browser.command("start").description("Start browser").action(startAction);
|
|
browser.command("status").description("Show browser status").action(statusAction);
|
|
browser.command("tabs").description("List tabs").action(tabsAction);
|
|
browser
|
|
.command("tab")
|
|
.description("Tab shortcuts")
|
|
.command("new")
|
|
.description("Open a new tab")
|
|
.action(tabNewAction);
|
|
browser.command("open").description("Open URL").argument("<url>").action(openAction);
|
|
browser
|
|
.command("doctor")
|
|
.description("Check browser plugin readiness")
|
|
.option("--deep", "Run a live snapshot probe")
|
|
.action(doctorAction);
|
|
});
|
|
return {
|
|
doctorAction,
|
|
openAction,
|
|
registerBrowserManageCommands,
|
|
startAction,
|
|
statusAction,
|
|
tabNewAction,
|
|
tabsAction,
|
|
};
|
|
});
|
|
const inspectMocks = vi.hoisted(() => ({
|
|
registerBrowserInspectCommands: vi.fn(),
|
|
}));
|
|
const actionInputMocks = vi.hoisted(() => ({
|
|
registerBrowserActionInputCommands: vi.fn(),
|
|
}));
|
|
const actionObserveMocks = vi.hoisted(() => ({
|
|
registerBrowserActionObserveCommands: vi.fn(),
|
|
}));
|
|
const debugMocks = vi.hoisted(() => ({
|
|
registerBrowserDebugCommands: vi.fn(),
|
|
}));
|
|
const stateMocks = vi.hoisted(() => ({
|
|
registerBrowserStateCommands: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("./browser-cli-manage.js", () => manageMocks);
|
|
vi.mock("./browser-cli-inspect.js", () => inspectMocks);
|
|
vi.mock("./browser-cli-actions-input.js", () => actionInputMocks);
|
|
vi.mock("./browser-cli-actions-observe.js", () => actionObserveMocks);
|
|
vi.mock("./browser-cli-debug.js", () => debugMocks);
|
|
vi.mock("./browser-cli-state.js", () => stateMocks);
|
|
|
|
const { registerBrowserCli } = await import("./browser-cli.js");
|
|
|
|
function requireFirstCall<TArgs extends unknown[]>(
|
|
mock: { mock: { calls: TArgs[] } },
|
|
label: string,
|
|
): TArgs {
|
|
const [call] = mock.mock.calls;
|
|
if (!call) {
|
|
throw new Error(`expected ${label}`);
|
|
}
|
|
return call;
|
|
}
|
|
|
|
function requireTrailingCommand(args: unknown[], label: string): Command {
|
|
const command = args.at(-1);
|
|
if (!(command instanceof Command)) {
|
|
throw new Error(`expected trailing command for ${label}`);
|
|
}
|
|
return command;
|
|
}
|
|
|
|
describe("registerBrowserCli lazy browser subcommands", () => {
|
|
it.each([
|
|
["evaluate", ["browser", "evaluate", "--fn", "return 1"]],
|
|
["console", ["browser", "console"]],
|
|
["cookies", ["browser", "cookies"]],
|
|
["local storage", ["browser", "storage", "local", "get"]],
|
|
["session storage", ["browser", "storage", "session", "get", "key"]],
|
|
["native host", ["browser", "extension", "native-host"]],
|
|
])("declares default JSON output for %s", (_name, args) => {
|
|
expect(isBrowserMachineOutput({ argv: ["node", "openclaw", ...args] })).toBe(true);
|
|
});
|
|
|
|
it("keeps human browser commands out of machine-output mode", () => {
|
|
expect(isBrowserMachineOutput({ argv: ["node", "openclaw", "browser", "status"] })).toBe(false);
|
|
expect(
|
|
isBrowserMachineOutput({ argv: ["node", "openclaw", "browser", "cookies", "set"] }),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("accepts supported root options after browser", () => {
|
|
expect(
|
|
isBrowserMachineOutput({
|
|
argv: ["node", "openclaw", "browser", "--log-level", "debug", "evaluate"],
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
beforeEach(() => {
|
|
vi.unstubAllEnvs();
|
|
manageMocks.registerBrowserManageCommands.mockClear();
|
|
manageMocks.doctorAction.mockClear();
|
|
manageMocks.openAction.mockClear();
|
|
manageMocks.startAction.mockClear();
|
|
manageMocks.statusAction.mockClear();
|
|
manageMocks.tabNewAction.mockClear();
|
|
manageMocks.tabsAction.mockClear();
|
|
inspectMocks.registerBrowserInspectCommands.mockClear();
|
|
actionInputMocks.registerBrowserActionInputCommands.mockClear();
|
|
actionObserveMocks.registerBrowserActionObserveCommands.mockClear();
|
|
debugMocks.registerBrowserDebugCommands.mockClear();
|
|
stateMocks.registerBrowserStateCommands.mockClear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it("registers browser placeholders without loading handlers for help", () => {
|
|
const program = new Command();
|
|
program.name("openclaw");
|
|
|
|
registerBrowserCli(program, ["node", "openclaw", "browser", "--help"]);
|
|
|
|
const browser = program.commands.find((command) => command.name() === "browser");
|
|
expect(browser?.commands.map((command) => command.name())).toContain("status");
|
|
expect(browser?.commands.map((command) => command.name())).toContain("snapshot");
|
|
const doctor = browser?.commands.find((command) => command.name() === "doctor");
|
|
if (!doctor) {
|
|
throw new Error("expected browser doctor command placeholder");
|
|
}
|
|
expect(doctor.options.map((option) => option.long)).toContain("--deep");
|
|
expect(manageMocks.registerBrowserManageCommands).not.toHaveBeenCalled();
|
|
expect(inspectMocks.registerBrowserInspectCommands).not.toHaveBeenCalled();
|
|
expect(actionInputMocks.registerBrowserActionInputCommands).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("registers only the requested browser group before dispatch", async () => {
|
|
const program = new Command();
|
|
program.name("openclaw");
|
|
|
|
registerBrowserCli(program, ["node", "openclaw", "browser", "status"]);
|
|
|
|
const browser = program.commands.find((command) => command.name() === "browser");
|
|
expect(browser?.commands.map((command) => command.name())).toEqual(["status"]);
|
|
|
|
await program.parseAsync(["browser", "status"], { from: "user" });
|
|
|
|
expect(manageMocks.registerBrowserManageCommands).toHaveBeenCalledTimes(1);
|
|
expect(inspectMocks.registerBrowserInspectCommands).not.toHaveBeenCalled();
|
|
expect(manageMocks.statusAction).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("loads browser doctor from the manage group so --deep is available", async () => {
|
|
const program = new Command();
|
|
program.name("openclaw");
|
|
|
|
registerBrowserCli(program, ["node", "openclaw", "browser", "doctor", "--deep"]);
|
|
|
|
await program.parseAsync(["browser", "doctor", "--deep"], { from: "user" });
|
|
|
|
expect(manageMocks.registerBrowserManageCommands).toHaveBeenCalledTimes(1);
|
|
expect(debugMocks.registerBrowserDebugCommands).not.toHaveBeenCalled();
|
|
expect(manageMocks.doctorAction).toHaveBeenCalledTimes(1);
|
|
const [doctorOptions] = requireFirstCall(manageMocks.doctorAction, "doctor action call");
|
|
expect(doctorOptions.deep).toBe(true);
|
|
});
|
|
|
|
it("preserves parent --json while reparsing lazy manage commands", async () => {
|
|
const program = new Command();
|
|
program.name("openclaw");
|
|
|
|
registerBrowserCli(program, ["node", "openclaw", "browser", "--json", "open", "about:blank"]);
|
|
|
|
await program.parseAsync(["browser", "--json", "open", "about:blank"], { from: "user" });
|
|
|
|
expect(manageMocks.openAction).toHaveBeenCalledTimes(1);
|
|
const openCommand = requireTrailingCommand(
|
|
requireFirstCall(manageMocks.openAction, "open action call"),
|
|
"open action",
|
|
);
|
|
expect(openCommand.parent?.opts().json).toBe(true);
|
|
|
|
const tabsProgram = new Command();
|
|
tabsProgram.name("openclaw");
|
|
registerBrowserCli(tabsProgram, ["node", "openclaw", "browser", "--json", "tabs"]);
|
|
|
|
await tabsProgram.parseAsync(["browser", "--json", "tabs"], { from: "user" });
|
|
|
|
expect(manageMocks.tabsAction).toHaveBeenCalledTimes(1);
|
|
const tabsCommand = requireTrailingCommand(
|
|
requireFirstCall(manageMocks.tabsAction, "tabs action call"),
|
|
"tabs action",
|
|
);
|
|
expect(tabsCommand.parent?.opts().json).toBe(true);
|
|
});
|
|
|
|
it("accepts the shipped trailing browser profile order after lazy loading", async () => {
|
|
const program = new Command().name("openclaw").enablePositionalOptions();
|
|
registerBrowserCli(program, [
|
|
"node",
|
|
"openclaw",
|
|
"browser",
|
|
"tabs",
|
|
"--browser-profile",
|
|
"remote",
|
|
]);
|
|
|
|
await program.parseAsync(["browser", "tabs", "--browser-profile", "remote"], {
|
|
from: "user",
|
|
});
|
|
|
|
const tabsCommand = requireTrailingCommand(
|
|
requireFirstCall(manageMocks.tabsAction, "tabs action call"),
|
|
"tabs action",
|
|
);
|
|
expect(tabsCommand.parent?.opts().browserProfile).toBe("remote");
|
|
});
|
|
|
|
it("skips browser option values when selecting the lazy command group", async () => {
|
|
const program = new Command();
|
|
program.name("openclaw");
|
|
|
|
registerBrowserCli(program, [
|
|
"node",
|
|
"openclaw",
|
|
"browser",
|
|
"--browser-profile",
|
|
"status",
|
|
"start",
|
|
]);
|
|
|
|
const browser = program.commands.find((command) => command.name() === "browser");
|
|
expect(browser?.commands.map((command) => command.name())).toContain("start");
|
|
|
|
await program.parseAsync(["browser", "--browser-profile", "status", "start"], {
|
|
from: "user",
|
|
});
|
|
|
|
expect(manageMocks.registerBrowserManageCommands).toHaveBeenCalledTimes(1);
|
|
expect(manageMocks.startAction).toHaveBeenCalledTimes(1);
|
|
expect(manageMocks.statusAction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("resolves browser parent options for nested commands", async () => {
|
|
const program = new Command();
|
|
program.name("openclaw");
|
|
|
|
registerBrowserCli(program, [
|
|
"node",
|
|
"openclaw",
|
|
"browser",
|
|
"--browser-profile",
|
|
"work",
|
|
"tab",
|
|
"new",
|
|
]);
|
|
|
|
await program.parseAsync(["browser", "--browser-profile", "work", "--json", "tab", "new"], {
|
|
from: "user",
|
|
});
|
|
|
|
expect(manageMocks.tabNewAction).toHaveBeenCalledTimes(1);
|
|
const tabCommand = requireTrailingCommand(
|
|
requireFirstCall(manageMocks.tabNewAction, "tab new action call"),
|
|
"tab new action",
|
|
);
|
|
expect(tabCommand.parent?.parent?.opts()).toMatchObject({ browserProfile: "work", json: true });
|
|
});
|
|
|
|
it("can eagerly register all browser groups for compatibility", async () => {
|
|
vi.stubEnv("OPENCLAW_DISABLE_LAZY_SUBCOMMANDS", "1");
|
|
const program = new Command();
|
|
program.name("openclaw");
|
|
|
|
registerBrowserCli(program, ["node", "openclaw", "browser", "--help"]);
|
|
|
|
await vi.waitFor(() =>
|
|
expect(manageMocks.registerBrowserManageCommands).toHaveBeenCalledTimes(1),
|
|
);
|
|
expect(inspectMocks.registerBrowserInspectCommands).toHaveBeenCalledTimes(1);
|
|
expect(actionInputMocks.registerBrowserActionInputCommands).toHaveBeenCalledTimes(1);
|
|
expect(actionObserveMocks.registerBrowserActionObserveCommands).toHaveBeenCalledTimes(1);
|
|
expect(debugMocks.registerBrowserDebugCommands).toHaveBeenCalledTimes(1);
|
|
expect(stateMocks.registerBrowserStateCommands).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|