fix(web-probe): reject empty analyze stdout contracts

This commit is contained in:
Codex
2026-07-02 18:36:04 +00:00
parent e2500ba418
commit f37729061d
3 changed files with 65 additions and 9 deletions
@@ -85,6 +85,50 @@ test("child JSON recovery falls back to artifact when stdout is not JSON", () =>
assert.equal(resolved.diagnostics.fallbackReason, "stdout-not-json");
});
test("child JSON recovery rejects ok-only stdout contract and uses artifact", () => {
const resolved = resolveCliChildJsonObject({
stdout: JSON.stringify({ ok: true }),
requestedStdoutType: "web-probe observe analyze compact JSON",
acceptParsed: (value) => typeof value.reportJsonPath === "string" || typeof value.counts === "object",
artifactFallback: {
path: "analysis/report.json",
nextCommand: "collect report",
read: () => ({ ok: true, value: { ok: true, reportJsonPath: "analysis/report.json", counts: { network: 9 } } }),
},
});
assert.equal(resolved.source, "artifact");
assert.equal(resolved.parsed?.reportJsonPath, "analysis/report.json");
assert.deepEqual(resolved.parsed?.counts, { network: 9 });
assert.equal(resolved.diagnostics.stdoutKind, "json");
assert.equal(resolved.diagnostics.fallbackReason, "stdout-json-contract-invalid");
assert.equal(resolved.diagnostics.stdoutContractAccepted, false);
});
test("child JSON recovery drops invalid ok-only stdout when artifact fallback is missing", () => {
const resolved = resolveCliChildJsonObject({
stdout: JSON.stringify({ ok: true }),
requestedStdoutType: "web-probe observe analyze compact JSON",
acceptParsed: (value) => typeof value.reportJsonPath === "string" || typeof value.counts === "object",
artifactFallback: {
path: "analysis/report.json",
nextCommand: "collect report",
read: () => ({ ok: false, reason: "artifact-missing", path: "analysis/report.json" }),
},
});
assert.equal(resolved.parsed, null);
assert.equal(resolved.source, null);
assert.equal(resolved.diagnostics.stdoutKind, "json");
assert.equal(resolved.diagnostics.fallbackReason, "stdout-json-contract-invalid");
assert.equal(resolved.diagnostics.stdoutContractAccepted, false);
const artifact = resolved.diagnostics.artifact as Record<string, unknown>;
assert.equal(artifact.ok, false);
assert.equal(artifact.reason, "artifact-missing");
assert.equal(artifact.path, "analysis/report.json");
assert.equal(artifact.nextCommand, "collect report");
});
test("child JSON recovery reports artifact fallback failure when stdout is unusable and artifact is missing", () => {
const resolved = resolveCliChildJsonObject({
stdout: "not json",
+3 -2
View File
@@ -35,8 +35,8 @@ export function resolveCliChildJsonObject(options: {
?? contractFallbackReason
?? (parsedStdout.parsed === null ? fallbackReasonForStdout(parsedStdout.stdoutKind) : null);
let parsed = parsedStdout.parsed;
let source: CliChildJsonSource = parsedStdout.source;
let parsed = contractFallbackReason === null ? parsedStdout.parsed : null;
let source: CliChildJsonSource = contractFallbackReason === null ? parsedStdout.source : null;
let artifactDiagnostics: Record<string, unknown> | null = null;
if (fallbackReason !== null && options.artifactFallback) {
const artifact = options.artifactFallback.read();
@@ -65,6 +65,7 @@ export function resolveCliChildJsonObject(options: {
fallbackReason,
parsedFromStdout: parsedStdout.source === "stdout",
parsedFromDump: parsedStdout.source === "dump",
stdoutContractAccepted: accepted,
dumpPath: parsedStdout.dumpPath,
dumpReason: parsedStdout.dumpReason,
dumpReadOk: parsedStdout.dumpReadOk,
+18 -7
View File
@@ -2607,13 +2607,24 @@ function webObserveAnalyzeCollectCommand(options: NodeWebProbeObserveOptions, fi
}
function isWebObserveAnalyzeJsonContract(value: Record<string, unknown>): boolean {
return value.ok === true
|| value.ok === false
|| typeof value.reportJsonPath === "string"
|| typeof value.reportMdPath === "string"
|| typeof value.error === "string"
|| Object.keys(recordValue(value.analyzer)).length > 0
|| Object.keys(recordValue(value.counts)).length > 0;
const hasReportPath = typeof value.reportJsonPath === "string" || typeof value.reportMdPath === "string";
const hasAnalyzer = Object.keys(recordValue(value.analyzer)).length > 0;
const hasFindings = arrayRecordsValue(value.findings).length > 0 || arrayRecordsValue(value.archiveRedFindings).length > 0;
const hasAnalyzeData = hasReportPath
|| hasAnalyzer
|| hasFindings
|| Object.keys(recordValue(value.counts)).length > 0
|| Object.keys(recordValue(value.sampleMetrics)).length > 0
|| Object.keys(recordValue(value.requestRate)).length > 0
|| Object.keys(recordValue(value.requestRateCurve)).length > 0
|| Object.keys(recordValue(value.frontendPerformance)).length > 0
|| Object.keys(recordValue(value.webPerformanceRuntimeDiagnostics)).length > 0
|| Object.keys(recordValue(value.runtimeAlerts)).length > 0
|| Object.keys(recordValue(value.archiveSummary)).length > 0
|| Object.keys(recordValue(value.analysisWindow)).length > 0;
const hasFailureData = typeof value.error === "string" || hasAnalyzer || hasFindings || hasReportPath;
if (value.ok === false) return hasFailureData;
return hasAnalyzeData;
}
function compactWebObserveAnalyzePayloadForRaw(payload: Record<string, unknown>, compactRaw: boolean): Record<string, unknown> {