diff --git a/src/cli/program.smoke.test.ts b/src/cli/program.smoke.test.ts index c86a2651af3..259dd3b0360 100644 --- a/src/cli/program.smoke.test.ts +++ b/src/cli/program.smoke.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { configureCommand, ensureConfigReady, @@ -26,15 +26,20 @@ vi.mock("./config-cli.js", () => ({ const { buildProgram } = await import("./program.js"); describe("cli program (smoke)", () => { + let program = createProgram(); + function createProgram() { return buildProgram(); } async function runProgram(argv: string[]) { - const program = createProgram(); await program.parseAsync(argv, { from: "user" }); } + beforeAll(() => { + program = createProgram(); + }); + beforeEach(() => { vi.clearAllMocks(); runTui.mockResolvedValue(undefined); @@ -42,7 +47,6 @@ describe("cli program (smoke)", () => { }); it("registers memory + status commands", () => { - const program = createProgram(); const names = program.commands.map((command) => command.name()); expect(names).toContain("message"); expect(names).toContain("memory"); diff --git a/src/cli/program/preaction.test.ts b/src/cli/program/preaction.test.ts index 9afed73c09f..bee9ac8fabf 100644 --- a/src/cli/program/preaction.test.ts +++ b/src/cli/program/preaction.test.ts @@ -73,6 +73,9 @@ afterEach(() => { describe("registerPreActionHooks", () => { let program: Command; + let preActionHook: + | ((thisCommand: Command, actionCommand: Command) => Promise | void) + | null = null; function buildProgram() { const program = new Command().name("openclaw"); @@ -104,22 +107,32 @@ describe("registerPreActionHooks", () => { return program; } - async function runCommand( - params: { parseArgv: string[]; processArgv?: string[] }, - program: Command, - ) { + function resolveActionCommand(parseArgv: string[]): Command { + let current = program; + for (const segment of parseArgv) { + const next = current.commands.find((command) => command.name() === segment); + if (!next) { + break; + } + current = next; + } + return current; + } + + async function runPreAction(params: { parseArgv: string[]; processArgv?: string[] }) { process.argv = params.processArgv ?? [...params.parseArgv]; - await program.parseAsync(params.parseArgv, { from: "user" }); + const actionCommand = resolveActionCommand(params.parseArgv); + if (!preActionHook) { + throw new Error("missing preAction hook"); + } + await preActionHook(program, actionCommand); } it("emits banner, resolves config, and enables verbose from --debug", async () => { - await runCommand( - { - parseArgv: ["status"], - processArgv: ["node", "openclaw", "status", "--debug"], - }, - program, - ); + await runPreAction({ + parseArgv: ["status"], + processArgv: ["node", "openclaw", "status", "--debug"], + }); expect(emitCliBannerMock).toHaveBeenCalledWith("9.9.9-test"); expect(setVerboseMock).toHaveBeenCalledWith(true); @@ -132,13 +145,10 @@ describe("registerPreActionHooks", () => { }); it("loads plugin registry for plugin-required commands", async () => { - await runCommand( - { - parseArgv: ["message", "send"], - processArgv: ["node", "openclaw", "message", "send"], - }, - program, - ); + await runPreAction({ + parseArgv: ["message", "send"], + processArgv: ["node", "openclaw", "message", "send"], + }); expect(setVerboseMock).toHaveBeenCalledWith(false); expect(process.env.NODE_NO_WARNINGS).toBe("1"); @@ -150,37 +160,28 @@ describe("registerPreActionHooks", () => { }); it("loads plugin registry for configure command", async () => { - await runCommand( - { - parseArgv: ["configure"], - processArgv: ["node", "openclaw", "configure"], - }, - program, - ); + await runPreAction({ + parseArgv: ["configure"], + processArgv: ["node", "openclaw", "configure"], + }); expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledTimes(1); }); it("skips config guard for doctor command", async () => { - await runCommand( - { - parseArgv: ["doctor"], - processArgv: ["node", "openclaw", "doctor"], - }, - program, - ); + await runPreAction({ + parseArgv: ["doctor"], + processArgv: ["node", "openclaw", "doctor"], + }); expect(ensureConfigReadyMock).not.toHaveBeenCalled(); }); it("skips preaction work when argv indicates help/version", async () => { - await runCommand( - { - parseArgv: ["status"], - processArgv: ["node", "openclaw", "--version"], - }, - program, - ); + await runPreAction({ + parseArgv: ["status"], + processArgv: ["node", "openclaw", "--version"], + }); expect(emitCliBannerMock).not.toHaveBeenCalled(); expect(setVerboseMock).not.toHaveBeenCalled(); @@ -189,43 +190,21 @@ describe("registerPreActionHooks", () => { it("hides banner when OPENCLAW_HIDE_BANNER is truthy", async () => { process.env.OPENCLAW_HIDE_BANNER = "1"; - await runCommand( - { - parseArgv: ["status"], - processArgv: ["node", "openclaw", "status"], - }, - program, - ); + await runPreAction({ + parseArgv: ["status"], + processArgv: ["node", "openclaw", "status"], + }); expect(emitCliBannerMock).not.toHaveBeenCalled(); expect(ensureConfigReadyMock).toHaveBeenCalledTimes(1); }); - it("suppresses doctor stdout for any --json output command", async () => { - await runCommand( - { - parseArgv: ["message", "send", "--json"], - processArgv: ["node", "openclaw", "message", "send", "--json"], - }, - program, - ); - - expect(ensureConfigReadyMock).toHaveBeenCalledWith({ - runtime: runtimeMock, - commandPath: ["message", "send"], - suppressDoctorStdout: true, + it("suppresses doctor stdout for --json output command", async () => { + await runPreAction({ + parseArgv: ["update", "status", "--json"], + processArgv: ["node", "openclaw", "update", "status", "--json"], }); - vi.clearAllMocks(); - - await runCommand( - { - parseArgv: ["update", "status", "--json"], - processArgv: ["node", "openclaw", "update", "status", "--json"], - }, - program, - ); - expect(ensureConfigReadyMock).toHaveBeenCalledWith({ runtime: runtimeMock, commandPath: ["update", "status"], @@ -234,13 +213,10 @@ describe("registerPreActionHooks", () => { }); it("does not treat config set --json (strict-parse alias) as json output mode", async () => { - await runCommand( - { - parseArgv: ["config", "set", "gateway.auth.mode", "{bad", "--json"], - processArgv: ["node", "openclaw", "config", "set", "gateway.auth.mode", "{bad", "--json"], - }, - program, - ); + await runPreAction({ + parseArgv: ["config", "set", "gateway.auth.mode", "{bad", "--json"], + processArgv: ["node", "openclaw", "config", "set", "gateway.auth.mode", "{bad", "--json"], + }); expect(ensureConfigReadyMock).toHaveBeenCalledWith({ runtime: runtimeMock, @@ -250,5 +226,13 @@ describe("registerPreActionHooks", () => { beforeAll(() => { program = buildProgram(); + const hooks = ( + program as unknown as { + _lifeCycleHooks?: { + preAction?: Array<(thisCommand: Command, actionCommand: Command) => Promise | void>; + }; + } + )._lifeCycleHooks?.preAction; + preActionHook = hooks?.[0] ?? null; }); });