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;