From 32fb2fc766f8ecfee34299e367daf96b20318db6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 21 Aug 2026 01:48:16 -0700 Subject: [PATCH] fix(cli): render task flow JSON failures (#127080) * fix(cli): render task flow JSON failures * test(cli): stabilize entry console capture * fix(cli): keep JSON terminal resets off stdout * fix(cli): keep task JSON resets off stdout --- src/commands/flows.test.ts | 22 ++++++++++++++++++++++ src/commands/flows.ts | 13 +++++++++---- src/commands/tasks.test.ts | 1 + src/commands/tasks.ts | 2 +- src/entry.run-main.test.ts | 2 ++ test/cli-json-stdout.e2e.test.ts | 21 +++++++++++++++++++++ 6 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/commands/flows.test.ts b/src/commands/flows.test.ts index e00bfdf3ea3c..9c871a3d4b00 100644 --- a/src/commands/flows.test.ts +++ b/src/commands/flows.test.ts @@ -318,6 +318,28 @@ describe("flows commands", () => { }); }); + it("keeps terminal reset bytes off stdout for JSON lookup failures", async () => { + await withTaskFlowCommandStateDir(async () => { + const runtime = createRuntime(); + + await flowsShowCommand({ lookup: "missing-flow", json: true }, runtime); + + expect(runtime.error).not.toHaveBeenCalled(); + expect(runtime.writeJson).toHaveBeenCalledWith( + { + ok: false, + error: { + type: "cli_error", + message: + "TaskFlow not found: missing-flow. Run openclaw tasks flow list to see recent flow ids.", + }, + }, + 2, + ); + expect(runtime.exit).toHaveBeenCalledWith(1, { resetStream: process.stderr }); + }); + }); + it("shows one TaskFlow with linked task details in text mode", async () => { await withTaskFlowCommandStateDir(async () => { const flow = createManagedTaskFlow({ diff --git a/src/commands/flows.ts b/src/commands/flows.ts index e00fd9ca9123..a4afea2793c4 100644 --- a/src/commands/flows.ts +++ b/src/commands/flows.ts @@ -7,10 +7,10 @@ import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text import { isRich, theme } from "../../packages/terminal-core/src/theme.js"; import { formatCliCommand } from "../cli/command-format.js"; import { parseCliEnumFilter } from "../cli/enum-filter.js"; +import { formatCliJsonFailure } from "../cli/failure-output.js"; import { getRuntimeConfig } from "../config/config.js"; import { info } from "../globals.js"; -import type { RuntimeEnv } from "../runtime.js"; -import { writeRuntimeJson } from "../runtime.js"; +import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js"; import { listTasksForFlowId } from "../tasks/runtime-internal.js"; import { cancelFlowById, getFlowTaskSummary } from "../tasks/task-executor.js"; import { @@ -211,8 +211,13 @@ export async function flowsShowCommand( ) { const flow = resolveTaskFlowForLookupToken(opts.lookup); if (!flow) { - runtime.error(formatFlowLookupMiss(opts.lookup)); - runtime.exit(1); + const message = formatFlowLookupMiss(opts.lookup); + if (opts.json) { + writeRuntimeJson(runtime, formatCliJsonFailure(message)); + } else { + runtime.error(message); + } + runtime.exit(1, opts.json ? { resetStream: process.stderr } : undefined); return; } const tasks = listTasksForFlowId(flow.flowId); diff --git a/src/commands/tasks.test.ts b/src/commands/tasks.test.ts index 4e4f7dc015e3..3107932cd69e 100644 --- a/src/commands/tasks.test.ts +++ b/src/commands/tasks.test.ts @@ -811,6 +811,7 @@ describe("tasks commands", () => { message: expect.stringContaining("Task not found: missing"), }, }); + expect(jsonLookupRuntime.exit).toHaveBeenCalledWith(1, { resetStream: process.stderr }); }); }); diff --git a/src/commands/tasks.ts b/src/commands/tasks.ts index 477440e7d060..4398526018fc 100644 --- a/src/commands/tasks.ts +++ b/src/commands/tasks.ts @@ -330,7 +330,7 @@ export async function tasksShowCommand( } else { runtime.error(message); } - runtime.exit(1); + runtime.exit(1, opts.json ? { resetStream: process.stderr } : undefined); return; } diff --git a/src/entry.run-main.test.ts b/src/entry.run-main.test.ts index ec5a383ff742..597f41ed34d1 100644 --- a/src/entry.run-main.test.ts +++ b/src/entry.run-main.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { ExpectedCliError } from "./cli/failure-output.js"; import { runMainOrRootHelp } from "./entry.js"; +import { enableConsoleCapture } from "./logging.js"; describe("entry run-main boundary", () => { it("retains JSON console routing through process finalization", async () => { @@ -25,6 +26,7 @@ describe("entry run-main boundary", () => { humanOutput: message, machineOutput: message, }); + enableConsoleCapture(); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); process.exitCode = undefined; diff --git a/test/cli-json-stdout.e2e.test.ts b/test/cli-json-stdout.e2e.test.ts index 4420a455059e..9459e2544813 100644 --- a/test/cli-json-stdout.e2e.test.ts +++ b/test/cli-json-stdout.e2e.test.ts @@ -295,6 +295,27 @@ describe("cli json stdout contract", () => { ); }); + it("renders a missing TaskFlow as one canonical JSON document without stderr", async () => { + await withTempHome( + async (tempHome) => { + const result = runBuiltCli(tempHome, ["tasks", "flow", "show", "missing-flow", "--json"]); + + expect(result.status, result.stderr).toBe(1); + expect(result.stdout, result.stderr).not.toBe(""); + expect(JSON.parse(result.stdout)).toEqual({ + ok: false, + error: { + type: "cli_error", + message: + "TaskFlow not found: missing-flow. Run openclaw tasks flow list to see recent flow ids.", + }, + }); + expect(result.stderr).toBe(""); + }, + { prefix: "openclaw-task-flow-json-failure-e2e-" }, + ); + }); + it.each([ { name: "qr", command: ["qr"] }, { name: "clawbot qr", command: ["clawbot", "qr"] },