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.
This commit is contained in:
bryanpedini committed 2026-10-06 10:52:25 +02:00
1 parent a02eef13a7
commit fe0cd4bf6d
33 files changed
+6370

No files matched your search

+89
View File
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2026 Bryan Joshua Pedini
/**
* Small deterministic PRNG (sfc32 seeded through a string hash) for reproducible fuzzing.
* The run seed comes from $DPSAPI_E2E_SEED (default DEFAULT_SEED); each test derives its own
* stream from the run seed and its title path, so results do not depend on scheduling.
*/
import crypto from 'node:crypto';
export class Rng {
private a: number;
private b: number;
private c: number;
private d: number;
/** `seed` is any string; equal seeds produce equal streams. */
constructor(readonly seed: string) {
const h = crypto.createHash('sha256').update(seed, 'utf8').digest();
this.a = h.readUInt32LE(0);
this.b = h.readUInt32LE(4);
this.c = h.readUInt32LE(8);
this.d = h.readUInt32LE(12);
for (let i = 0; i < 12; i++) this.nextUint32();
}
/** Independent child stream, e.g. rng.fork('descriptions'). */
fork(label: string): Rng {
return new Rng(`${this.seed}/${label}`);
}
nextUint32(): number {
const t = (((this.a + this.b) >>> 0) + this.d) >>> 0;
this.d = (this.d + 1) >>> 0;
this.a = this.b ^ (this.b >>> 9);
this.b = (this.c + (this.c << 3)) >>> 0;
this.c = ((this.c << 21) | (this.c >>> 11)) >>> 0;
this.c = (this.c + t) >>> 0;
return t;
}
/** Uniform float in [0, 1). */
next(): number {
return this.nextUint32() / 4294967296;
}
/** Uniform integer in [min, max] (inclusive). */
int(min: number, max: number): number {
return min + Math.floor(this.next() * (max - min + 1));
}
bool(pTrue = 0.5): boolean {
return this.next() < pTrue;
}
pick<T>(items: readonly T[]): T {
if (items.length === 0) throw new Error('pick from empty array');
return items[this.int(0, items.length - 1)];
}
/** Fisher–Yates shuffle (returns a new array). */
shuffle<T>(items: readonly T[]): T[] {
const a = [...items];
for (let i = a.length - 1; i > 0; i--) {
const j = this.int(0, i);
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
bytes(n: number): Buffer {
const b = Buffer.alloc(n);
for (let i = 0; i < n; i++) b[i] = this.nextUint32() & 0xff;
return b;
}
/** Random string of `length` characters drawn from `alphabet` (code points). */
string(length: number, alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'): string {
const chars = [...alphabet];
let s = '';
for (let i = 0; i < length; i++) s += chars[this.int(0, chars.length - 1)];
return s;
}
}
/** Seed of a test: `<run seed>::<title path joined by ' > '>`. */
export function testSeed(runSeed: string, titlePath: readonly string[]): string {
return `${runSeed}::${titlePath.join(' > ')}`;
}