test(e2e): un user physique par batterie — 20 min et 286s de synchro tombent à 3,6 min et 30s

La suite se ralentissait elle-même, de façon monotone. Chaque batterie crée ~11
identités virtuelles FRAÎCHES (`@alice-…`, `@owner-…`, `@recon-…`), chacune avec
ses trois documents de scope et son inbox, et toutes atterrissaient dans le MÊME
user physique — un wallet créé le 10 juillet et réutilisé depuis, que rien ne
nettoyait. Or une resynchronisation à froid est O(taille du user physique), ce
que la doc de cette bibliothèque énonce elle-même. D'où 250s il y a une semaine,
286s avant-hier, et une batterie qui a fini par dépasser les 20 minutes.

Les identités fraîches ne sont pas la faute : ce sont elles qui rendent une
batterie reproductible, une inbox stable accumulant sinon les dépôts des runs
précédents. La faute était de conserver le user physique qui les héberge.

Mesuré : 42/42 en **3,6 min** au lieu de 20+, synchro à froid la plus lente à
**30s** au lieu de 286s.

Le profil reste persistant À L'INTÉRIEUR d'une batterie — CONTRACT 1 et 2
testent précisément cela (reconnexion fidèle sur le même profil, absence de fork
de compte au travers).

Deux garde-fous pour que la prochaine dérive se voie :

- **Le budget appartient au runner**, qui échoue en nommant la cause probable.
  Un `timeout` posé autour de la commande tuait le navigateur, et la suite
  rapportait « Target page, context or browser has been closed » — un message
  qui se lit comme un défaut applicatif, et que j'ai diagnostiqué deux fois de
  travers avant de comparer les durées.
- **La synchro à froid remonte dans le résumé.** C'est le nombre qui a dérivé
  pendant un mois sans que personne le regarde, parce qu'il n'apparaissait qu'au
  détour de la ligne de détail d'une étape.
