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.
99 lines
3.0 KiB
TypeScript
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}`);
|
|
}
|
|
}
|