#!/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); }); }