mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 15:11:32 +00:00
498 lines
16 KiB
TypeScript
498 lines
16 KiB
TypeScript
// Tests CLI entrypoint argument handling and startup behavior.
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { tryHandlePrecomputedCommandHelpFastPath, tryHandleRootHelpFastPath } from "./entry.js";
|
|
|
|
describe("entry root help fast path", () => {
|
|
it.each([
|
|
{ name: "long root help", argv: ["node", "openclaw", "--help"] },
|
|
{ name: "short root help", argv: ["node", "openclaw", "-h"] },
|
|
{
|
|
name: "profile-prefixed root help",
|
|
argv: ["node", "openclaw", "--profile", "work", "--help"],
|
|
},
|
|
{
|
|
name: "no-color-prefixed root help",
|
|
argv: ["node", "openclaw", "--no-color", "--help"],
|
|
},
|
|
])("respects the startup help fast path kill switch for $name", async ({ argv }) => {
|
|
const outputPrecomputedRootHelpText = vi.fn(() => true);
|
|
const outputRootHelp = vi.fn();
|
|
const loadRootHelpRenderOptionsForConfigSensitivePlugins = vi.fn(async () => null);
|
|
|
|
await expect(
|
|
tryHandleRootHelpFastPath(argv, {
|
|
env: { OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH: "1" },
|
|
outputPrecomputedRootHelpText,
|
|
outputRootHelp,
|
|
loadRootHelpRenderOptionsForConfigSensitivePlugins,
|
|
}),
|
|
).resolves.toBe(false);
|
|
|
|
expect(loadRootHelpRenderOptionsForConfigSensitivePlugins).not.toHaveBeenCalled();
|
|
expect(outputPrecomputedRootHelpText).not.toHaveBeenCalled();
|
|
expect(outputRootHelp).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("respects the process env startup help fast path kill switch", async () => {
|
|
const original = process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH;
|
|
const outputPrecomputedRootHelpText = vi.fn(() => true);
|
|
const outputRootHelp = vi.fn();
|
|
const loadRootHelpRenderOptionsForConfigSensitivePlugins = vi.fn(async () => null);
|
|
process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH = "1";
|
|
|
|
try {
|
|
await expect(
|
|
tryHandleRootHelpFastPath(["node", "openclaw", "--help"], {
|
|
outputPrecomputedRootHelpText,
|
|
outputRootHelp,
|
|
loadRootHelpRenderOptionsForConfigSensitivePlugins,
|
|
}),
|
|
).resolves.toBe(false);
|
|
|
|
expect(loadRootHelpRenderOptionsForConfigSensitivePlugins).not.toHaveBeenCalled();
|
|
expect(outputPrecomputedRootHelpText).not.toHaveBeenCalled();
|
|
expect(outputRootHelp).not.toHaveBeenCalled();
|
|
} finally {
|
|
if (original === undefined) {
|
|
delete process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH;
|
|
} else {
|
|
process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH = original;
|
|
}
|
|
}
|
|
});
|
|
|
|
it("prefers precomputed root help text when available", async () => {
|
|
let outputPrecomputedRootHelpTextCalls = 0;
|
|
|
|
const handled = await tryHandleRootHelpFastPath(["node", "openclaw", "--help"], {
|
|
env: {},
|
|
outputPrecomputedRootHelpText: () => {
|
|
outputPrecomputedRootHelpTextCalls += 1;
|
|
return true;
|
|
},
|
|
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => null,
|
|
});
|
|
|
|
expect(handled).toBe(true);
|
|
expect(outputPrecomputedRootHelpTextCalls).toBe(1);
|
|
});
|
|
|
|
it("renders root help without importing the full program", async () => {
|
|
let outputRootHelpCalls = 0;
|
|
|
|
const handled = await tryHandleRootHelpFastPath(["node", "openclaw", "--help"], {
|
|
outputRootHelp: () => {
|
|
outputRootHelpCalls += 1;
|
|
},
|
|
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => null,
|
|
env: {},
|
|
});
|
|
|
|
expect(handled).toBe(true);
|
|
expect(outputRootHelpCalls).toBe(1);
|
|
});
|
|
|
|
it("renders live root help when plugin config changes command descriptors", async () => {
|
|
let outputPrecomputedRootHelpTextCalls = 0;
|
|
const outputRootHelpOptions: unknown[] = [];
|
|
const liveOptions = {
|
|
config: {
|
|
plugins: {
|
|
slots: {
|
|
memory: "memory-lancedb",
|
|
},
|
|
},
|
|
},
|
|
env: {},
|
|
};
|
|
|
|
const handled = await tryHandleRootHelpFastPath(["node", "openclaw", "--help"], {
|
|
env: {},
|
|
outputPrecomputedRootHelpText: () => {
|
|
outputPrecomputedRootHelpTextCalls += 1;
|
|
return true;
|
|
},
|
|
outputRootHelp: (options) => {
|
|
outputRootHelpOptions.push(options);
|
|
},
|
|
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => liveOptions,
|
|
});
|
|
|
|
expect(handled).toBe(true);
|
|
expect(outputPrecomputedRootHelpTextCalls).toBe(0);
|
|
expect(outputRootHelpOptions).toEqual([liveOptions]);
|
|
});
|
|
|
|
it("structures root help rendering failures for JSON console style", async () => {
|
|
const logging = await import("./logging.js");
|
|
logging.setLoggerOverride({ level: "silent", consoleLevel: "info", consoleStyle: "json" });
|
|
const stderrSpy = vi
|
|
.spyOn(process.stderr, "write")
|
|
.mockImplementation(() => true as unknown as ReturnType<typeof process.stderr.write>);
|
|
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
|
|
throw new Error(`exit ${String(code)}`);
|
|
}) as typeof process.exit);
|
|
|
|
try {
|
|
await expect(
|
|
tryHandleRootHelpFastPath(["node", "openclaw", "--help"], {
|
|
env: {},
|
|
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => ({
|
|
config: {},
|
|
env: {},
|
|
}),
|
|
outputRootHelp: () => {
|
|
throw new Error("render failed");
|
|
},
|
|
}),
|
|
).rejects.toThrow("exit 1");
|
|
const line = stderrSpy.mock.calls.map(([value]) => String(value)).join("");
|
|
expect(JSON.parse(line)).toMatchObject({
|
|
level: "error",
|
|
message: expect.stringContaining("Failed to display help"),
|
|
});
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
} finally {
|
|
logging.resetLogger();
|
|
vi.restoreAllMocks();
|
|
}
|
|
});
|
|
|
|
it("ignores non-root help invocations", async () => {
|
|
let outputRootHelpCalls = 0;
|
|
|
|
const handled = await tryHandleRootHelpFastPath(["node", "openclaw", "status", "--help"], {
|
|
outputRootHelp: () => {
|
|
outputRootHelpCalls += 1;
|
|
},
|
|
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => null,
|
|
env: {},
|
|
});
|
|
|
|
expect(handled).toBe(false);
|
|
expect(outputRootHelpCalls).toBe(0);
|
|
});
|
|
|
|
it("skips the host help fast path when a container target is active", async () => {
|
|
let outputRootHelpCalls = 0;
|
|
|
|
const handled = await tryHandleRootHelpFastPath(
|
|
["node", "openclaw", "--container", "demo", "--help"],
|
|
{
|
|
outputRootHelp: () => {
|
|
outputRootHelpCalls += 1;
|
|
},
|
|
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => null,
|
|
env: {},
|
|
},
|
|
);
|
|
|
|
expect(handled).toBe(false);
|
|
expect(outputRootHelpCalls).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("entry precomputed command help fast path", () => {
|
|
it("renders browser help from startup metadata without importing the full program", async () => {
|
|
let outputPrecomputedBrowserHelpTextCalls = 0;
|
|
|
|
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
|
["node", "openclaw", "browser", "--help"],
|
|
{
|
|
env: {},
|
|
outputPrecomputedBrowserHelpText: () => {
|
|
outputPrecomputedBrowserHelpTextCalls += 1;
|
|
return true;
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(handled).toBe(true);
|
|
expect(outputPrecomputedBrowserHelpTextCalls).toBe(1);
|
|
});
|
|
|
|
it("renders secrets help from startup metadata without importing the full program", async () => {
|
|
let outputPrecomputedSecretsHelpTextCalls = 0;
|
|
|
|
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
|
["node", "openclaw", "secrets", "--help"],
|
|
{
|
|
env: {},
|
|
outputPrecomputedSecretsHelpText: () => {
|
|
outputPrecomputedSecretsHelpTextCalls += 1;
|
|
return true;
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(handled).toBe(true);
|
|
expect(outputPrecomputedSecretsHelpTextCalls).toBe(1);
|
|
});
|
|
|
|
it("renders nodes help from startup metadata without importing the full program", async () => {
|
|
let outputPrecomputedNodesHelpTextCalls = 0;
|
|
|
|
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
|
["node", "openclaw", "nodes", "--help"],
|
|
{
|
|
env: {},
|
|
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => null,
|
|
outputPrecomputedNodesHelpText: () => {
|
|
outputPrecomputedNodesHelpTextCalls += 1;
|
|
return true;
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(handled).toBe(true);
|
|
expect(outputPrecomputedNodesHelpTextCalls).toBe(1);
|
|
});
|
|
|
|
it.each(["doctor", "gateway", "plugins", "sessions", "tasks"])(
|
|
"renders precomputed %s help from startup metadata without importing the full program",
|
|
async (commandName) => {
|
|
const outputPrecomputedSubcommandHelpTextCalls: string[] = [];
|
|
|
|
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
|
["node", "openclaw", commandName, "--help"],
|
|
{
|
|
env: {},
|
|
outputPrecomputedSubcommandHelpText: (requestedCommandName) => {
|
|
outputPrecomputedSubcommandHelpTextCalls.push(requestedCommandName);
|
|
return true;
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(handled).toBe(true);
|
|
expect(outputPrecomputedSubcommandHelpTextCalls).toEqual([commandName]);
|
|
},
|
|
);
|
|
|
|
it("renders precomputed subcommand help with leading root options", async () => {
|
|
const outputPrecomputedSubcommandHelpTextCalls: string[] = [];
|
|
|
|
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
|
["node", "openclaw", "--profile", "work", "--no-color", "models", "-h"],
|
|
{
|
|
env: {},
|
|
outputPrecomputedSubcommandHelpText: (commandName) => {
|
|
outputPrecomputedSubcommandHelpTextCalls.push(commandName);
|
|
return true;
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(handled).toBe(true);
|
|
expect(outputPrecomputedSubcommandHelpTextCalls).toEqual(["models"]);
|
|
});
|
|
|
|
it("keeps subcommand help fast path strict for extra or mixed flags", async () => {
|
|
const invocations = [
|
|
["node", "openclaw", "doctor", "--version"],
|
|
["node", "openclaw", "gateway", "-V"],
|
|
["node", "openclaw", "doctor", "--help", "--version"],
|
|
["node", "openclaw", "doctor", "--help", "--bogus"],
|
|
["node", "openclaw", "doctor", "--help", "extra"],
|
|
["node", "openclaw", "doctor", "--version", "-h"],
|
|
["node", "openclaw", "--bogus", "doctor", "--help"],
|
|
["node", "openclaw", "gateway", "status", "--help"],
|
|
["node", "openclaw", "status", "--help"],
|
|
];
|
|
let outputPrecomputedSubcommandHelpTextCalls = 0;
|
|
|
|
for (const argv of invocations) {
|
|
const handled = await tryHandlePrecomputedCommandHelpFastPath(argv, {
|
|
env: {},
|
|
outputPrecomputedSubcommandHelpText: () => {
|
|
outputPrecomputedSubcommandHelpTextCalls += 1;
|
|
return true;
|
|
},
|
|
});
|
|
|
|
expect(handled).toBe(false);
|
|
}
|
|
expect(outputPrecomputedSubcommandHelpTextCalls).toBe(0);
|
|
});
|
|
|
|
it("defers nodes help when plugin config can change command metadata", async () => {
|
|
let outputPrecomputedNodesHelpTextCalls = 0;
|
|
let liveConfigChecks = 0;
|
|
|
|
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
|
["node", "openclaw", "nodes", "--help"],
|
|
{
|
|
env: {},
|
|
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => {
|
|
liveConfigChecks += 1;
|
|
return { env: {} };
|
|
},
|
|
outputPrecomputedNodesHelpText: () => {
|
|
outputPrecomputedNodesHelpTextCalls += 1;
|
|
return true;
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(handled).toBe(false);
|
|
expect(liveConfigChecks).toBe(1);
|
|
expect(outputPrecomputedNodesHelpTextCalls).toBe(0);
|
|
});
|
|
|
|
it("falls through when startup metadata is unavailable", async () => {
|
|
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
|
["node", "openclaw", "secrets", "--help"],
|
|
{
|
|
env: {},
|
|
outputPrecomputedSecretsHelpText: () => false,
|
|
},
|
|
);
|
|
|
|
expect(handled).toBe(false);
|
|
});
|
|
|
|
it("falls through when startup metadata loading fails", async () => {
|
|
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
|
["node", "openclaw", "secrets", "--help"],
|
|
{
|
|
env: {},
|
|
outputPrecomputedSecretsHelpText: () => {
|
|
throw new Error("startup metadata failed");
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(handled).toBe(false);
|
|
});
|
|
|
|
it("falls through when the nodes live-config probe fails", async () => {
|
|
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
|
["node", "openclaw", "nodes", "--help"],
|
|
{
|
|
env: {},
|
|
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => {
|
|
throw new Error("live config failed");
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(handled).toBe(false);
|
|
});
|
|
|
|
it("ignores nested subcommand help invocations", async () => {
|
|
let outputPrecomputedNodesHelpTextCalls = 0;
|
|
|
|
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
|
["node", "openclaw", "nodes", "invoke", "--help"],
|
|
{
|
|
env: {},
|
|
outputPrecomputedNodesHelpText: () => {
|
|
outputPrecomputedNodesHelpTextCalls += 1;
|
|
return true;
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(handled).toBe(false);
|
|
expect(outputPrecomputedNodesHelpTextCalls).toBe(0);
|
|
});
|
|
|
|
it("ignores command version invocations", async () => {
|
|
let outputPrecomputedNodesHelpTextCalls = 0;
|
|
|
|
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
|
["node", "openclaw", "nodes", "--version"],
|
|
{
|
|
env: {},
|
|
outputPrecomputedNodesHelpText: () => {
|
|
outputPrecomputedNodesHelpTextCalls += 1;
|
|
return true;
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(handled).toBe(false);
|
|
expect(outputPrecomputedNodesHelpTextCalls).toBe(0);
|
|
});
|
|
|
|
it("respects the startup help fast path kill switch", async () => {
|
|
let outputPrecomputedSecretsHelpTextCalls = 0;
|
|
|
|
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
|
["node", "openclaw", "secrets", "--help"],
|
|
{
|
|
env: { OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH: "1" },
|
|
outputPrecomputedSecretsHelpText: () => {
|
|
outputPrecomputedSecretsHelpTextCalls += 1;
|
|
return true;
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(handled).toBe(false);
|
|
expect(outputPrecomputedSecretsHelpTextCalls).toBe(0);
|
|
});
|
|
|
|
it("respects the process env startup help fast path kill switch", async () => {
|
|
let outputPrecomputedSecretsHelpTextCalls = 0;
|
|
const original = process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH;
|
|
process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH = "1";
|
|
try {
|
|
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
|
["node", "openclaw", "secrets", "--help"],
|
|
{
|
|
outputPrecomputedSecretsHelpText: () => {
|
|
outputPrecomputedSecretsHelpTextCalls += 1;
|
|
return true;
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(handled).toBe(false);
|
|
expect(outputPrecomputedSecretsHelpTextCalls).toBe(0);
|
|
} finally {
|
|
if (original === undefined) {
|
|
delete process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH;
|
|
} else {
|
|
process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH = original;
|
|
}
|
|
}
|
|
});
|
|
|
|
it("skips the host command help fast path when a container target is active", async () => {
|
|
let outputPrecomputedSecretsHelpTextCalls = 0;
|
|
|
|
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
|
["node", "openclaw", "--container", "demo", "secrets", "--help"],
|
|
{
|
|
env: {},
|
|
outputPrecomputedSecretsHelpText: () => {
|
|
outputPrecomputedSecretsHelpTextCalls += 1;
|
|
return true;
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(handled).toBe(false);
|
|
expect(outputPrecomputedSecretsHelpTextCalls).toBe(0);
|
|
});
|
|
|
|
it("skips the host command help fast path when a container target comes from env", async () => {
|
|
let outputPrecomputedBrowserHelpTextCalls = 0;
|
|
|
|
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
|
["node", "openclaw", "browser", "--help"],
|
|
{
|
|
env: { OPENCLAW_CONTAINER: "demo" },
|
|
outputPrecomputedBrowserHelpText: () => {
|
|
outputPrecomputedBrowserHelpTextCalls += 1;
|
|
return true;
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(handled).toBe(false);
|
|
expect(outputPrecomputedBrowserHelpTextCalls).toBe(0);
|
|
});
|
|
});
|