From 974d25107959cf4b4bd744c36a57a4be467489c8 Mon Sep 17 00:00:00 2001 From: Sylvain Duchesne Date: Thu, 1 Oct 2026 15:45:41 +0200 Subject: [PATCH] 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. --- src/shared/support/hooks.ts | 87 ++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/src/shared/support/hooks.ts b/src/shared/support/hooks.ts index 6725a22..b2c68a1 100644 --- a/src/shared/support/hooks.ts +++ b/src/shared/support/hooks.ts @@ -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;