diff --git a/src/cli/command-execution-startup.test.ts b/src/cli/command-execution-startup.test.ts index b15afaa42dab..9fe3a1dc6ff7 100644 --- a/src/cli/command-execution-startup.test.ts +++ b/src/cli/command-execution-startup.test.ts @@ -56,7 +56,7 @@ describe("command-execution-startup", () => { commandPath: ["status"], startupPolicy: { suppressDoctorStdout: true, - hideBanner: false, + hideBanner: true, skipConfigGuard: true, loadPlugins: false, pluginRegistry: { scope: "channels" }, diff --git a/src/cli/command-startup-policy.test.ts b/src/cli/command-startup-policy.test.ts index f6d5449b4f62..ca78d7ed6f44 100644 --- a/src/cli/command-startup-policy.test.ts +++ b/src/cli/command-startup-policy.test.ts @@ -328,6 +328,9 @@ describe("command-startup-policy", () => { }).hideBanner, ).toBe(true); expect(resolvePolicy({ commandPath: ["status"], env: {} }).hideBanner).toBe(false); + expect( + resolvePolicy({ commandPath: ["status"], jsonOutputMode: true, env: {} }).hideBanner, + ).toBe(true); }); it("uses process env banner suppression when startup env is omitted", () => { @@ -366,7 +369,7 @@ describe("command-startup-policy", () => { }), ).toEqual({ suppressDoctorStdout: true, - hideBanner: false, + hideBanner: true, skipConfigGuard: true, loadPlugins: false, pluginRegistry: { scope: "channels" }, diff --git a/src/cli/command-startup-policy.ts b/src/cli/command-startup-policy.ts index e676586cdc94..ffc67e882bfa 100644 --- a/src/cli/command-startup-policy.ts +++ b/src/cli/command-startup-policy.ts @@ -35,9 +35,10 @@ export function resolveCliStartupPolicy(params: { ? commandPolicy.configGuard({ argv: params.argv ?? [], commandPath: params.commandPath }) : commandPolicy.configGuard; const env = params.env ?? process.env; + const hideBanner = params.jsonOutputMode || commandPolicy.hideBanner; return { suppressDoctorStdout, - hideBanner: isTruthyEnvValue(env.OPENCLAW_HIDE_BANNER) || commandPolicy.hideBanner, + hideBanner: hideBanner || isTruthyEnvValue(env.OPENCLAW_HIDE_BANNER), skipConfigGuard: configGuard === "skip" || (configGuard === "when-suppressed" && suppressDoctorStdout), ...(configGuard === "validate" ? { validateConfigOnly: true } : {}), diff --git a/src/cli/program/preaction.test.ts b/src/cli/program/preaction.test.ts index 70c97d2ce993..b7a9175368d4 100644 --- a/src/cli/program/preaction.test.ts +++ b/src/cli/program/preaction.test.ts @@ -891,6 +891,7 @@ describe("registerPreActionHooks", () => { } expect(routeLogsToStderrMock).toHaveBeenCalledOnce(); + expect(emitCliBannerMock).not.toHaveBeenCalled(); expect(observedMachineOutputStdoutIsTTY).toBe(false); expect(ensureConfigReadyMock).toHaveBeenCalledWith({ runtime: runtimeMock, diff --git a/test/cli-json-stdout.e2e.test.ts b/test/cli-json-stdout.e2e.test.ts index 301c29d5e701..8c206a766973 100644 --- a/test/cli-json-stdout.e2e.test.ts +++ b/test/cli-json-stdout.e2e.test.ts @@ -97,7 +97,29 @@ describe("cli json stdout contract", () => { commander: true, }, { - name: "dual-TTY JSON", + name: "dual-TTY implicit JSON", + args: ["cron", "edit", "job-1", "--enable", "--disable"], + tty: true, + }, + { + name: "dual-TTY automation alias implicit JSON", + args: ["automations", "edit", "job-1", "--enable", "--disable"], + tty: true, + }, + { + name: "dual-TTY implicit JSON command sibling", + args: ["cron", "runs", "--id", "job-1", "--limit", "invalid"], + message: "Invalid --limit (must be a positive integer).", + tty: true, + }, + { + name: "dual-TTY raw-output command sibling", + args: ["cron", "scratch", "job-1", "--set", "updated", "--unset"], + message: "choose only one of --set, --file, or --unset", + tty: true, + }, + { + name: "dual-TTY explicit JSON", args: ["cron", "edit", "job-1", "--enable", "--disable", "--json"], tty: true, }, @@ -107,6 +129,13 @@ describe("cli json stdout contract", () => { message: "--agent must not be blank", human: true, }, + { + name: "dual-TTY human-output sibling", + args: ["cron", "list", "--agent", ""], + message: "--agent must not be blank", + human: true, + tty: true, + }, ])("renders cron edit failures through the shared owner for $name", async (testCase) => { await withTempHome( async (tempHome) => { @@ -139,10 +168,14 @@ describe("cli json stdout contract", () => { : (testCase.message ?? "Choose --enable or --disable, not both"); expect(result.status, result.stderr).toBe(1); - expect(result.stdout, result.stderr).not.toMatch(/[\u001B\u0007]/u); if ("human" in testCase) { - expect(result.stdout).toBe(""); + if ("tty" in testCase) { + expect(result.stdout).toContain("OpenClaw"); + } else { + expect(result.stdout).toBe(""); + } } else { + expect(result.stdout, result.stderr).not.toMatch(/[\u001B\u0007]/u); expect(JSON.parse(result.stdout)).toEqual({ ok: false, error: { type: "cli_error", message }, @@ -155,7 +188,7 @@ describe("cli json stdout contract", () => { expect(result.stderr).not.toContain(gatewayError); await expect(fs.stat(stateDir)).rejects.toMatchObject({ code: "ENOENT" }); } - if ("tty" in testCase) { + if ("tty" in testCase && !("human" in testCase)) { expect(result.stderr).toContain("\u001B[?25h"); } await expect(fs.stat(configPath)).rejects.toMatchObject({ code: "ENOENT" });