From 914363fbfbdfa203e7c3ec79783816598f0167d5 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 11 Nov 2026 17:10:58 +0800 Subject: [PATCH] fix(browser-scenario): summarize all captured numeric statuses (#9936) * fix(browser-scenario): summarize all captured numeric statuses * test(browser-scenario): position status regression before view groups --- .../analysisView/BrowserScenarioViews.test.ts | 55 +++++++++++++++++++ .../browserScenarioNetworkView.ts | 30 ++++++---- 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/application/analysisView/BrowserScenarioViews.test.ts b/src/application/analysisView/BrowserScenarioViews.test.ts index 95b1949096..82b2c3f9a3 100644 --- a/src/application/analysisView/BrowserScenarioViews.test.ts +++ b/src/application/analysisView/BrowserScenarioViews.test.ts @@ -1,4 +1,8 @@ import { createHash } from "node:crypto"; +import { Ajv2020 } from "ajv/dist/2020.js"; +import { z } from "zod"; + +import { ANALYSIS_VIEW_TOOL_CONTRACTS } from "../../contracts/analysisViewToolContracts.js"; import { describe, expect, it } from "vitest"; @@ -79,6 +83,57 @@ const view = async ( return projected.value.normalized_result as Record; }; +it("summarizes every three-digit status bucket within the advertised output contract", async () => { + const contract = ANALYSIS_VIEW_TOOL_CONTRACTS.find( + ({ name }) => name === "inspect_analysis_view", + ); + if (contract === undefined) + throw new Error("Missing inspect_analysis_view contract"); + const validateOutput = new Ajv2020({ + strict: false, + validateFormats: false, + inlineRefs: false, + code: { optimize: false }, + }).compile(z.toJSONSchema(contract.outputSchema)); + for (const [status, statusClass] of [ + [100, "1xx"], + [200, "2xx"], + [300, "3xx"], + [400, "4xx"], + [599, "5xx"], + [600, "6xx"], + [700, "7xx"], + [800, "8xx"], + [999, "9xx"], + ] as const) { + const capture = scenarioCaptureFixture([ + { method: "GET", url: "/status", status }, + ]); + const original = JSON.stringify(capture); + const result = await inspectAnalysisView({ + source: { kind: "inline", evidence: scenarioCaptureEvidence(capture) }, + view: { kind: "summary" }, + }); + if (!result.ok) throw result.error; + expect(result.value.normalized_result).toMatchObject({ + summary: { + network: { + by_status_class: [{ status_class: statusClass, transactions: 1 }], + }, + }, + }); + expect(contract.outputSchema.safeParse(result.value).success).toBe(true); + expect( + validateOutput(result.value), + JSON.stringify(validateOutput.errors), + ).toBe(true); + expect(JSON.stringify(capture)).toBe(original); + expect(capture.events.items).toContainEqual( + expect.objectContaining({ kind: "response", status }), + ); + } +}); + describe("browser scenario analysis views", () => { it("counts inherited Evidence limitations alongside capture-local caveats", async () => { const capture = scenarioCaptureFixture(); diff --git a/src/domain/analysisView/browserScenarioNetworkView.ts b/src/domain/analysisView/browserScenarioNetworkView.ts index 34be1d06ea..5d0aca32b3 100644 --- a/src/domain/analysisView/browserScenarioNetworkView.ts +++ b/src/domain/analysisView/browserScenarioNetworkView.ts @@ -4,15 +4,23 @@ import { isImmutableJsonSnapshot } from "../immutableJson.js"; import { compareAsciiStrings } from "../canonicalOrdering.js"; const count = z.int().nonnegative(); -const statusClass = z.enum([ - "1xx", - "2xx", - "3xx", - "4xx", - "5xx", - "failed", - "unknown", -]); +const statusClass = z + .enum([ + "1xx", + "2xx", + "3xx", + "4xx", + "5xx", + "6xx", + "7xx", + "8xx", + "9xx", + "failed", + "unknown", + ]) + .describe( + "Numeric hundreds bucket of the recorded three-digit status. The 6xx through 9xx buckets retain nonstandard source values without assigning standard HTTP semantics.", + ); /** Counts concern retained transaction identities, including those without a request event. */ export const browserScenarioNetworkSummarySchema = z.strictObject({ @@ -107,9 +115,7 @@ export const browserScenarioTransactionGroups = ( : null, status_class: group.statuses.size === 1 && status !== undefined - ? (`${String(Math.floor(status / 100))}xx` as z.infer< - typeof statusClass - >) + ? statusClass.parse(`${String(Math.floor(status / 100))}xx`) : group.statuses.size === 0 && group.failed ? "failed" : "unknown",