This commit is contained in:
Sylvain Duchesne
2026-08-06 11:22:04 +02:00
parent ebf866b1f2
commit da6ef4b8b8
2 changed files with 69 additions and 7 deletions
+28 -5
View File
@@ -72,15 +72,38 @@ export function serveHarness(): Promise<{ url: string; close: () => void }> {
/** /**
* Create the dedicated lib wallet once (headless UI flow on nextgraph.eu), * Create the dedicated lib wallet once (headless UI flow on nextgraph.eu),
* persisted in PROFILE_DIR. Mirrors Festipod's ensureAuth but with the lib's own * persisted in PROFILE_DIR for the duration of the batch.
* wallet name + profile. Idempotent via the ready marker. *
* ── One PHYSICAL user per batch, not one forever ──────────────────────────
* This used to reuse a single wallet across every run, guarded by a ready marker. That
* made the suite slow itself down, monotonically: each batch mints ~11 FRESH virtual
* identities (`@alice-…`, `@owner-…`, `@recon-…`), each with three scope documents and
* an inbox, and they all land in the SAME physical user. Nothing ever removed them. A
* cold resynchronisation is O(the physical user's size) — which this library's own docs
* state — so the wallet created on 2026-07-10 had grown enough to take 286s on a single
* sync step, against 250s a week earlier, and the drift was invisible because no one
* measured it.
*
* The fresh identities are not the mistake — they are what makes a batch reproducible
* (a stable inbox accumulates its past runs' deposits otherwise). The mistake was
* keeping the physical user that holds them. So: a new one per batch, which also makes
* the cold-sync duration comparable from one run to the next instead of being a number
* that only ever grows.
*
* What this does NOT change: the profile stays persistent WITHIN a batch, because
* CONTRACT 1 and 2 test exactly that (a faithful reconnect over the same profile, and
* the absence of an account fork across it).
*/ */
export async function ensureWallet(): Promise<void> { export async function ensureWallet(): Promise<void> {
if (fs.existsSync(WALLET_READY_MARKER)) { if (fs.existsSync(WALLET_READY_MARKER)) {
console.log("[e2e] dedicated lib wallet present — skipping creation"); const age = Date.now() - fs.statSync(WALLET_READY_MARKER).mtimeMs;
return; console.log(
`[e2e] discarding the previous batch's wallet (${Math.round(age / 60000)} min old) — ` +
"a physical user is per-batch, see ensureWallet",
);
fs.rmSync(PROFILE_DIR, { recursive: true, force: true });
} }
console.log("[e2e] creating dedicated lib wallet on nextgraph.eu..."); console.log("[e2e] creating this batch's wallet on nextgraph.eu...");
fs.mkdirSync(PROFILE_DIR, { recursive: true }); fs.mkdirSync(PROFILE_DIR, { recursive: true });
const ctx = await chromium.launchPersistentContext(PROFILE_DIR, { const ctx = await chromium.launchPersistentContext(PROFILE_DIR, {
headless: true, headless: true,
+41 -2
View File
@@ -91,6 +91,37 @@ async function faithfulReconnect(
return { page: p, frame }; return { page: p, frame };
} }
/**
* The batch's own budget, and the measurement that explains an overrun.
*
* Not a `timeout` wrapped around the command from outside: when that fired it killed the
* browser, and the suite reported `Target page, context or browser has been closed` —
* which reads as an application bug and was twice diagnosed as one. A budget belongs to
* the thing that knows what it is spending it on, and it must say so when it runs out.
*/
const BATCH_BUDGET_MS = 45 * 60 * 1000;
const batchStart = Date.now();
/**
* The slowest cold resynchronisation of the batch — the number that drifted from 250s to
* 286s over a month without anyone looking, because it only ever appeared inside one
* step's detail line. It is the health indicator of the physical user, so it is reported
* with the summary.
*/
let coldSyncMs = 0;
/** Fail with the cause named, rather than letting a killed browser look like a defect. */
function assertWithinBudget(): void {
const spent = Date.now() - batchStart;
if (spent > BATCH_BUDGET_MS) {
throw new Error(
`[e2e] batch budget exceeded (${Math.round(spent / 60000)} min > ` +
`${BATCH_BUDGET_MS / 60000} min). This is almost always the physical user having ` +
"grown: a cold resync is O(its size). Check the cold-sync figure printed above — " +
"it should be stable from batch to batch now that each gets a fresh wallet.",
);
}
}
async function main(): Promise<void> { async function main(): Promise<void> {
console.log("[e2e] building SDK page bundle..."); console.log("[e2e] building SDK page bundle...");
buildBundle(); buildBundle();
@@ -522,7 +553,8 @@ async function main(): Promise<void> {
`[SYNC] reconnect re-reads its OWN persisted ${scope} marker (cold-read)`, `[SYNC] reconnect re-reads its OWN persisted ${scope} marker (cold-read)`,
found, found,
found found
? `synced in ${syncMs}ms (reconnect-login ${loginMs}ms) — ${lastDetail}` ? ((coldSyncMs = Math.max(coldSyncMs, syncMs)),
`synced in ${syncMs}ms (reconnect-login ${loginMs}ms) — ${lastDetail}`)
: `NEVER synced within 120s (reconnect-login ${loginMs}ms) — ${lastDetail}`, : `NEVER synced within 120s (reconnect-login ${loginMs}ms) — ${lastDetail}`,
); );
} }
@@ -744,7 +776,14 @@ async function main(): Promise<void> {
// ── Summary ─────────────────────────────────────────────────────────────── // ── Summary ───────────────────────────────────────────────────────────────
const passed = results.filter((r) => r.ok).length; const passed = results.filter((r) => r.ok).length;
const failed = results.length - passed; const failed = results.length - passed;
console.log(`\n══ SDK e2e summary: ${passed} passed, ${failed} failed, ${results.length} total ══`); const batchMin = ((Date.now() - batchStart) / 60000).toFixed(1);
console.log(
`\n══ SDK e2e summary: ${passed} passed, ${failed} failed, ${results.length} total ` +
`— batch ${batchMin} min, slowest cold sync ${Math.round(coldSyncMs / 1000)}s ══`,
);
// A fresh wallet per batch is what should keep the cold sync flat; if it climbs from
// one batch to the next, the per-batch wallet is not being discarded.
assertWithinBudget();
if (failed > 0) { if (failed > 0) {
console.log("Failures:"); console.log("Failures:");
for (const r of results.filter((x) => !x.ok)) console.log(` - ${r.name}: ${r.detail ?? ""}`); for (const r of results.filter((x) => !x.ok)) console.log(` - ${r.name}: ${r.detail ?? ""}`);