fix(cli): render skills JSON failures (#127016)

This commit is contained in:
Peter Steinberger
2026-08-20 22:23:52 -07:00
committed by GitHub
parent 3cf33583db
commit 57a2677c3c
2 changed files with 108 additions and 11 deletions
+5 -11
View File
@@ -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<void> {
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 <n>", "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
+103
View File
@@ -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) => {