Files
dpsapi/e2e/harness/recorder.ts
T
bryanpedini fe0cd4bf6d test(e2e): add playwright e2e harness
Process and API-only Playwright harness: binary and image setup, server/CLI runners, fake Docker Engine socket, read-only database inspection, secret redaction in reports, seeded fuzzing, leak detection and harness self-checks.
2026-10-06 10:52:25 +02:00

99 lines
3.0 KiB
TypeScript

// 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<string, string>;
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<CliRecord, 'at'>): 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<HttpRecord, 'at'>): 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}`);
}
}