Rotate the bloated test profile before opening the wallet

The @data suite writes into one wallet that nothing empties, so every run
leaves more behind. Past some volume the broker stops answering in time and
scenarios die on their step timeout, SILENTLY -- nothing in the console. That
failure looks exactly like a bug in whatever was last edited and gets diagnosed
as one: it happened twice in this session before anybody measured the profile,
which had reached 94MB.

Ported from the sibling branch's validate.ts, which already had the remedy and
the threshold. Placed just before ensureAuth rather than at the top of
BeforeAll: that is the branch which actually opens the profile, and the mock
fallback would pay a broker sign-up for nothing.

Two things the comment records rather than hides. Here the rotation resets the
DATA, not just browser state -- nothing serves a shared wallet file in the @data
path, so the wallet is the broker-side one reached through this profile; the
sibling branch documents the opposite, correctly for itself. And it is not free:
a new wallet comes from driving the real broker's sign-up, so each rotation
abandons one there. FESTIPOD_PROFILE_BLOAT_MB=0 opts out.

MEASURED AND UNRESOLVED: one run leaves 54MB, so a 50MB threshold fires every
time. Either it rises or systematic rotation is accepted.
This commit is contained in:
Sylvain Duchesne
2026-10-01 15:45:41 +02:00
parent fd02570b8b
commit 974d251079
+86 -1
View File
@@ -1,6 +1,6 @@
import { Before, After, BeforeAll, AfterAll, Status, setDefaultTimeout } from '@cucumber/cucumber';
import { chromium, type Browser, type BrowserContext, type Page, type Frame } from 'playwright';
import { execSync, spawn, type ChildProcess } from 'child_process';
import { execSync, spawn, spawnSync, type ChildProcess } from 'child_process';
import * as http from 'http';
import * as fs from 'fs';
import * as path from 'path';
@@ -37,6 +37,84 @@ const HARNESS_NG_OUT = path.join('dist', 'test-harness-ng.js');
// Persistent Chromium profile for NG wallet (not the user's daily browser)
const PLAYWRIGHT_PROFILE = path.resolve('.playwright-profile');
/**
* Rotate the profile once it exceeds this many MB. Override with
* FESTIPOD_PROFILE_BLOAT_MB; 0 disables the rotation entirely.
*/
const BLOAT_THRESHOLD_MB = Number(process.env.FESTIPOD_PROFILE_BLOAT_MB ?? 50);
function dirSizeMB(dir: string): number {
if (!fs.existsSync(dir)) return 0;
try {
const result = spawnSync('du', ['-sm', dir], { encoding: 'utf-8' });
const line = result.stdout.trim().split('\n')[0] ?? '';
return parseInt(line.split('\t')[0] ?? '0', 10);
} catch {
return 0;
}
}
/**
* Remove any stale Chromium singleton files from the profile. Chromium refuses to
* launch (ProcessSingleton error) if SingletonLock, SingletonCookie or
* SingletonSocket are left over from a previous crashed run. Idempotent.
*/
function cleanSingletons(profilePath: string): void {
if (!fs.existsSync(profilePath)) return;
for (const name of ['SingletonLock', 'SingletonCookie', 'SingletonSocket']) {
const f = path.join(profilePath, name);
if (!fs.existsSync(f)) continue;
try {
fs.rmSync(f, { force: true });
console.log(`[rotate] removed stale ${name}.`);
} catch {
// Non-fatal: if it cannot be removed, the launch fails with a clear error.
}
}
}
/**
* ROTATE THE TEST PROFILE when it has grown past the threshold.
*
* WHY THIS RUNS UNATTENDED. The @data suite writes into one wallet that nothing
* empties, so every run leaves more behind. Past some volume the broker stops
* answering in time and scenarios die on their step timeout — SILENTLY, nothing
* in the console. That failure looks exactly like a bug in whatever was last
* edited, and gets diagnosed as one: it happened twice in a single session before
* anybody measured the profile. A guard nobody has to remember is the point.
*
* On THIS branch the rotation resets the DATA, not just the browser state:
* nothing here serves a shared wallet file (`ngSession` configures none, and the
* `.ngw` is used only by the @e2e staging build), so the @data wallet is the
* broker-side one reached through this profile's storage. Removing the profile
* takes the `.wallet-ready` marker with it and `ensureAuth` creates a fresh
* wallet. The sibling branch documents the opposite, correctly for itself —
* there the data lives in the served file and survives this.
*
* NOT free, and not quiet about it: the new wallet is created by driving the real
* broker's sign-up, so each rotation leaves an abandoned wallet behind on it.
* That is the price of a measurable suite; `FESTIPOD_PROFILE_BLOAT_MB=0` opts out.
*/
function rotateProfileIfBloated(): void {
if (BLOAT_THRESHOLD_MB <= 0) {
console.log('[rotate] disabled (FESTIPOD_PROFILE_BLOAT_MB=0)');
return;
}
const sizeMB = dirSizeMB(PLAYWRIGHT_PROFILE);
if (sizeMB > BLOAT_THRESHOLD_MB) {
console.log(`[rotate] profile ${sizeMB}MB > ${BLOAT_THRESHOLD_MB}MB — rotating (a NEW broker wallet will be created)`);
try {
fs.rmSync(PLAYWRIGHT_PROFILE, { recursive: true, force: true });
console.log('[rotate] profile removed — it will be recreated, with empty data.');
} catch (e) {
console.warn(`[rotate] failed to remove the profile: ${e}`);
}
return;
}
cleanSingletons(PLAYWRIGHT_PROFILE);
console.log(`[rotate] profile ${sizeMB}MB — below threshold, keeping it.`);
}
let harnessServer: http.Server | null = null;
let harnessPort = 0;
let useRealBroker = false;
@@ -345,6 +423,13 @@ BeforeAll({ timeout: 10 * 60 * 1000 }, async function () {
execSync(`bun build ${HARNESS_NG_ENTRY} --outfile ${HARNESS_NG_OUT} --bundle --format=iife`, { stdio: 'pipe' });
console.log(`[Harness] Built NG (${(fs.statSync(HARNESS_NG_OUT).size / 1024).toFixed(0)} KB)`);
// BEFORE touching the wallet: rotate the profile if it has bloated past the
// point where the broker stops answering in time. Here and not earlier in
// BeforeAll, because this is the branch that actually uses the profile — the
// mock fallback below never opens it, and rotating it would cost a broker
// sign-up for nothing.
rotateProfileIfBloated();
// Ensure wallet exists in persistent profile (opens browser if needed)
await ensureAuth();
useRealBroker = true;