tooling(validate): sous-ensemble @data clé rapide par défaut + flag --full + budgets

`bun run validate` finissait en timeout (@data complet >14min). Désormais : par
défaut un SOUS-ENSEMBLE CLÉ de 8 scénarios @data (les couvertures des bugs terrain :
inscription, désinscription, isolation, reconnexion, créateur, compteur, auth×2) en
une invocation → finit en ~8m30. Flag `--full` pour toute la suite @data (budget
élargi 35min). Rotation profil avant @data/@multibrowser, matrice + exit non-zéro.

Baseline actuel : 8/8 @data clé VERTS (les fixes watchShape/optimiste/reconnexion/
anti-fork tiennent) ; polyfill:unit 123 ; rouges = SingletonLock (infra) + un
@multibrowser (limite wallet-partagé A/B).

Note : gate pre-push rapide (tsc+build+polyfill unit) ajouté dans .git/hooks/pre-push
(local, non versionné — pour partage : script tracké + install, suivi).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-10 17:06:40 +02:00
parent 91ee3567aa
commit 7dab6e44e2
+121 -28
View File
@@ -1,23 +1,26 @@
#!/usr/bin/env bun
/**
* validate.ts — Full validation matrix (broker-level tests only, no @ui/@e2e).
* validate.ts — Validation matrix (broker-level tests only, no @ui/@e2e).
*
* Runs in sequence:
* Default run (no flags) — key subset only, fast:
* (a) Polyfill unit tests (@ng-eventually/client — bun test)
* (b) Polyfill e2e real-broker (@ng-eventually/client — bun run test:e2e)
* (c) Festipod @data (cucumber --tags @data)
* (b) Polyfill e2e real-broker (@ng-eventually/client — bun run e2e/run.ts)
* (c) Festipod @data KEY SUBSET (cucumber --name regex covering terrain bugs)
* (d) Festipod @multibrowser (cucumber --tags @multibrowser)
* (e) Festipod @wip [informational only, non-blocking]
*
* With --full flag:
* (c) becomes full @data suite (cucumber --tags @data)
*
* Each step runs even if the previous one failed (--bail mode is OFF).
* Exit code is non-zero if any non-informational step has failures.
*
* Profile rotation: both Playwright profiles are rotated before the run
* when their size exceeds BLOAT_THRESHOLD_MB (default 50 MB), to avoid
* the sparql_query hang described in caveat_wallet-bloat-hang.
* Profile rotation: both Playwright profiles are rotated before @data and
* @multibrowser when their size exceeds BLOAT_THRESHOLD_MB (default 50 MB),
* to avoid the sparql_query hang described in caveat_wallet-bloat-hang.
*/
import { spawnSync, execSync } from "child_process";
import { spawnSync } from "child_process";
import * as fs from "fs";
import * as path from "path";
@@ -37,8 +40,41 @@ const POLYFILL_PROFILE = path.join(
/** Rotate profile when it exceeds this many MB (caveat_wallet-bloat-hang). */
const BLOAT_THRESHOLD_MB = 50;
/** Per-step timeout in ms — generous for broker + headless wallet creation. */
const STEP_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
/**
* Per-step timeouts:
* - polyfill unit/e2e: short steps, keep 10 min
* - @data key subset: generous — BeforeAll + 8 scenarios, ~15 min margin
* - @data full: full suite, ~35 min margin
* - @multibrowser: 7 scenarios, ~10 min margin
* - @wip: informational, 10 min
*/
const TIMEOUT_POLYFILL_UNIT_MS = 10 * 60 * 1000; // 10 min
const TIMEOUT_POLYFILL_E2E_MS = 10 * 60 * 1000; // 10 min
const TIMEOUT_DATA_KEY_MS = 15 * 60 * 1000; // 15 min (key subset)
const TIMEOUT_DATA_FULL_MS = 35 * 60 * 1000; // 35 min (--full)
const TIMEOUT_MULTIBROWSER_MS = 10 * 60 * 1000; // 10 min
const TIMEOUT_WIP_MS = 10 * 60 * 1000; // 10 min
/**
* Key-subset --name regex: matches exactly the 8 scenarios that cover the
* known terrain bugs (inscription, désinscription, isolation, reconnexion,
* compteur dérivé, créateur ne participe pas, auth vide, auth distinctes).
*
* Uses a single cucumber invocation so BeforeAll (broker login) runs once.
*
* French accent chars must be URL-safe in the regex — cucumber uses JS
* RegExp, which handles unicode natively; we pass the literal string.
*/
const DATA_KEY_NAME_REGEX = [
"S'inscrire à un événement",
"Se désinscrire d'un événement$",
"Une identité fraîche ne voit pas la participation d'une autre",
"Une page fraîche pour la même identité relit ses propres données",
"Le créateur ne participe pas automatiquement à son événement",
"L'inscription fait converger le compteur dérivé du propriétaire",
"Un portefeuille connecté est vide par défaut",
"Les données du portefeuille sont distinctes des données par défaut",
].join("|");
// ─── Helpers ───────────────────────────────────────────────────────────────
@@ -102,12 +138,14 @@ function runStep(
cmd: string,
args: string[],
cwd: string,
timeoutMs: number,
extraEnv: Record<string, string> = {},
): StepResult {
const t0 = Date.now();
console.log(`\n${"═".repeat(60)}`);
console.log(`${label}`);
console.log(` ${cmd} ${args.join(" ")} (cwd: ${cwd})`);
console.log(` timeout: ${Math.round(timeoutMs / 60000)}min`);
console.log(`${"═".repeat(60)}`);
const env = { ...process.env, ...extraEnv };
@@ -116,7 +154,7 @@ function runStep(
cwd,
env,
encoding: "utf-8",
timeout: STEP_TIMEOUT_MS,
timeout: timeoutMs,
maxBuffer: 20 * 1024 * 1024, // 20MB
});
@@ -197,7 +235,7 @@ function extractFailures(output: string, label: string): string[] {
}
// Cucumber "N scenarios (M failed)" summary
const summary = lines.find((l) =>
/\d+ scenarios?.*(failed|undefined)/.test(l),
/\d+ sc[eé]narios?.*(failed|undefined)/.test(l),
);
if (summary) failures.push(summary.trim());
// Individual scenario fail lines: "✗ Scenario name (features/...)"
@@ -222,10 +260,16 @@ function fmtDuration(ms: number): string {
function printMatrix(
steps: StepResult[],
wipResult: StepResult | null,
fullMode: boolean,
): void {
console.log("\n");
console.log("╔══════════════════════════════════════════════════════════╗");
console.log("║ VALIDATION MATRIX ║");
if (fullMode) {
console.log("║ (mode: --full, @data complet) ║");
} else {
console.log("║ (mode: défaut, sous-ensemble clé) ║");
}
console.log("╚══════════════════════════════════════════════════════════╝");
console.log("");
@@ -267,12 +311,15 @@ function printMatrix(
console.log(` 🔴 ${failed.length} STEP(S) FAILED: ${failed.map((s) => s.label).join(", ")}`);
}
console.log(` ⏱ Total: ${fmtDuration(totalMs)}`);
if (!fullMode) {
console.log(" ️ Pour @data complet : bun run validate -- --full");
}
console.log("");
}
// ─── Cucumber command builder ───────────────────────────────────────────────
function cucumberArgs(tags: string): string[] {
function cucumberArgsByTags(tags: string): string[] {
return [
"--import",
"tsx/esm",
@@ -284,16 +331,38 @@ function cucumberArgs(tags: string): string[] {
];
}
function cucumberArgsByName(nameRegex: string): string[] {
return [
"--import",
"tsx/esm",
"node_modules/.bin/cucumber-js",
"--config",
"cucumber.json",
"--tags",
"@data",
"--name",
nameRegex,
];
}
// ─── Main ──────────────────────────────────────────────────────────────────
async function main(): Promise<void> {
console.log("🔍 Festipod — Full Validation Run");
const args = process.argv.slice(2);
const fullMode = args.includes("--full");
if (fullMode) {
console.log("🔍 Festipod — Full Validation Run (--full : @data complet)");
} else {
console.log("🔍 Festipod — Validation Run (sous-ensemble clé)");
console.log(" Pour @data complet : bun run validate -- --full");
}
console.log(` Festipod: ${FESTIPOD_DIR}`);
console.log(` Polyfill: ${POLYFILL_DIR}`);
console.log("");
// ── Profile rotation ─────────────────────────────────────────────────────
console.log("── Profile rotation check ──────────────────────────────────");
// ── Profile rotation AVANT les étapes broker ──────────────────────────────
console.log("── Profile rotation check (avant @data et @multibrowser) ────");
rotateProfile(FESTIPOD_PROFILE, "festipod");
rotateProfile(POLYFILL_PROFILE, "polyfill-lib");
@@ -301,7 +370,13 @@ async function main(): Promise<void> {
// ── (a) Polyfill unit tests ───────────────────────────────────────────────
steps.push(
runStep("polyfill:unit", "bun", ["test"], POLYFILL_DIR),
runStep(
"polyfill:unit",
"bun",
["test"],
POLYFILL_DIR,
TIMEOUT_POLYFILL_UNIT_MS,
),
);
// ── (b) Polyfill e2e real broker ──────────────────────────────────────────
@@ -311,26 +386,43 @@ async function main(): Promise<void> {
"bun",
["run", "e2e/run.ts"],
POLYFILL_DIR,
TIMEOUT_POLYFILL_E2E_MS,
),
);
// ── (c) Festipod @data ────────────────────────────────────────────────────
steps.push(
runStep(
"festipod:@data",
"node",
cucumberArgs("@data"),
FESTIPOD_DIR,
),
);
if (fullMode) {
// --full : lance tout @data
steps.push(
runStep(
"festipod:@data (complet)",
"node",
cucumberArgsByTags("@data"),
FESTIPOD_DIR,
TIMEOUT_DATA_FULL_MS,
),
);
} else {
// défaut : sous-ensemble clé en UNE invocation (BeforeAll partagé)
steps.push(
runStep(
"festipod:@data (clé)",
"node",
cucumberArgsByName(DATA_KEY_NAME_REGEX),
FESTIPOD_DIR,
TIMEOUT_DATA_KEY_MS,
),
);
}
// ── (d) Festipod @multibrowser ────────────────────────────────────────────
steps.push(
runStep(
"festipod:@multibrowser",
"node",
cucumberArgs("@multibrowser"),
cucumberArgsByTags("@multibrowser"),
FESTIPOD_DIR,
TIMEOUT_MULTIBROWSER_MS,
),
);
@@ -339,12 +431,13 @@ async function main(): Promise<void> {
const wipResult = runStep(
"festipod:@wip",
"node",
cucumberArgs("@wip"),
cucumberArgsByTags("@wip"),
FESTIPOD_DIR,
TIMEOUT_WIP_MS,
);
// ── Matrix ────────────────────────────────────────────────────────────────
printMatrix(steps, wipResult);
printMatrix(steps, wipResult, fullMode);
// ── Exit code ─────────────────────────────────────────────────────────────
const anyFailed = steps.some((s) => s.status !== "passed");