openclaw / src /cli /program /help.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
eb3f11e verified
Raw
History Blame Contribute Delete
9.49 kB
// Help tests cover command help generation and inherited help options.
import { Command, CommanderError } from "commander";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { configureProgramHelp } from "./help.js";
import { OpenClawCommand } from "./openclaw-command.js";
const hasEmittedCliBannerMock = vi.hoisted(() => vi.fn(() => false));
const formatCliBannerLineMock = vi.hoisted(() => vi.fn(() => "BANNER-LINE"));
const formatDocsLinkMock = vi.hoisted(() =>
vi.fn((_path: string, full: string) => `https://${full}`),
);
const resolveCommitHashMock = vi.hoisted(() => vi.fn<() => string | null>(() => "abc1234"));
vi.mock("../../../packages/terminal-core/src/links.js", () => ({
formatDocsLink: formatDocsLinkMock,
}));
vi.mock("../../../packages/terminal-core/src/theme.js", () => ({
isRich: () => false,
theme: {
heading: (s: string) => s,
muted: (s: string) => s,
option: (s: string) => s,
command: (s: string) => s,
error: (s: string) => s,
},
}));
vi.mock("../banner.js", () => ({
formatCliBannerLine: formatCliBannerLineMock,
hasEmittedCliBanner: hasEmittedCliBannerMock,
}));
vi.mock("../../infra/git-commit.js", () => ({
resolveCommitHash: resolveCommitHashMock,
}));
vi.mock("./command-registry.js", () => ({
getCoreCliCommandsWithSubcommands: () => ["models", "message"],
}));
vi.mock("./register.subclis.js", () => ({
getSubCliCommandsWithSubcommands: () => ["gateway"],
}));
const testProgramContext = { programVersion: "9.9.9-test" };
describe("configureProgramHelp", () => {
let originalArgv: string[];
let originalSuppressHelpBanner: string | undefined;
beforeEach(() => {
vi.clearAllMocks();
originalArgv = [...process.argv];
originalSuppressHelpBanner = process.env.OPENCLAW_SUPPRESS_HELP_BANNER;
hasEmittedCliBannerMock.mockReturnValue(false);
resolveCommitHashMock.mockReturnValue("abc1234");
delete process.env.OPENCLAW_SUPPRESS_HELP_BANNER;
});
afterEach(() => {
process.argv = originalArgv;
if (originalSuppressHelpBanner === undefined) {
delete process.env.OPENCLAW_SUPPRESS_HELP_BANNER;
} else {
process.env.OPENCLAW_SUPPRESS_HELP_BANNER = originalSuppressHelpBanner;
}
});
function makeProgramWithCommands() {
const program = new Command();
program.command("models").description("models");
program.command("status").description("status");
return program;
}
function captureHelpOutput(program: Command): string {
let output = "";
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(((
chunk: string | Uint8Array,
) => {
output += String(chunk);
return true;
}) as typeof process.stdout.write);
try {
program.outputHelp();
return output;
} finally {
writeSpy.mockRestore();
}
}
function expectVersionExit(params: { expectedVersion: string }) {
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
throw new Error(`exit:${code ?? ""}`);
}) as typeof process.exit);
try {
const program = makeProgramWithCommands();
expect(() => configureProgramHelp(program, testProgramContext)).toThrow("exit:0");
expect(logSpy).toHaveBeenCalledWith(params.expectedVersion);
expect(exitSpy).toHaveBeenCalledWith(0);
} finally {
logSpy.mockRestore();
exitSpy.mockRestore();
}
}
async function parseHelp(argv: string[]) {
process.argv = ["node", "openclaw", ...argv];
let stdout = "";
let stderr = "";
const program = new OpenClawCommand().enablePositionalOptions().exitOverride();
configureProgramHelp(program, testProgramContext);
program.configureOutput({
writeOut: (value) => {
stdout += value;
},
writeErr: (value) => {
stderr += value;
},
});
const plugins = program.command("plugins").description("Manage plugins");
plugins
.command("list")
.description("List plugins")
.action(() => {});
const error = await program.parseAsync(process.argv).catch((cause: unknown) => cause);
expect(error).toBeInstanceOf(CommanderError);
return { error: error as CommanderError, stderr, stdout };
}
it("adds root help hint and marks commands with subcommands", () => {
process.argv = ["node", "openclaw", "--help"];
const program = makeProgramWithCommands();
configureProgramHelp(program, testProgramContext);
const help = captureHelpOutput(program);
expect(help).toContain("Hint: commands suffixed with * have subcommands");
expect(help).toContain("models *");
expect(help).toContain("status");
expect(help).not.toContain("status *");
});
it("includes banner and docs/examples in root help output", () => {
process.argv = ["node", "openclaw", "--help"];
const program = makeProgramWithCommands();
configureProgramHelp(program, testProgramContext);
const help = captureHelpOutput(program);
expect(help).toContain("BANNER-LINE");
const [version, options] = (formatCliBannerLineMock.mock.calls[0] as unknown as
| [string, { mode?: string }]
| undefined) ?? [undefined, undefined];
expect(version).toBe(testProgramContext.programVersion);
expect(options?.mode).toBe("default");
expect(help).toContain("Examples:");
expect(help).toContain("https://docs.openclaw.ai/cli");
});
it("keeps valid root, group, subcommand, short, and help-command output successful", async () => {
const rootHelp = await parseHelp(["--help"]);
const shortHelp = await parseHelp(["-h"]);
const groupHelp = await parseHelp(["plugins", "--help"]);
const subcommandHelp = await parseHelp(["plugins", "list", "--help"]);
const helpCommand = await parseHelp(["help", "plugins"]);
for (const result of [rootHelp, shortHelp, groupHelp, subcommandHelp, helpCommand]) {
expect(result.error.exitCode).toBe(0);
expect(result.stderr).toBe("");
}
expect(rootHelp.error.code).toBe("commander.helpDisplayed");
expect(shortHelp.stdout).toBe(rootHelp.stdout);
expect(groupHelp.stdout).toContain("Usage: openclaw plugins [options] [command]");
expect(subcommandHelp.stdout).toContain("Usage: openclaw plugins list [options]");
expect(helpCommand.stdout).toBe(groupHelp.stdout);
});
it("formats parse errors from the exact Commander command path", async () => {
let stderr = "";
process.argv = ["node", "openclaw", "plugins", "--source", "list", "list", "--wat"];
const program = new OpenClawCommand().enablePositionalOptions().exitOverride();
configureProgramHelp(program, testProgramContext);
program.configureOutput({
writeErr: (value) => {
stderr += value;
},
});
program
.command("plugins")
.option("--source <source>")
.command("list")
.action(() => {});
const firstError = await program.parseAsync(process.argv).catch((error: unknown) => error);
expect(firstError).toBeInstanceOf(CommanderError);
process.argv = ["node", "openclaw", "plugins", "list", "--still-wat"];
const secondError = await program.parseAsync(process.argv).catch((error: unknown) => error);
expect(secondError).toBeInstanceOf(CommanderError);
process.argv = ["node", "openclaw", "plugins", "lis"];
const thirdError = await program.parseAsync(process.argv).catch((error: unknown) => error);
expect(thirdError).toBeInstanceOf(CommanderError);
expect(stderr.match(/Try: openclaw plugins list --help/g)).toHaveLength(2);
expect(stderr).not.toContain("openclaw plugins list list --help");
expect(stderr).toContain("Did you mean this?\n openclaw plugins list\n");
});
it("suppresses banner formatting when parent default help requests it", () => {
process.argv = ["node", "openclaw", "channels"];
process.env.OPENCLAW_SUPPRESS_HELP_BANNER = "1";
const program = makeProgramWithCommands();
configureProgramHelp(program, testProgramContext);
const help = captureHelpOutput(program);
expect(help).not.toContain("BANNER-LINE");
expect(formatCliBannerLineMock).not.toHaveBeenCalled();
});
it("prints version and exits immediately when version flags are present", () => {
process.argv = ["node", "openclaw", "--version"];
expectVersionExit({ expectedVersion: "OpenClaw 9.9.9-test (abc1234)" });
});
it("prints version and exits immediately without commit metadata", () => {
process.argv = ["node", "openclaw", "--version"];
resolveCommitHashMock.mockReturnValue(null);
expectVersionExit({ expectedVersion: "OpenClaw 9.9.9-test" });
});
it("does not treat subcommand --version options as root version requests", () => {
process.argv = ["node", "openclaw", "skills", "verify", "discrawl", "--version", "1.0.0"];
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
throw new Error(`exit:${code ?? ""}`);
}) as typeof process.exit);
try {
const program = makeProgramWithCommands();
expect(() => configureProgramHelp(program, testProgramContext)).not.toThrow();
expect(logSpy).not.toHaveBeenCalled();
expect(exitSpy).not.toHaveBeenCalled();
} finally {
logSpy.mockRestore();
exitSpy.mockRestore();
}
});
});