openclaw / src /cli /program /action-reparse.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
eb3f11e verified
Raw
History Blame Contribute Delete
10.3 kB
// Action reparse tests cover Commander action reparsing for nested CLI commands.
import { Command } from "commander";
import { describe, expect, it, vi } from "vitest";
import { reparseProgramFromActionCommand } from "./action-reparse.js";
import { registerLazyCommand } from "./register-lazy-command.js";
function setRawArgs(command: Command, rawArgs: string[]): void {
(command as Command & { rawArgs: string[] }).rawArgs = rawArgs;
}
async function expectReparseArgv(params: {
parent: Command;
action: Command;
argv: string[];
expected: string[];
}): Promise<void> {
let root = params.parent;
while (root.parent) {
root = root.parent;
}
setRawArgs(root, params.argv);
const parseAsync = vi.spyOn(root, "parseAsync").mockResolvedValue(root);
await reparseProgramFromActionCommand(params.parent, params.action);
expect(parseAsync).toHaveBeenCalledWith(params.expected);
}
describe("reparseProgramFromActionCommand", () => {
it.each([
{ args: ["--", "config", "get", "--help"] },
{ args: ["config", "--", "get", "--help"] },
{ args: ["config", "get", "--", "--help"] },
])("keeps literal flag-looking values through actual lazy reparse: $args", async ({ args }) => {
const program = new Command().name("openclaw").enablePositionalOptions();
const received: string[] = [];
registerLazyCommand({
program,
name: "config",
description: "Read config",
register: () => {
program
.command("config")
.command("get")
.argument("<path>")
.action((value: string) => {
received.push(value);
});
},
});
await program.parseAsync(["node", "openclaw", ...args]);
expect(received).toEqual(["--help"]);
});
it("uses root raw args and reparses the root for nested lazy commands", async () => {
const root = new Command().name("openclaw");
setRawArgs(root, ["node", "openclaw", "workspaces", "audit", "export", "--since", "1"]);
const workspaces = root.command("workspaces");
const audit = workspaces.command("audit");
const exportCommand = audit.command("export");
const parseAsync = vi.spyOn(root, "parseAsync").mockResolvedValue(root);
const auditParseAsync = vi.spyOn(audit, "parseAsync");
await reparseProgramFromActionCommand(audit, exportCommand);
expect(parseAsync).toHaveBeenCalledWith([
"node",
"openclaw",
"workspaces",
"audit",
"export",
"--since",
"1",
]);
expect(auditParseAsync).not.toHaveBeenCalled();
});
it("hoists a trailing lazy-parent option before the loaded command", async () => {
const root = new Command().name("openclaw");
const browser = root.command("browser").option("--browser-profile <name>");
const tabs = browser.command("tabs");
await expectReparseArgv({
parent: browser,
action: tabs,
argv: ["node", "openclaw", "browser", "tabs", "--browser-profile", "remote"],
expected: ["node", "openclaw", "browser", "--browser-profile", "remote", "tabs"],
});
});
it("hoists a lazy-parent short option with an attached required value", async () => {
const root = new Command().name("openclaw");
const browser = root.command("browser").option("-p, --browser-profile <name>");
const tabs = browser.command("tabs");
await expectReparseArgv({
parent: browser,
action: tabs,
argv: ["node", "openclaw", "browser", "tabs", "-premote"],
expected: ["node", "openclaw", "browser", "-premote", "tabs"],
});
});
it("hoists a lazy-parent short option with an attached optional value", async () => {
const root = new Command().name("openclaw");
const browser = root.command("browser").option("-p, --browser-profile [name]");
const tabs = browser.command("tabs");
await expectReparseArgv({
parent: browser,
action: tabs,
argv: ["node", "openclaw", "browser", "tabs", "-premote"],
expected: ["node", "openclaw", "browser", "-premote", "tabs"],
});
});
it("skips root option values that match the parent command name", async () => {
const root = new Command().name("openclaw").option("--profile <name>");
const browser = root.command("browser").option("--browser-profile <name>");
const tabs = browser.command("tabs");
await expectReparseArgv({
parent: browser,
action: tabs,
argv: [
"node",
"openclaw",
"--profile",
"browser",
"browser",
"tabs",
"--browser-profile",
"remote",
],
expected: [
"node",
"openclaw",
"--profile",
"browser",
"browser",
"--browser-profile",
"remote",
"tabs",
],
});
});
it("skips an attached root option value that matches the parent command name", async () => {
const root = new Command().name("openclaw").option("-p, --profile <name>");
const browser = root.command("browser").option("--browser-profile <name>");
const tabs = browser.command("tabs");
await expectReparseArgv({
parent: browser,
action: tabs,
argv: ["node", "openclaw", "-pbrowser", "browser", "tabs", "--browser-profile", "remote"],
expected: ["node", "openclaw", "-pbrowser", "browser", "--browser-profile", "remote", "tabs"],
});
});
it("hoists parent options after nested lazy commands", async () => {
const root = new Command().name("openclaw");
const browser = root.command("browser").option("--browser-profile <name>");
const tab = browser.command("tab");
tab.command("new");
await expectReparseArgv({
parent: browser,
action: tab,
argv: ["node", "openclaw", "browser", "tab", "new", "--browser-profile", "work"],
expected: ["node", "openclaw", "browser", "--browser-profile", "work", "tab", "new"],
});
});
it("leaves a child-owned option collision after the child command", async () => {
const root = new Command().name("openclaw");
const browser = root.command("browser").option("--json");
const extension = browser.command("extension");
extension.command("path");
extension.command("pair").option("--json");
const argv = ["node", "openclaw", "browser", "extension", "pair", "--json"];
await expectReparseArgv({ parent: browser, action: extension, argv, expected: argv });
});
it("leaves a child-owned attached short option after the child command", async () => {
const root = new Command().name("openclaw");
const browser = root.command("browser").option("-p, --browser-profile <name>");
const extension = browser.command("extension");
extension.command("pair").option("-p, --pairing-profile <name>");
const argv = ["node", "openclaw", "browser", "extension", "pair", "-premote"];
await expectReparseArgv({ parent: browser, action: extension, argv, expected: argv });
});
it("preserves an unknown suffix after a child-owned boolean short flag", async () => {
const root = new Command().name("openclaw");
const browser = root.command("browser").option("-p, --browser-profile <name>");
const extension = browser.command("extension");
const pair = extension.command("pair").option("-p, --preview");
const parsed = pair.parseOptions(["-pfoo"]);
expect(parsed.unknown).toEqual(["-foo"]);
expect(pair.opts()).toEqual({ preview: true });
const argv = ["node", "openclaw", "browser", "extension", "pair", "-pfoo"];
await expectReparseArgv({ parent: browser, action: extension, argv, expected: argv });
});
it.each([
{
label: "boolean flags",
retryFlags: "-r, --retry",
tokens: ["-pr"],
expected: { preview: true, retry: true },
},
{
label: "a required attached value",
retryFlags: "-r, --retry <value>",
tokens: ["-prremote"],
expected: { preview: true, retry: "remote" },
},
{
label: "a required separate value",
retryFlags: "-r, --retry <value>",
tokens: ["-pr", "remote"],
expected: { preview: true, retry: "remote" },
},
{
label: "an optional attached value",
retryFlags: "-r, --retry [value]",
tokens: ["-prremote"],
expected: { preview: true, retry: "remote" },
},
{
label: "an optional separate value",
retryFlags: "-r, --retry [value]",
tokens: ["-pr", "remote"],
expected: { preview: true, retry: "remote" },
},
] as const)(
"preserves child-owned short groups with $label",
async ({ retryFlags, tokens, expected }) => {
const root = new Command().name("openclaw");
const browser = root.command("browser").option("-p, --browser-profile <name>");
const extension = browser.command("extension");
const pair = extension.command("pair").option("-p, --preview").option(retryFlags);
const parsed = pair.parseOptions([...tokens]);
expect(parsed.unknown).toEqual([]);
expect(pair.opts()).toEqual(expected);
const argv = ["node", "openclaw", "browser", "extension", "pair", ...tokens];
await expectReparseArgv({ parent: browser, action: extension, argv, expected: argv });
},
);
it("hoists a parent option when only a sibling command owns the same flag", async () => {
const root = new Command().name("openclaw");
const browser = root.command("browser").option("--url <url>");
const cookies = browser.command("cookies");
cookies.command("list");
cookies.command("set").option("--url <url>");
await expectReparseArgv({
parent: browser,
action: cookies,
argv: ["node", "openclaw", "browser", "cookies", "list", "--url", "ws://gateway"],
expected: ["node", "openclaw", "browser", "--url", "ws://gateway", "cookies", "list"],
});
});
it("keeps a missing parent option value after the loaded command", async () => {
const root = new Command().name("openclaw");
const browser = root.command("browser").option("--browser-profile <name>");
const tabs = browser.command("tabs");
const argv = ["node", "openclaw", "browser", "tabs", "--browser-profile"];
await expectReparseArgv({ parent: browser, action: tabs, argv, expected: argv });
});
});