import { createHash } from "node:crypto"; import { z } from "zod"; export type AttemptStatus = number | "NETWORK_REJECTION" | "PENDING_ABORT"; export type RetryReason = "NETWORK_FAILURE" | "HTTP_429" | "HTTP_503"; export type BodyDisposition = | "FULLY_READ_WITHIN_BOUND" | "CANCELLED_WITHOUT_READ" | "REJECTED_LIMIT" | "NO_RESPONSE"; const attemptStatusSchema = z.union([ z.number().int().min(100).max(599), z.literal("NETWORK_REJECTION"), z.literal("PENDING_ABORT"), ]); export const httpScenarioAssertionGroupsSchema = z .object({ status: z .object({ attempts: z.array(attemptStatusSchema).min(1), final: attemptStatusSchema, }) .strict(), outcome: z.object({ kind: z.string().min(1), detail: z.string().nullable() }).strict(), effect: z.object({ outcome: z.string().min(1), observer: z.string().min(1) }).strict(), retry: z .object({ count: z.number().int().nonnegative(), reasons: z.array(z.enum(["NETWORK_FAILURE", "HTTP_429", "HTTP_503"])), }) .strict(), fetch: z .object({ count: z.number().int().positive(), observerAttempts: z.number().int().positive(), agrees: z.boolean(), }) .strict(), media: z.object({ attempts: z.array(z.string().nullable()).min(1), final: z.string().nullable() }).strict(), body: z .object({ attempts: z .array( z .object({ disposition: z.enum([ "FULLY_READ_WITHIN_BOUND", "CANCELLED_WITHOUT_READ", "REJECTED_LIMIT", "NO_RESPONSE", ]), pulledBytes: z.number().int().nonnegative(), ceiling: z.number().int().nonnegative(), }) .strict(), ) .min(1), }) .strict(), scope: z .object({ start: z.literal("CURRENT"), end: z.enum(["CURRENT", "STALE"]), signal: z.enum(["ACTIVE", "ABORTED"]), cancellationOwner: z.enum(["NONE", "CALLER", "SCOPE_FENCE", "DEADLINE"]), }) .strict(), }) .strict() .superRefine((groups, context) => { const fail = (path: (string | number)[], message: string) => { context.addIssue({ code: "custom", path, message }); }; if (groups.status.final !== groups.status.attempts.at(-1)) { fail(["status", "final"], "must equal the final attempt status"); } if (groups.retry.count !== groups.retry.reasons.length) { fail(["retry", "count"], "must equal retry reasons length"); } if (groups.retry.count !== groups.fetch.count - 1) { fail(["retry", "count"], "must equal the non-final fetch attempt count"); } for (const [index, reason] of groups.retry.reasons.entries()) { const status = groups.status.attempts[index]; const expectedReason = status === "NETWORK_REJECTION" ? "NETWORK_FAILURE" : status === 429 ? "HTTP_429" : status === 503 ? "HTTP_503" : null; if (reason !== expectedReason) { fail( ["retry", "reasons", index], "must match the corresponding non-final attempt status", ); } } if (groups.fetch.count !== groups.status.attempts.length) { fail(["fetch", "count"], "must equal status attempts length"); } if (groups.fetch.count !== groups.media.attempts.length) { fail(["media", "attempts"], "must equal fetch count"); } if (groups.fetch.count !== groups.body.attempts.length) { fail(["body", "attempts"], "must equal fetch count"); } if (groups.fetch.observerAttempts !== groups.fetch.count) { fail(["fetch", "observerAttempts"], "must equal physical fetch count"); } if (!groups.fetch.agrees) { fail(["fetch", "agrees"], "must prove physical/observer agreement"); } if (groups.media.final !== groups.media.attempts.at(-1)) { fail(["media", "final"], "must equal the final attempt media essence"); } }); export type HttpScenarioAssertionGroups = Readonly<{ status: Readonly<{ attempts: readonly AttemptStatus[]; final: AttemptStatus }>; outcome: Readonly<{ kind: string; detail: string | null }>; effect: Readonly<{ outcome: string; observer: string }>; retry: Readonly<{ count: number; reasons: readonly RetryReason[] }>; fetch: Readonly<{ count: number; observerAttempts: number; agrees: boolean; }>; media: Readonly<{ attempts: readonly (string | null)[]; final: string | null; }>; body: Readonly<{ attempts: readonly Readonly<{ disposition: BodyDisposition; pulledBytes: number; ceiling: number; }>[]; }>; scope: Readonly<{ start: "CURRENT"; end: "CURRENT" | "STALE"; signal: "ACTIVE" | "ABORTED"; cancellationOwner: "NONE" | "CALLER" | "SCOPE_FENCE" | "DEADLINE"; }>; }>; export const httpScenarioExpectationSchema = z .object({ executionId: z.string().min(1), operationId: z.string().min(1), scenarioId: z.string().min(1), expected: httpScenarioAssertionGroupsSchema, testDeadlineOverrideMs: z.number().int().positive().nullable(), }) .strict() .superRefine((entry, context) => { if (entry.executionId !== `${entry.operationId}::${entry.scenarioId}`) { context.addIssue({ code: "custom", path: ["executionId"], message: "must equal operationId::scenarioId", }); } }); export type HttpScenarioExpectation = Readonly<{ executionId: string; operationId: string; scenarioId: string; expected: HttpScenarioAssertionGroups; testDeadlineOverrideMs: number | null; }>; export const httpScenarioReceiptRowSchema = z .object({ executionId: z.string().min(1), expected: httpScenarioAssertionGroupsSchema, observed: httpScenarioAssertionGroupsSchema, testDeadlineOverrideMs: z.number().int().positive().nullable(), }) .strict(); export const httpScenarioReceiptSchema = z .object({ schemaVersion: z.number().int().positive(), catalogDigest: z.string().regex(/^sha256:[0-9a-f]{64}$/), catalogTotal: z.number().int().nonnegative(), executedIds: z.array(z.string().min(1)), rows: z.array(httpScenarioReceiptRowSchema), }) .strict() .superRefine((receipt, context) => { const rowIds = receipt.rows.map((row) => row.executionId); const sortedIds = [...receipt.executedIds].sort((left, right) => left.localeCompare(right), ); const sortedRowIds = [...rowIds].sort((left, right) => left.localeCompare(right), ); if (!sameScenarioJson(receipt.executedIds, sortedIds)) { context.addIssue({ code: "custom", path: ["executedIds"], message: "must be sorted by execution ID", }); } if (!sameScenarioJson(rowIds, sortedRowIds)) { context.addIssue({ code: "custom", path: ["rows"], message: "must be sorted by execution ID", }); } if (!sameScenarioJson(receipt.executedIds, rowIds)) { context.addIssue({ code: "custom", path: ["rows"], message: "row IDs must exactly equal executed IDs", }); } }); export type HttpScenarioReceipt = Readonly<{ schemaVersion: number; catalogDigest: string; catalogTotal: number; executedIds: readonly string[]; rows: readonly Readonly<{ executionId: string; expected: HttpScenarioAssertionGroups; observed: HttpScenarioAssertionGroups; testDeadlineOverrideMs: number | null; }>[]; }>; export function stableScenarioJson(value: unknown): string { const normalize = (candidate: unknown): unknown => { if (Array.isArray(candidate)) return candidate.map(normalize); if (candidate && typeof candidate === "object") { return Object.fromEntries( Object.entries(candidate as Readonly>) .sort(([left], [right]) => left.localeCompare(right)) .map(([key, nested]) => [key, normalize(nested)]), ); } return candidate; }; return JSON.stringify(normalize(value)); } export function sameScenarioJson(left: unknown, right: unknown): boolean { return stableScenarioJson(left) === stableScenarioJson(right); } export function computeHttpScenarioCatalogDigest( schemaVersion: number, expectations: readonly HttpScenarioExpectation[], ): `sha256:${string}` { const tuples = [...expectations] .sort((left, right) => left.executionId.localeCompare(right.executionId)) .map((entry) => [ entry.executionId, entry.expected.status, entry.expected.outcome, entry.expected.effect, entry.expected.retry, entry.expected.fetch, entry.expected.media, entry.expected.body, entry.expected.scope, entry.testDeadlineOverrideMs, ]); return `sha256:${createHash("sha256") .update(stableScenarioJson([schemaVersion, ...tuples])) .digest("hex")}`; }