Files
festipod/scripts/validate.ts
T

170 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bun
/**
* validate.ts — the Festipod validation matrix.
*
* SCOPE (2026-10-09, after the ActivityPods-first switch). Two layers are in
* the gate:
*
* (a) Festipod @ui — the UI layer. The `@ui` tag does not exist in this repo;
* the layer is exactly the complement of the polyfill/broker tags, and its
* command is, verbatim:
* node --import tsx/esm node_modules/@cucumber/cucumber/bin/cucumber.js \
* --config cucumber.json \
* --tags 'not @wip and not @humain and not @data and not @e2e and not @multibrowser'
* `cucumber.json` additionally excludes `@suspendu` and `@e2e-activitypods`
* (Cucumber ANDs the config tags with the CLI ones), so this stays the
* stack-free UI layer exactly as verified at 125 scenarios / 0 failed.
*
* (b) Festipod @e2e-activitypods — the smoke over the LOCAL ActivityPods stack
* (pod provider + Festipod backend + the app served for real). Distinct
* from the suspended `@e2e`: no wallet, no broker, so `cucumber.json`
* excludes it. Its config is `cucumber.smoke.json`; its reproducible setup
* is the concept note `data-layer`, `cookbook_local-activitypods-platform.md`.
* Requires the stack to be UP (the app server and Chromium are started
* here); an unreachable stack fails the layer rather than skipping it.
*
* SUSPENDED, NOT RUN HERE (documented skip, code preserved — nothing deleted):
* - `@data`, `@e2e`, `@multibrowser`, `@shared-wallet`, `@smoke` scenarios are
* polyfill/broker-bound: they sign the shared NextGraph wallet through the
* harness (`src/shared/test-harness/harness-ng.tsx`). They are tagged
* `@suspendu` in the feature files and excluded there, awaiting the NextGraph
* workstream.
* - the SDK's own tests (nextgraph/ng-eventually-js) are NOT Festipod's and are
* no longer run from here (concept `bdd-testing`,
* `rule_tests-validate-festipod-not-the-sdk`).
*
* A smoke layer that cannot reach its stack FAILS, loudly — it never silently
* skips. Exit code is non-zero if any layer fails.
*/
import { spawnSync } from "child_process";
// ─── Config ────────────────────────────────────────────────────────────────
const REPO_DIR = process.cwd();
/** The UI layer, verbatim — see the header. */
const UI_TAGS = "not @wip and not @humain and not @data and not @e2e and not @multibrowser";
/** The ActivityPods smoke, minus its documented suspends. */
const APODS_TAGS = "@e2e-activitypods and not @suspendu";
const TIMEOUT_UI_MS = 15 * 60 * 1000; // 15 min
const TIMEOUT_APODS_MS = 10 * 60 * 1000; // 10 min
// ─── Helpers ───────────────────────────────────────────────────────────────
interface StepResult {
label: string;
status: "passed" | "failed" | "error";
failures: string[];
exitCode: number;
durationMs: number;
}
/** The Cucumber invocation every layer shares (same form as the UI gate command). */
function cucumberArgs(config: string, tags: string): string[] {
return [
"--import",
"tsx/esm",
"node_modules/@cucumber/cucumber/bin/cucumber.js",
"--config",
config,
"--tags",
tags,
];
}
function runStep(label: string, args: string[], timeoutMs: number): StepResult {
const t0 = Date.now();
console.log(`\n${"═".repeat(64)}`);
console.log(`▶ ${label}`);
console.log(` node ${args.join(" ")}`);
console.log(`${"═".repeat(64)}`);
const result = spawnSync("node", args, {
cwd: REPO_DIR,
env: process.env,
encoding: "utf-8",
timeout: timeoutMs,
maxBuffer: 20 * 1024 * 1024,
});
const durationMs = Date.now() - t0;
const combined = (result.stdout ?? "") + "\n" + (result.stderr ?? "");
process.stdout.write(result.stdout ?? "");
process.stderr.write(result.stderr ?? "");
if (result.error) {
return {
label,
status: "error",
failures: [`Process error: ${result.error.message}`],
exitCode: result.status ?? 1,
durationMs,
};
}
return {
label,
status: (result.status ?? 1) === 0 ? "passed" : "failed",
failures: extractFailures(combined),
exitCode: result.status ?? 1,
durationMs,
};
}
/** The failure lines worth surfacing: failed scenario names and the summary. */
function extractFailures(output: string): string[] {
const lines = output.split("\n");
const failures: string[] = [];
for (const line of lines) {
const l = line.trim();
if (/^(✗|✕|×|✖|✘)\s/.test(l)) failures.push(l);
if (/\d+ sc[eé]narios?.*(failed|undefined)/.test(l) && !failures.includes(l)) {
failures.push(l);
}
}
return failures.filter(Boolean);
}
function fmtDuration(ms: number): string {
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
return `${Math.floor(ms / 60_000)}m${((ms % 60_000) / 1000).toFixed(0)}s`;
}
function printMatrix(steps: StepResult[]): void {
console.log("\n");
console.log("╔══════════════════════════════════════════════════════════╗");
console.log("║ VALIDATION MATRIX ║");
console.log("╚══════════════════════════════════════════════════════════╝\n");
const maxLabel = Math.max(...steps.map((s) => s.label.length));
for (const step of steps) {
const icon = step.status === "passed" ? "✅" : step.status === "failed" ? "❌" : "⚠️ ";
console.log(` ${icon} ${step.label.padEnd(maxLabel + 2)} [${fmtDuration(step.durationMs)}]`);
for (const f of step.failures) console.log(` ↳ ${f}`);
}
console.log("");
console.log(" ℹ️ @data/@e2e/@multibrowser/@shared-wallet/@smoke : suspendus (workstream NextGraph).");
const failed = steps.filter((s) => s.status !== "passed");
console.log(failed.length === 0
? " 🟢 ALL LAYERS PASSED"
: ` 🔴 ${failed.length} LAYER(S) FAILED: ${failed.map((s) => s.label).join(", ")}`);
console.log(` ⏱ Total: ${fmtDuration(steps.reduce((sum, s) => sum + s.durationMs, 0))}`);
console.log("");
}
// ─── Main ──────────────────────────────────────────────────────────────────
function main(): void {
console.log(`🔍 Festipod — validation (repo: ${REPO_DIR})`);
const steps: StepResult[] = [];
steps.push(runStep("festipod:@ui", cucumberArgs("cucumber.json", UI_TAGS), TIMEOUT_UI_MS));
steps.push(runStep("festipod:@e2e-activitypods", cucumberArgs("cucumber.smoke.json", APODS_TAGS), TIMEOUT_APODS_MS));
printMatrix(steps);
process.exit(steps.some((s) => s.status !== "passed") ? 1 : 0);
}
main();