test(e2e): le parcours d'un primo-arrivant, et un harnais qui échoue au lieu de se pendre
Aucun parcours n'avait jamais marché le chemin d'un nouvel arrivant : tous pré-injectaient l'identifiant dans l'URL, ce qui fait résoudre l'identité sans jamais afficher la barrière. La suite était verte par-dessus un lien de téléchargement pointant sur un fichier que personne ne servait — et le serveur de test répondait la page HTML de l'application pour tout chemin inconnu, donc un fichier manquant ne POUVAIT pas échouer. Le nouveau parcours part d'un profil vide : barrière, téléchargement réel, import dans l'application portefeuille, saisie de l'identifiant, remise au broker, retour dans l'iframe. Il vérifie neuf points, dont celui qui compte — l'identifiant a survécu et l'identité rapportée est celle qui a été saisie. Le harnais, lui, se pendait au lieu d'échouer. Cause observée : le tuyau devtools de Chromium lâche et Playwright n'émet ni close ni disconnected, si bien que la suite bloquait dans son propre nettoyage sans imprimer ni résumé ni l'échec déjà en route. Toutes les attentes sont désormais bornées et nomment ce qu'elles attendaient ; vérifié en cassant délibérément une attente, et observé en conditions réelles — trois minutes et « gave up waiting for: alice to sign in » là où j'ai tué trois exécutions d'une heure ce matin. Deux exécutions simultanées ne se détruisent plus : verrou atomique sur le profil, et récupération d'un navigateur laissé par une exécution tuée. Le marqueur devient .user-consumed — il n'a jamais attesté d'une disponibilité, seulement qu'un lot avait déjà pris l'utilisateur de ce profil. Au passage, la destruction du profil dépendait du marqueur, écrit en FIN de lot : une exécution tuée avant laissait un profil que la suivante réutilisait, et héritait de sa casse. Elle dépend maintenant du profil. La suite applicative reste non mesurée sur cette machine : un conteneur en boucle de redémarrage recycle son interface réseau, et sept exécutions sur dix échouent sur le transport. Trois sont passées 21/21.
This commit is contained in:
@@ -19,12 +19,17 @@ import type { Frame, Page, BrowserContext } from "playwright";
|
||||
import {
|
||||
buildBundle,
|
||||
serveHarness,
|
||||
closeContext,
|
||||
ensureWallet,
|
||||
launchWalletContext,
|
||||
launchCleanProfileContext,
|
||||
importWalletViaFile,
|
||||
newPage,
|
||||
setupBrokerPage,
|
||||
PROFILE_DIR,
|
||||
} from "./broker";
|
||||
import { armSuiteDeadline, closeQuietly, within } from "./deadline";
|
||||
import { acquireRunLock } from "./run-lock";
|
||||
|
||||
type Check = { name: string; ok: boolean; detail?: string };
|
||||
const results: Check[] = [];
|
||||
@@ -36,27 +41,42 @@ function record(name: string, ok: boolean, detail?: string): void {
|
||||
function check(name: string, cond: boolean, detail?: string): void {
|
||||
record(name, !!cond, detail);
|
||||
}
|
||||
/**
|
||||
* One step of the batch, under its own deadline.
|
||||
*
|
||||
* The bound is what makes the catch honest: recording a FAIL is the right answer for a step
|
||||
* that fails, but a step that never RETURNS is caught by nothing — and from the outside
|
||||
* that is indistinguishable from a machine that has stopped.
|
||||
*/
|
||||
async function step(name: string, fn: () => Promise<void>): Promise<void> {
|
||||
try {
|
||||
await fn();
|
||||
await within(`the step "${name}"`, STEP_MS, fn);
|
||||
} catch (e: any) {
|
||||
record(name, false, "threw: " + String(e?.message ?? e));
|
||||
}
|
||||
}
|
||||
|
||||
// A short helper: call a bridge method inside the iframe.
|
||||
/**
|
||||
* Call a bridge method inside the iframe — under a deadline, because `frame.evaluate()`
|
||||
* has none.
|
||||
*
|
||||
* This is the single most important bound in the file: every one of the thirty-odd
|
||||
* `sdk(...)` calls below is an `evaluate`, and Playwright will wait on one for ever. A
|
||||
* bridge method that never settles — a broker round-trip that gets no answer — used to
|
||||
* stop the batch dead with nothing printed and no way to tell which call it was. The name
|
||||
* carried into the error is the method's own, so the report says which.
|
||||
*/
|
||||
function sdk<T>(frame: Frame, method: string, ...args: unknown[]): Promise<T> {
|
||||
return frame.evaluate(
|
||||
([m, a]) => (window as any).__sdk[m as string](...(a as unknown[])),
|
||||
[method, args] as const,
|
||||
return within(`__sdk.${method}() in the broker iframe`, BRIDGE_MS, () =>
|
||||
frame.evaluate(
|
||||
([m, a]) => (window as any).__sdk[m as string](...(a as unknown[])),
|
||||
[method, args] as const,
|
||||
),
|
||||
) as Promise<T>;
|
||||
}
|
||||
function sdkGet<T>(frame: Frame, method: string, ...args: unknown[]): Promise<T> {
|
||||
// Same as sdk() but for synchronous getters (no await inside the bridge).
|
||||
return frame.evaluate(
|
||||
([m, a]) => (window as any).__sdk[m as string](...(a as unknown[])),
|
||||
[method, args] as const,
|
||||
) as Promise<T>;
|
||||
return sdk<T>(frame, method, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,7 +100,7 @@ async function faithfulReconnect(
|
||||
ctx: BrowserContext,
|
||||
url: string,
|
||||
): Promise<{ page: Page; frame: Frame }> {
|
||||
const p = await ctx.newPage();
|
||||
const p = await newPage("the faithful reconnect", ctx);
|
||||
p.on("pageerror", (e) => console.error("[iframe error:reconnect]", e.message));
|
||||
p.on("console", (m) => {
|
||||
if (m.type() === "error") console.error("[iframe console:reconnect]", m.text());
|
||||
@@ -98,8 +118,26 @@ async function faithfulReconnect(
|
||||
* 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.
|
||||
*
|
||||
* ENFORCED WHILE IT RUNS, not merely checked at the end. Declared as 45 min and only ever
|
||||
* asserted after the last step, it could not fire on the one case that matters — a batch
|
||||
* that never reaches its last step. 25 min is the number now, against a healthy batch of
|
||||
* ~3.3 min: the drift the old figure was sized against (a physical user growing across
|
||||
* batches, so an O(size) cold resync) is gone since each batch mints its own user, and a
|
||||
* budget that cannot interrupt anything is not a budget.
|
||||
*/
|
||||
const BATCH_BUDGET_MS = 45 * 60 * 1000;
|
||||
const BATCH_BUDGET_MS = 25 * 60 * 1000;
|
||||
/**
|
||||
* One `__sdk` bridge call. The slowest measured on this broker is a reconnect read at
|
||||
* ~90-105s (open-repo heal + anti-fork retry + anchored read, all round-tripping); four
|
||||
* minutes is well past that and still names a stuck call in minutes rather than never.
|
||||
*/
|
||||
const BRIDGE_MS = 4 * 60 * 1000;
|
||||
/**
|
||||
* One step. The longest are the reconnect contracts, which poll for up to 120s per scope
|
||||
* on top of a fresh broker login — a few minutes when healthy, ten before we call it stuck.
|
||||
*/
|
||||
const STEP_MS = 10 * 60 * 1000;
|
||||
const batchStart = Date.now();
|
||||
/**
|
||||
* The slowest cold resynchronisation of the batch — the number that drifted from 250s to
|
||||
@@ -123,6 +161,10 @@ function assertWithinBudget(): void {
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
// Before anything touches the shared profile: this batch is about to DELETE it (see
|
||||
// `ensureWallet`), so a second run alive right now would be destroyed by this one.
|
||||
acquireRunLock("the polyfill suite (e2e/run.ts)", PROFILE_DIR);
|
||||
armSuiteDeadline("the polyfill suite", BATCH_BUDGET_MS);
|
||||
console.log("[e2e] building SDK page bundle...");
|
||||
buildBundle();
|
||||
console.log("[e2e] ensuring dedicated lib wallet...");
|
||||
@@ -133,8 +175,8 @@ async function main(): Promise<void> {
|
||||
let ctx: BrowserContext | null = null;
|
||||
let page: Page | null = null;
|
||||
try {
|
||||
ctx = await launchWalletContext();
|
||||
page = await ctx.newPage();
|
||||
ctx = await launchWalletContext("sdk-harness");
|
||||
page = await newPage("the SDK harness", ctx);
|
||||
page.on("pageerror", (e) => console.error("[iframe error]", e.message));
|
||||
page.on("console", (m) => {
|
||||
if (m.type() === "error") console.error("[iframe console]", m.text());
|
||||
@@ -456,7 +498,7 @@ async function main(): Promise<void> {
|
||||
const launched = await launchCleanProfileContext();
|
||||
cleanCtx = launched.ctx;
|
||||
cleanDir = launched.dir;
|
||||
cleanPage = await cleanCtx.newPage();
|
||||
cleanPage = await newPage("the clean-profile session", cleanCtx);
|
||||
cleanPage.on("pageerror", (e) => console.error("[iframe error:clean]", e.message));
|
||||
cleanPage.on("console", (m) => { if (m.type() === "error") console.error("[iframe console:clean]", m.text()); });
|
||||
|
||||
@@ -480,8 +522,8 @@ async function main(): Promise<void> {
|
||||
`rawAnchoredNoOpen=${r.rawRowCount} listed=${r.listedCount} foundEntity=${r.foundEntity} subjects=${r.subjectCount} markerPresent=${r.markerPresent}`,
|
||||
);
|
||||
} finally {
|
||||
try { if (cleanPage) await cleanPage.close(); } catch { /* ignore */ }
|
||||
try { if (cleanCtx) await cleanCtx.close(); } catch { /* ignore */ }
|
||||
if (cleanPage) await closeQuietly("the clean-profile page", () => cleanPage!.close());
|
||||
if (cleanCtx) await closeContext("clean-profile", cleanCtx);
|
||||
try { if (cleanDir) fs.rmSync(cleanDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||
try { fs.rmSync(ngwPath, { force: true }); } catch { /* ignore */ }
|
||||
}
|
||||
@@ -566,7 +608,7 @@ async function main(): Promise<void> {
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
try { if (rp) await rp.close(); } catch { /* ignore */ }
|
||||
if (rp) await closeQuietly("the reconnect page", () => rp!.close());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -623,7 +665,7 @@ async function main(): Promise<void> {
|
||||
: `FORKED — orig pub=${String(orig.docPublic).slice(0, 20)}… got pub=${String(last?.docPublic).slice(0, 20)}… (differs)`,
|
||||
);
|
||||
} finally {
|
||||
try { if (rp) await rp.close(); } catch { /* ignore */ }
|
||||
if (rp) await closeQuietly("the reconnect page", () => rp!.close());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -775,8 +817,10 @@ async function main(): Promise<void> {
|
||||
await sdk(frame, "stateProbeStop");
|
||||
});
|
||||
} finally {
|
||||
try { if (page) await page.close(); } catch { /* ignore */ }
|
||||
try { if (ctx) await ctx.close(); } catch { /* ignore */ }
|
||||
// Bounded, and it has to be: `BrowserContext.close()` on a browser that has already
|
||||
// gone never resolves, and this `finally` is where that hang swallowed the summary.
|
||||
if (page) await closeQuietly("the SDK harness page", () => page!.close());
|
||||
if (ctx) await closeContext("sdk-harness", ctx);
|
||||
closeServer();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user