diff --git a/src/cli/skills-cli.ts b/src/cli/skills-cli.ts index 2702424a488b..91adcb47ef65 100644 --- a/src/cli/skills-cli.ts +++ b/src/cli/skills-cli.ts @@ -68,7 +68,7 @@ import type { } from "../skills/workshop/types.js"; import { CONFIG_DIR } from "../utils.js"; import { resolveClawHubRiskAcknowledgementCliOptions } from "./clawhub-risk-acknowledgement.js"; -import { resolveOptionFromCommand } from "./cli-utils.js"; +import { resolveOptionFromCommand, runCommandWithRuntime } from "./cli-utils.js"; import { inheritOptionFromParent } from "./command-options.js"; import { formatCliJsonFailure, rethrowExpectedCliError } from "./failure-output.js"; import { resolveInstallPolicyWarningAcknowledgementCliOptions } from "./install-policy-warning-acknowledgement.js"; @@ -219,13 +219,10 @@ async function runSkillsAction( render: (report: SkillStatusReport) => string, options?: ResolveSkillsWorkspaceOptions, ): Promise { - try { + await runCommandWithRuntime(defaultRuntime, async () => { const report = await loadSkillsStatusReport(options); defaultRuntime.writeStdout(render(report)); - } catch (err) { - defaultRuntime.error(formatErrorMessage(err)); - defaultRuntime.exit(1); - } + }); } function resolveSkillsWorkspaceForCommand( @@ -602,7 +599,7 @@ export function registerSkillsCli(program: Command) { .option("--limit ", "Max results", (value) => parseStrictPositiveIntOption(value, "--limit")) .option("--json", "Output as JSON", false) .action(async (queryParts: string[], opts: { limit?: number; json?: boolean }) => { - try { + await runCommandWithRuntime(defaultRuntime, async () => { const results = await searchSkillsFromClawHub({ query: normalizeOptionalString(queryParts.join(" ")), limit: opts.limit, @@ -627,10 +624,7 @@ export function registerSkillsCli(program: Command) { const trust = isExternalSource ? ` ${CLAWHUB_SKILLS_SH_TRUST_LABEL}` : ""; defaultRuntime.log(`${skillRef}${version} ${displayName}${summary}${trust}`); } - } catch (err) { - defaultRuntime.error(formatErrorMessage(err)); - defaultRuntime.exit(1); - } + }); }); skills diff --git a/test/cli-json-stdout.e2e.test.ts b/test/cli-json-stdout.e2e.test.ts index bb93999bb1ab..4420a455059e 100644 --- a/test/cli-json-stdout.e2e.test.ts +++ b/test/cli-json-stdout.e2e.test.ts @@ -358,6 +358,109 @@ describe("cli json stdout contract", () => { ); }); + it.each([ + { + name: "search with a leaf JSON flag", + args: ["skills", "search", "fixture", "--json"], + message: "ClawHub /api/v1/search failed (400): offline fixture", + }, + { + name: "search with a parent JSON flag", + args: ["skills", "--json", "search", "fixture"], + message: "ClawHub /api/v1/search failed (400): offline fixture", + }, + { + name: "list with a leaf JSON flag", + args: ["skills", "list", "--agent", "", "--json"], + message: "--agent must not be blank", + }, + { + name: "list with a parent JSON flag", + args: ["skills", "--json", "list", "--agent", ""], + message: "--agent must not be blank", + }, + { + name: "info with a leaf JSON flag", + args: ["skills", "info", "fixture", "--agent", "", "--json"], + message: "--agent must not be blank", + }, + { + name: "info with a parent JSON flag", + args: ["skills", "--json", "info", "fixture", "--agent", ""], + message: "--agent must not be blank", + }, + { + name: "check with a leaf JSON flag", + args: ["skills", "check", "--agent", "", "--json"], + message: "--agent must not be blank", + }, + { + name: "check with a parent JSON flag", + args: ["skills", "--json", "check", "--agent", ""], + message: "--agent must not be blank", + }, + { + name: "the default report after its agent flag", + args: ["skills", "--agent", "", "--json"], + message: "--agent must not be blank", + }, + { + name: "the default report before its agent flag", + args: ["skills", "--json", "--agent", ""], + message: "--agent must not be blank", + }, + ])("returns one canonical JSON document when skills $name fails", async (testCase) => { + await withTempHome( + async (tempHome) => { + const preload = `data:text/javascript,${encodeURIComponent( + 'globalThis.fetch = async () => new Response("offline fixture", { status: 400 });', + )}`; + const result = runBuiltCli(tempHome, testCase.args, { + NODE_OPTIONS: `--import=${preload}`, + OPENCLAW_STATE_DIR: path.join(tempHome, "isolated-state"), + OPENCLAW_CONFIG_PATH: path.join(tempHome, "missing-openclaw.json"), + }); + + expect(result.status, result.stderr).toBe(1); + expect(JSON.parse(result.stdout)).toEqual({ + ok: false, + error: { + type: "cli_error", + message: testCase.message, + }, + }); + expect(result.stderr).toContain(testCase.message); + expect(result.stderr.length).toBeLessThan(2_048); + }, + { prefix: "openclaw-skills-json-failure-e2e-" }, + ); + }); + + it.each([ + { name: "off", debug: "0", includesCause: false }, + { name: "on", debug: "1", includesCause: true }, + ])("keeps skills search nested causes behind debug mode ($name)", async (testCase) => { + await withTempHome( + async (tempHome) => { + const preload = `data:text/javascript,${encodeURIComponent( + 'globalThis.fetch = async () => new Response("not-json", { status: 200 });', + )}`; + const result = runBuiltCli(tempHome, ["skills", "search", "fixture"], { + NODE_OPTIONS: `--import=${preload}`, + OPENCLAW_DEBUG: testCase.debug, + OPENCLAW_STATE_DIR: path.join(tempHome, "isolated-state"), + OPENCLAW_CONFIG_PATH: path.join(tempHome, "missing-openclaw.json"), + }); + + expect(result.status, result.stderr).toBe(1); + expect(result.stdout).toBe(""); + expect(result.stderr).toContain("ClawHub /api/v1/search returned malformed JSON"); + expect(result.stderr.includes("Unexpected token")).toBe(testCase.includesCause); + }, + { prefix: "openclaw-skills-human-failure-e2e-" }, + ); + }); + it("returns one canonical document when docs search fails", async () => { await withTempHome( async (tempHome) => {