125 lines
4.9 KiB
JavaScript
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);
|
|
});
|
|
}
|