Google API errors as typed findings, never raw JSON: cli/lib/google-errors.ts parses SERVICE_DISABLED (api-disabled: service, Cloud project, console page), PERMISSION_DENIED (no-app-access: which app, Users and permissions), 401 / invalid_grant (auth-revoked) and 429 (quota); health / analytics print one line with the URL, raw text in detail and the log; desktop Health tab: the line, 'Turn it on in Google Cloud' and 'Check again', raw text in a collapsed Details; Portfolio shows a Cloud project blocking several apps once. Findings exit codes are results: the sidecar reports outcome (ok / findings / declined / failed) and the command's own error line; scan / check / validate / health exit 1 with findings show and cache the findings; no bare 'Exit code N' anywhere. tests/google-errors.test.ts, boot check errors cases (api-disabled, scan-findings, scan-failed) (pitfall 57)

This commit is contained in:
CRTkafa 2026-10-13 23:20:04 +08:00
parent f69c10d3a0
commit 89256f1e80
28 changed files with 666 additions and 47 deletions

View file

@ -69,6 +69,48 @@ export interface ToolResult {
output: string;
/** Structured result of a --json capable command. */
data?: unknown;
/** What the exit code means: 0 = ok; a documented findings code with the --json result printed (scan / check /
* validate / health exit 1 when they find problems) = findings; 3 = declined; anything else = failed. Missing in
* results cached by older versions (outcomeOf below decides then). */
outcome?: Outcome;
/** failed only: the command's own error line (stderr's last "error: ..." line, else its last meaningful line). */
error?: string;
}
export type Outcome = 'ok' | 'findings' | 'declined' | 'failed';
/** Read tools whose exit code 1 means "found problems" (the result is still printed with --json): by design, so CI fails. */
export const FINDINGS_EXIT: Readonly<Record<string, readonly number[]>> = {
scan: [1], check: [1], validate: [1], health: [1], check_aso: [1], shots_check: [1], catalog_diff: [1], aso_audit: [1],
};
/** lib/ctx DECLINED_EXIT: the owner's answer was missing or "no". */
export const DECLINED_CODE = 3;
/** The outcome of a run (`tool`: the MCP tool name or the desktop method, dots or underscores). */
export function outcomeOf(tool: string, code: number, hasData: boolean): Outcome {
if (code === 0) return 'ok';
if (code === DECLINED_CODE) return 'declined';
if (hasData && (FINDINGS_EXIT[tool.replace(/\./g, '_')] ?? []).includes(code)) return 'findings';
return 'failed';
}
/** Is this line noise rather than a message (blank, a stack frame, a lone brace, a usage line)? */
const noise = (l: string): boolean => !l.trim() || /^\s+at\s/.test(l) || /^[\s{}[\](),;]*$/.test(l) || /^\s*(usage|options|examples?):/i.test(l) || /^\s{2,}--?\w/.test(l);
/** A log line without its "12:00:00 [tag] " prefix. */
const bare = (l: string): string => l.trim().replace(/^\d\d:\d\d:\d\d \[[^\]]+\] /, '').replace(/^\S+ \[[^\]]+\] /, '');
/**
* The command's own error line: the last "error: ..." line of stderr (the CLI's top-level catch), else the first
* meaningful stderr line (unknown command / option: the message comes before the usage), else the last meaningful
* output line. '' when the command said nothing. Pure.
*/
export function errorLine(stderr: string, output = ''): string {
const err = stderr.split('\n');
const tagged = err.filter(l => /^\s*error:\s*\S/i.test(bare(l))).at(-1);
if (tagged) return bare(tagged).replace(/^error:\s*/i, '').slice(0, 500);
const first = err.find(l => !noise(l));
if (first) return bare(first).slice(0, 500);
const last = output.split('\n').filter(l => !noise(l)).at(-1);
return last ? bare(last).slice(0, 500) : '';
}
export interface ProgressParams { requestId: string | number; line?: string; stream?: 'stdout' | 'stderr'; n?: number; total?: number; project?: string }