// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini /** * Per-test record of every external interaction, attached (redacted) to failed tests. */ export type CliVia = 'argv' | 'shell' | 'docker-exec' | 'docker-run'; export interface CliRecord { at: string; via: CliVia; command: string; args: string[]; /** Application-relevant environment (DB_PATH, HTTP_*, LOG_LEVEL and explicit overrides). */ env: Record; exitCode: number | null; signal: string | null; timedOut: boolean; durationMs: number; stdout: string; stderr: string; } export type HttpClientKind = 'playwright' | 'curl' | 'raw'; export interface HttpRecord { at: string; client: HttpClientKind; method: string; url: string; requestHeaders: Array<[string, string]>; requestBodyBytes: number; status?: number; responseHeaders?: Array<[string, string]>; bodyPreview?: string; durationMs: number; /** Transport-level outcome / error text when there was no (complete) response. */ outcome?: string; curlExitCode?: number; } const MAX_TEXT = 16 * 1024; /** Keeps at most the last MAX_TEXT characters. */ export function clip(s: string, max = MAX_TEXT): string { return s.length <= max ? s : `…[${s.length - max} chars clipped]…` + s.slice(-max); } /** * JSON.stringify replacer that renders BigInt values (e.g. audit selections: limits and windows in * ns) as decimal strings instead of throwing "Do not know how to serialize a BigInt". */ export function bigintReplacer(_key: string, value: unknown): unknown { return typeof value === 'bigint' ? value.toString() : value; } /** JSON.stringify that accepts BigInt values (rendered as decimal strings). */ export function safeStringify(value: unknown, space?: number): string { return JSON.stringify(value, bigintReplacer, space); } /** A plain JSON value equal to `value` as serialized by safeStringify (a snapshot: later mutations do not show). */ export function jsonSafe(value: unknown): unknown { const s = safeStringify(value); return s === undefined ? undefined : JSON.parse(s); } const MAX_RECORDS = 2000; export class Recorder { readonly cli: CliRecord[] = []; readonly http: HttpRecord[] = []; readonly notes: string[] = []; /** JSON-safe snapshots of the recorded fuzz cases (see FuzzRecorder.record). */ readonly fuzzCases: unknown[] = []; droppedCli = 0; droppedHttp = 0; addCli(r: Omit): void { if (this.cli.length >= MAX_RECORDS) { this.cli.shift(); this.droppedCli++; } this.cli.push({ at: new Date().toISOString(), ...r, stdout: clip(r.stdout), stderr: clip(r.stderr) }); } addHttp(r: Omit): void { if (this.http.length >= MAX_RECORDS) { this.http.shift(); this.droppedHttp++; } this.http.push({ at: new Date().toISOString(), ...r, bodyPreview: r.bodyPreview === undefined ? undefined : clip(r.bodyPreview, 2048) }); } note(text: string): void { this.notes.push(`${new Date().toISOString()} ${text}`); } }