fix(cli): preserve gateway startup policy with parent options (#117675)

* fix(cli): use parsed command path for startup

Fixes #117674

* style(cli): format startup regression
This commit is contained in:
Peter Steinberger
2026-08-01 16:48:27 -07:00
committed by GitHub
parent 4153451a9a
commit 91c9d8f8d5
5 changed files with 62 additions and 16 deletions
+13 -3
View File
@@ -123,11 +123,21 @@ describe("command-execution-startup", () => {
).toBe(true);
});
it("uses the resolved action command path for protocol startup policy", () => {
it("uses the resolved action command path for every execution startup decision", () => {
const context = mod.resolveCliExecutionStartupContext({
argv: ["node", "openclaw", "gateway", "--token", "secret", "call", "health"],
commandPath: ["gateway", "call"],
jsonOutputMode: false,
env: {},
});
expect(context.invocation.commandPath).toEqual(["gateway", "secret"]);
expect(context.commandPath).toEqual(["gateway", "call"]);
expect(
mod.resolveCliExecutionStartupContext({
argv: ["node", "openclaw", "acp", "--token", "-secret"],
protocolCommandPath: ["acp"],
commandPath: ["acp"],
jsonOutputMode: false,
env: {},
}).startupPolicy.suppressDoctorStdout,
@@ -135,7 +145,7 @@ describe("command-execution-startup", () => {
expect(
mod.resolveCliExecutionStartupContext({
argv: ["node", "openclaw", "acp", "--verbose", "client"],
protocolCommandPath: ["acp", "client"],
commandPath: ["acp", "client"],
jsonOutputMode: false,
env: {},
}).startupPolicy.suppressDoctorStdout,
+4 -4
View File
@@ -16,21 +16,21 @@ const hasVersionFlag = (argv: readonly string[]) =>
export function resolveCliExecutionStartupContext(params: {
argv: string[];
protocolCommandPath?: string[];
commandPath?: string[];
jsonOutputMode: boolean;
env?: NodeJS.ProcessEnv;
routeMode?: boolean;
}) {
// Resolve argv once so startup policy, routing, and bootstrap share the same command path.
const invocation = resolveCliArgvInvocation(params.argv);
const { commandPath } = invocation;
// Commander owns the action path after parsing option values. Route-first
// callers omit it and keep using raw argv discovery.
const commandPath = params.commandPath ?? invocation.commandPath;
return {
invocation,
commandPath,
startupPolicy: resolveCliStartupPolicy({
argv: params.argv,
commandPath,
protocolCommandPath: params.protocolCommandPath,
jsonOutputMode: params.jsonOutputMode,
env: params.env,
routeMode: params.routeMode,
+1 -7
View File
@@ -28,19 +28,13 @@ function shouldLoadPlugins(params: {
export function resolveCliStartupPolicy(params: {
argv?: string[];
commandPath: string[];
protocolCommandPath?: string[];
jsonOutputMode: boolean;
env?: NodeJS.ProcessEnv;
routeMode?: boolean;
}) {
const commandPolicy = resolveCliCommandPathPolicy(params.commandPath);
// Commander resolves required option values before selecting the action command, so this path
// remains authoritative when a protocol option value itself begins with "-".
const ownsProtocolStdout = params.protocolCommandPath
? resolveCliCommandPathPolicy(params.protocolCommandPath).ownsProtocolStdout
: commandPolicy.ownsProtocolStdout;
// Protocol commands own stdout from process startup, before their action installs later routing.
const suppressDoctorStdout = params.jsonOutputMode || ownsProtocolStdout;
const suppressDoctorStdout = params.jsonOutputMode || commandPolicy.ownsProtocolStdout;
const env = params.env ?? process.env;
return {
suppressDoctorStdout,
+43 -1
View File
@@ -3,6 +3,7 @@ import { Command } from "commander";
import { repoInstallSpec } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { loggingState } from "../../logging/state.js";
import { shouldMigrateStateFromPath } from "../argv.js";
import { isConfigSetJsonParseOnly } from "../config-output-mode.js";
import { setCommandJsonMode } from "./json-mode.js";
import { applyParentDefaultHelpAction } from "./parent-default-help.js";
@@ -149,7 +150,7 @@ describe("registerPreActionHooks", () => {
| null = null;
function buildProgram() {
const programLocal = new Command().name("openclaw");
const programLocal = new Command().name("openclaw").enablePositionalOptions();
const agent = programLocal
.command("agent")
.argument("[note]")
@@ -181,6 +182,8 @@ describe("registerPreActionHooks", () => {
.action(() => {});
const gateway = programLocal
.command("gateway")
.option("--port <port>")
.option("--token <token>")
.option("--allow-unconfigured")
.option("--force")
.option("--reset")
@@ -191,6 +194,15 @@ describe("registerPreActionHooks", () => {
.option("--force")
.option("--reset")
.action(() => {});
gateway
.command("call")
.argument("<method>")
.option("--json")
.action(() => {});
gateway
.command("health")
.option("--json")
.action(() => {});
programLocal
.command("backup")
.command("create")
@@ -890,6 +902,36 @@ describe("registerPreActionHooks", () => {
});
});
it.each([
{
name: "keeps a remote call migration-free",
argv: ["node", "openclaw", "gateway", "--token", "secret", "call", "health", "--json"],
expectedPath: ["gateway", "call"],
expectedMigration: false,
},
{
name: "keeps health on the invalid-config allowlist",
argv: ["node", "openclaw", "gateway", "--port", "19083", "health", "--json"],
expectedPath: ["gateway", "health"],
expectedMigration: true,
},
])("uses the Commander path past parent option values and $name", async (testCase) => {
const parseProgram = buildProgram();
process.argv = testCase.argv;
await parseProgram.parseAsync(process.argv);
const bootstrap = ensureConfigReadyMock.mock.calls.at(-1)?.[0];
expect(bootstrap).toEqual({
runtime: runtimeMock,
commandPath: testCase.expectedPath,
suppressDoctorStdout: true,
});
expect(shouldMigrateStateFromPath(bootstrap?.commandPath ?? [])).toBe(
testCase.expectedMigration,
);
});
it("does not preload plugins for agents list JSON output", async () => {
await runPreAction({
parseArgv: ["agents", "list"],
+1 -1
View File
@@ -121,7 +121,7 @@ export function registerPreActionHooks(program: Command, programVersion: string)
applyResolvedCommandOutputMode(jsonOutputMode);
const { commandPath, startupPolicy } = resolveCliExecutionStartupContext({
argv,
protocolCommandPath: getCommanderCommandPath(actionCommand),
commandPath: getCommanderCommandPath(actionCommand),
jsonOutputMode,
env: process.env,
});