Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-onboarding-flow.mjs
T

125 lines
4.9 KiB
JavaScript

#!/usr/bin/env node
/* eslint-disable no-console */
import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { fileURLToPath } from "node:url";
import path from "node:path";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, "..");
export const s02OnboardingTestFiles = [
"apps/web/app/modules/onboarding/services/OnboardingService.single-user-tenant-flow.test.ts",
"apps/web/app/modules/onboarding/services/OnboardingSessionService.tenant-events.test.ts",
"apps/web/app/modules/onboarding/routes/api/sessions/OnboardingSessionsIndexApi.tenant-filter.test.ts",
];
export const gates = [
{ id: "vitest-tenantAuth-s02-onboarding-flow", command: "pnpm", args: ["exec", "vitest", "run", ...s02OnboardingTestFiles] },
{ id: "tenantAuth-baseline-gates", command: "pnpm", args: ["run", "verify:tenant-auth-baseline"] },
];
const forbiddenCommandPattern = /\b(?:playwright|cypress|browser|e2e|watch)\b/i;
export function shellCommand(gate) {
return [gate.command, ...gate.args].join(" ");
}
export function validateGateConfig(candidateGates = gates) {
const vitestGate = candidateGates.find((gate) => gate.id === "vitest-tenantAuth-s02-onboarding-flow");
const baselineGate = candidateGates.find((gate) => gate.id === "tenantAuth-baseline-gates");
const errors = [];
if (!vitestGate) errors.push("missing vitest-tenantAuth-s02-onboarding-flow gate");
if (!baselineGate) errors.push("missing tenantAuth-baseline-gates gate");
if (vitestGate) {
const testFiles = vitestGate.args.filter((arg) => arg.endsWith(".test.ts") || arg.endsWith(".test.tsx") || arg.endsWith(".test.mjs"));
const missingFiles = s02OnboardingTestFiles.filter((file) => !testFiles.includes(file));
const extraFiles = testFiles.filter((file) => !s02OnboardingTestFiles.includes(file));
if (vitestGate.command !== "pnpm" || vitestGate.args.slice(0, 3).join(" ") !== "exec vitest run") errors.push("S02 Vitest gate must use pnpm exec vitest run");
if (missingFiles.length > 0) errors.push(`missing test files: ${missingFiles.join(", ")}`);
if (extraFiles.length > 0) errors.push(`unexpected test files: ${extraFiles.join(", ")}`);
}
if (baselineGate && (baselineGate.command !== "pnpm" || baselineGate.args.join(" ") !== "run verify:tenant-auth-baseline")) errors.push("baseline gate must run pnpm run verify:tenant-auth-baseline");
for (const gate of candidateGates) {
if (forbiddenCommandPattern.test(shellCommand(gate))) errors.push(`forbidden E2E/watch command reference in ${gate.id}`);
}
return { valid: errors.length === 0, errors };
}
function outputFingerprint(output) {
const normalized = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-30).join("\n");
return { hash: createHash("sha256").update(output).digest("hex").slice(0, 12), excerpt: normalized.slice(0, 1200) || "(no output)" };
}
function runGate(gate) {
return new Promise((resolve) => {
const startedAt = Date.now();
const child = spawn(gate.command, gate.args, { cwd: repoRoot, env: { ...process.env, CI: "1" }, shell: false });
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => { stdout += chunk; });
child.stderr.on("data", (chunk) => { stderr += chunk; });
child.on("error", (error) => { stderr += `\n${error.message}`; });
child.on("close", (code) => {
const output = `${stdout}\n${stderr}`;
resolve({ id: gate.id, command: shellCommand(gate), exitCode: code ?? 1, durationMs: Date.now() - startedAt, fingerprint: outputFingerprint(output) });
});
});
}
export function renderSummary(results) {
const lines = [
"# Tenant Auth Onboarding Flow Verification",
"",
"Runs focused onboarding unit tests and the existing Tenant Auth baseline gate without browser/E2E commands.",
"",
];
for (const result of results) {
lines.push(
`## ${result.id}`,
"",
`- Command: \`${result.command}\``,
`- Exitcode: ${result.exitCode}`,
`- Duration: ${result.durationMs}ms`,
`- Output Fingerprint: ${result.fingerprint.hash}`,
"",
"```text",
result.fingerprint.excerpt,
"```",
"",
);
}
return `${lines.join("\n")}\n`;
}
function aggregateExitCode(results) {
return results.some((result) => result.exitCode !== 0) ? 1 : 0;
}
async function main() {
const validation = validateGateConfig(gates);
if (!validation.valid) {
console.error(`Invalid Tenant Auth verification config:\n- ${validation.errors.join("\n- ")}`);
process.exitCode = 1;
return;
}
const results = [];
for (const gate of gates) {
console.log(`Running ${gate.id}: ${shellCommand(gate)}`);
results.push(await runGate(gate));
}
console.log(renderSummary(results));
process.exitCode = aggregateExitCode(results);
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main().catch((error) => {
console.error(error);
process.exit(1);
});
}