fix(cli): propagate web provider failures (#116674)

This commit is contained in:
Vincent Koc
2026-07-31 13:21:21 +08:00
committed by GitHub
parent c45c318848
commit 59c3606366
2 changed files with 92 additions and 2 deletions

View File

@@ -3409,6 +3409,32 @@ describe("capability cli", () => {
expect(webSearchRuntime.runWebSearch).not.toHaveBeenCalled();
});
it("reports structured web search failures in the envelope and exits nonzero", async () => {
const webSearchRuntime = await import("../web-search/runtime.js");
vi.mocked(webSearchRuntime.runWebSearch).mockResolvedValueOnce({
provider: "kitchen-sink-search",
result: {
ok: false,
statusCode: 429,
error: { code: "rate_limited", message: "Kitchen Sink rate limit." },
results: [],
},
});
await expect(
runCap("capability", "web", "search", "--query", "rate limit", "--json"),
).rejects.toThrow("exit 1");
expect(firstJsonOutput()).toEqual(
expect.objectContaining({
ok: false,
capability: "web.search",
provider: "kitchen-sink-search",
error: "Kitchen Sink rate limit.",
}),
);
});
it("uses the infer web search provider override when resolving SecretRefs", async () => {
const unresolvedConfig = {
tools: { web: { search: { provider: "exa", enabled: true } } },
@@ -3623,6 +3649,33 @@ describe("capability cli", () => {
);
});
it("reports structured web fetch failures in the envelope and exits nonzero", async () => {
const webFetchRuntime = await import("../web-fetch/runtime.js");
vi.mocked(webFetchRuntime.resolveWebFetchDefinition).mockReturnValueOnce({
provider: { id: "kitchen-sink-fetch" },
definition: {
execute: vi.fn(async () => ({
ok: false,
statusCode: 504,
error: { code: "timeout", message: "Kitchen Sink fetch timed out." },
})),
},
} as never);
await expect(
runCap("capability", "web", "fetch", "--url", "kitchen://fixture/timeout", "--json"),
).rejects.toThrow("exit 1");
expect(firstJsonOutput()).toEqual(
expect.objectContaining({
ok: false,
capability: "web.fetch",
provider: "kitchen-sink-fetch",
error: "Kitchen Sink fetch timed out.",
}),
);
});
it("surfaces available, configured, and selected for web providers", async () => {
mocks.loadConfig.mockReturnValue({
tools: {

View File

@@ -25,6 +25,29 @@ import {
resolveLocalCapabilityRuntimeConfig,
} from "./shared.js";
function describeWebResultFailure(result: Record<string, unknown>): string | undefined {
const statusCode =
typeof result.statusCode === "number" && Number.isFinite(result.statusCode)
? result.statusCode
: undefined;
const error = result.error;
const errorMessage =
typeof error === "string"
? error
: error &&
typeof error === "object" &&
typeof (error as { message?: unknown }).message === "string"
? (error as { message: string }).message
: undefined;
if (result.ok !== false && (statusCode === undefined || statusCode < 400) && !errorMessage) {
return undefined;
}
return (
errorMessage ??
(statusCode ? `provider returned status ${statusCode}` : "provider reported failure")
);
}
async function runWebSearchCommand(params: { query: string; provider?: string; limit?: number }) {
const rawConfig = getRuntimeConfig();
const scopedTargets = getCapabilityWebSearchCommandSecretTargets(rawConfig, {
@@ -51,13 +74,15 @@ async function runWebSearchCommand(params: { query: string; provider?: string; l
limit: params.limit,
},
});
const error = describeWebResultFailure(result.result);
return {
ok: true,
ok: error === undefined,
capability: "web.search",
transport: "local" as const,
provider: result.provider,
attempts: [],
outputs: [{ result: result.result }],
...(error ? { error } : {}),
} satisfies CapabilityEnvelope;
}
@@ -89,13 +114,15 @@ async function runWebFetchCommand(params: { url: string; provider?: string; form
url: params.url,
format: params.format,
});
const error = describeWebResultFailure(result);
return {
ok: true,
ok: error === undefined,
capability: "web.fetch",
transport: "local" as const,
provider: resolved.provider.id,
attempts: [],
outputs: [{ result }],
...(error ? { error } : {}),
} satisfies CapabilityEnvelope;
}
@@ -110,14 +137,19 @@ export function registerWebCapabilityCommands(capability: Command): void {
.option("--limit <n>", "Result limit")
.option("--json", "Output JSON", false)
.action(async (opts) => {
let failed = false;
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await runWebSearchCommand({
query: String(opts.query),
provider: opts.provider as string | undefined,
limit: parseOptionalPositiveInteger(opts.limit, "--limit"),
});
failed = !result.ok;
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatEnvelopeForText);
});
if (failed) {
defaultRuntime.exit(1);
}
});
web
@@ -128,14 +160,19 @@ export function registerWebCapabilityCommands(capability: Command): void {
.option("--format <format>", "Format hint")
.option("--json", "Output JSON", false)
.action(async (opts) => {
let failed = false;
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await runWebFetchCommand({
url: String(opts.url),
provider: opts.provider as string | undefined,
format: opts.format as string | undefined,
});
failed = !result.ok;
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatEnvelopeForText);
});
if (failed) {
defaultRuntime.exit(1);
}
});
web