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
This commit is contained in:
Peter Steinberger
2026-08-21 01:48:16 -07:00
committed by GitHub
parent 5edc2a7f21
commit 32fb2fc766
6 changed files with 56 additions and 5 deletions
+22
View File
@@ -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({
+9 -4
View File
@@ -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);
+1
View File
@@ -811,6 +811,7 @@ describe("tasks commands", () => {
message: expect.stringContaining("Task not found: missing"),
},
});
expect(jsonLookupRuntime.exit).toHaveBeenCalledWith(1, { resetStream: process.stderr });
});
});
+1 -1
View File
@@ -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;
}
+2
View File
@@ -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;
+21
View File
@@ -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"] },