Files
16gagent/scripts/contract-consistency-test.mjs
2026-06-06 10:40:48 +08:00

777 lines
28 KiB
JavaScript
Raw Permalink 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 node
/**
* Contract Consistency Test
*
* 验证一个由 Pipeline 生成的项目中:
* 1. Schema 字段 100% 匹配 Contract 定义
* 2. Types 字段名(camelCase100% 匹配 Contract 的 fieldMapping
* 3. Routes 的 DTO 类型 100% 匹配 Contract
* 4. Services 的 SQL 列名 100% 匹配 Contract
* 5. Tests 使用的字段名 100% 匹配 Contract
*
* 验证失败则 exit(1),成功 exit(0)。
* 输出 contract-consistency-report.md。
*
* Usage:
* node scripts/contract-consistency-test.mjs --project <dir> [--prd <path>] [--arch <path>]
*
* Options:
* --project <dir> 项目根目录
* --prd <path> PRD JSON 文件路径(用于生成 Contract)
* --arch <path> Architecture JSON 文件路径(用于生成 Contract)
* --help 显示帮助
*/
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { createContract, toCamel, toSnake } from "./model-contract.mjs";
const __dirname = dirname(resolve(fileURLToPath(import.meta.url)));
const WORKSPACE = resolve(__dirname, "..");
// ═══════════════════════════════════════════════════════
// 1. Test Infrastructure
// ═══════════════════════════════════════════════════════
class ContractConsistencyTest {
constructor(contract) {
this.contract = contract;
this.failures = [];
this.warnings = [];
this.checks = { total: 0, passed: 0, failed: 0 };
}
fail(section, message) {
this.failures.push({ section, message });
this.checks.failed++;
this.checks.total++;
}
pass() {
this.checks.passed++;
this.checks.total++;
}
/**
* Recursively find files matching a glob in a directory.
*/
findFiles(dir, pattern) {
const results = [];
if (!existsSync(dir)) return results;
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = resolve(dir, entry.name);
if (entry.isDirectory()) {
// Skip node_modules and dist
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".next" || entry.name === "data") continue;
results.push(...this.findFiles(fullPath, pattern));
} else if (pattern.test(entry.name)) {
results.push(fullPath);
}
}
} catch { /* skip */ }
return results;
}
}
// ═══════════════════════════════════════════════════════
// 2. Check 1: Schema → Contract
// ═══════════════════════════════════════════════════════
function checkSchemaConsistency(test) {
const { contract } = test;
const entities = contract.entities;
console.log("── Check 1: Database Schema ↔ Contract ──");
// Find schema files
const projectDir = process.env.PROJECT_DIR || ".";
const schemaFiles = [
...test.findFiles(resolve(projectDir, "backend/src/db"), /schema\.ts$/),
...test.findFiles(resolve(projectDir, "src/db"), /schema\.ts$/),
...test.findFiles(resolve(projectDir, "apps/api/src/db"), /schema\.ts$/),
...test.findFiles(resolve(projectDir, "fullstack/apps/api/src/db"), /schema\.ts$/),
...test.findFiles(projectDir, /schema\.prisma$/),
];
if (schemaFiles.length === 0) {
test.fail("Schema", "No schema file found (src/db/schema.ts or schema.prisma)");
return;
}
for (const schemaFile of schemaFiles) {
console.log(` Schema file: ${schemaFile}`);
let content;
try {
content = readFileSync(schemaFile, "utf-8");
} catch (e) {
test.fail("Schema", `Cannot read ${schemaFile}: ${e.message}`);
continue;
}
for (const entity of entities) {
console.log(` Checking entity: ${entity.name} (${entity.table})`);
// Verify the table exists in the schema
const tableRegex = new RegExp(`CREATE TABLE IF NOT EXISTS ${entity.table} \\(([\\s\\S]+?)\\);`, "si");
const tableMatch = content.match(tableRegex);
if (!tableMatch) {
// Try without CREATE TABLE (for Prisma/simpler formats)
const altRegex = new RegExp(`(?:CREATE TABLE|model)\\s+(?:IF NOT EXISTS\\s+)?['"]?${entity.table}['"]?`, "i");
if (!altRegex.test(content)) {
test.warnings.push({ section: "Schema", message: `Table '${entity.table}' not found in schema` });
continue;
}
}
// Extract column definitions
let columns;
if (tableMatch) {
const body = tableMatch[1];
columns = body
.split(",")
.map(c => c.trim())
.filter(c => c.length > 0 && !c.startsWith("--"))
.map(c => {
// Handle DEFAULT (...) expressions with nested parens
const cleanCol = c.replace(/DEFAULT\s*\([^)]*\)/gi, "").replace(/^\\n\s*/, "").trim();
const parts = cleanCol.split(/\s+/);
return { name: parts[0], raw: c };
});
} else {
columns = [];
}
const columnNames = new Set(columns.map(c => c.name));
for (const field of entity.fields) {
test.pass();
if (field.isSecret) continue; // Secret fields may not be in output types
if (!columnNames.has(field.name)) {
test.fail(
`Schema/${entity.table}`,
`Field '${field.name}' defined in Contract but MISSING from schema table '${entity.table}'`
);
}
}
// Check for extra columns not in contract (warn only)
for (const col of columns) {
const hasField = entity.fields.some(f => f.name === col.name);
if (!hasField && !col.raw.toUpperCase().includes("FOREIGN KEY") &&
!col.raw.toUpperCase().includes("PRIMARY KEY") &&
!col.raw.toUpperCase().includes("CONSTRAINT") &&
!col.raw.toUpperCase().includes("UNIQUE(") &&
!col.raw.toUpperCase().includes("CHECK(") &&
col.name !== "" && !col.raw.startsWith(")")) {
test.warnings.push({
section: `Schema/${entity.table}`,
message: `Column '${col.name}' found in schema but NOT in Contract for table '${entity.table}'`
});
}
}
}
}
console.log(` Schema checks: ${test.checks.passed - (test.checks.passed > 0 ? 0 : 0)} passed, ${test.checks.failed} failures`);
}
// ═══════════════════════════════════════════════════════
// 3. Check 2: Types → Contract
// ═══════════════════════════════════════════════════════
function checkTypesConsistency(test) {
const { contract } = test;
const projectDir = process.env.PROJECT_DIR || ".";
console.log("── Check 2: TypeScript Types ↔ Contract ──");
const typeFiles = [
...test.findFiles(resolve(projectDir, "backend/src/types"), /\.ts$/),
...test.findFiles(resolve(projectDir, "src/types"), /\.ts$/),
...test.findFiles(resolve(projectDir, "apps/api/src/types"), /\.ts$/),
...test.findFiles(resolve(projectDir, "apps/api/packages/shared-types"), /\.ts$/),
...test.findFiles(resolve(projectDir, "packages/shared-types"), /\.ts$/),
].filter(f => !f.endsWith(".d.ts") || f.endsWith("fastify.d.ts"));
if (typeFiles.length === 0) {
test.fail("Types", "No TypeScript type files found");
return;
}
// Build a set of all expected camelCase field names from the contract
const expectedFields = new Map(); // entityName → Set of field names
for (const entity of contract.entities) {
const fieldSet = new Set();
for (const field of entity.fields) {
if (field.isSecret) continue;
const camel = toCamel(contract, field.name);
fieldSet.add(camel);
}
expectedFields.set(entity.name, fieldSet);
}
for (const typeFile of typeFiles) {
console.log(` Types file: ${typeFile}`);
let content;
try {
content = readFileSync(typeFile, "utf-8");
} catch (e) {
test.fail("Types", `Cannot read ${typeFile}: ${e.message}`);
continue;
}
// For each entity, check its interface exists and has correct fields
for (const entity of contract.entities) {
const expected = expectedFields.get(entity.name);
if (!expected) continue;
// Find the interface declaration
const ifaceRegex = new RegExp(`(?:export\\s+)?interface\\s+${entity.name}\\s*(?:extends\\s+[^{]+)?\\s*\\{([^}]*)\\}`, "s");
const match = content.match(ifaceRegex);
if (!match) {
// Entity types might not be directly in this file (e.g. in shared-types)
continue;
}
const body = match[1];
const fieldLines = body
.split("\n")
.map(l => l.trim())
.filter(l => l.length > 0 && !l.startsWith("//"));
const foundFields = new Set();
for (const line of fieldLines) {
// Extract field name: " username: string;" or " username?: string;"
const fieldMatch = line.match(/^\s*(\w+)(\?)?\s*:/);
if (fieldMatch) {
foundFields.add(fieldMatch[1]);
}
}
// Check each expected field exists
for (const fieldName of expected) {
test.pass();
if (!foundFields.has(fieldName)) {
test.fail(
`Types/${entity.name}`,
`Field '${fieldName}' (camelCase) expected in interface '${entity.name}' but MISSING from types file`
);
}
}
// Warn about extra fields
for (const found of foundFields) {
if (!expected.has(found)) {
test.warnings.push({
section: `Types/${entity.name}`,
message: `Field '${found}' found in interface '${entity.name}' but NOT in Contract`
});
}
}
}
}
// Also check that all entities exist as interfaces in the types
// Find ALL interface names declared
for (const typeFile of typeFiles) {
let content;
try { content = readFileSync(typeFile, "utf-8"); } catch { continue; }
for (const entity of contract.entities) {
const ifaceRegex = new RegExp(`interface\\s+${entity.name}\\s*(?:extends|\\{)`, "s");
if (!ifaceRegex.test(content)) {
// Check if entity's table is users — User is a special type
if (entity.table !== "users") {
// It might be in another type file; we accumulate all
continue;
}
}
}
}
}
// ═══════════════════════════════════════════════════════
// 4. Check 3: Routes → Contract
// ═══════════════════════════════════════════════════════
function checkRoutesConsistency(test) {
const { contract } = test;
const projectDir = process.env.PROJECT_DIR || ".";
console.log("── Check 3: Route DTO Types ↔ Contract ──");
const routeFiles = [
...test.findFiles(resolve(projectDir, "src/routes"), /\.ts$/),
...test.findFiles(resolve(projectDir, "apps/api/src/routes"), /\.ts$/),
];
if (routeFiles.length === 0) {
test.warnings.push({ section: "Routes", message: "No route files found, skipping route checks" });
return;
}
for (const routeFile of routeFiles) {
console.log(` Route file: ${routeFile}`);
let content;
try {
content = readFileSync(routeFile, "utf-8");
} catch (e) { continue; }
// Check auth routes use contract auth config
// Check CRUD routes use correct entity field names
for (const entity of contract.entities) {
if (entity.table === "users") continue; // Skip User — handled by auth routes
const routeName = entity.table;
if (routeFile.includes(routeName) || routeFile.endsWith(`${routeName}.ts`)) {
// Verify CreateInput and UpdateInput types are used
const pascalName = entity.name;
const hasCreateInput = new RegExp(`Create${pascalName}Input`).test(content);
const hasUpdateInput = new RegExp(`Update${pascalName}Input`).test(content);
if (!hasCreateInput) {
test.fail(
`Routes/${routeName}`,
`Route for '${routeName}' should use Create${pascalName}Input type`
);
} else { test.pass(); }
if (!hasUpdateInput) {
test.fail(
`Routes/${routeName}`,
`Route for '${routeName}' should use Update${pascalName}Input type`
);
} else { test.pass(); }
}
}
}
}
// ═══════════════════════════════════════════════════════
// 5. Check 4: Services → Contract
// ═══════════════════════════════════════════════════════
function checkServicesConsistency(test) {
const { contract } = test;
const projectDir = process.env.PROJECT_DIR || ".";
console.log("── Check 4: Service SQL Columns ↔ Contract ──");
const serviceFiles = [
...test.findFiles(resolve(projectDir, "src/services"), /\.ts$/),
...test.findFiles(resolve(projectDir, "apps/api/src/services"), /\.ts$/),
];
if (serviceFiles.length === 0) {
test.warnings.push({ section: "Services", message: "No service files found, skipping service checks" });
return;
}
for (const serviceFile of serviceFiles) {
console.log(` Service file: ${serviceFile}`);
let content;
try {
content = readFileSync(serviceFile, "utf-8");
} catch (e) { continue; }
// Extract SQL column names from INSERT/SELECT statements
const insertMatches = content.matchAll(/INSERT INTO\s+(\w+)\s*\(([^)]+)\)/gi);
for (const match of insertMatches) {
const tableName = match[1];
const colStr = match[2];
const columns = colStr.split(",").map(c => c.trim().replace(/"/g, ""));
// Find the entity for this table
const entity = contract.entities.find(e => e.table === tableName);
if (!entity) continue;
const contractCols = new Set(entity.fields.filter(f => !f.isSecret).map(f => f.name));
for (const col of columns) {
test.pass();
if (!contractCols.has(col)) {
test.fail(
`Services/${tableName}`,
`SQL column '${col}' in INSERT statement not found in Contract for table '${tableName}'`
);
}
}
}
// Check SELECT statements too
const selectMatches = content.matchAll(/SELECT\s+(.+?)\s+FROM\s+(\w+)/gi);
for (const match of selectMatches) {
const selectStr = match[1];
const tableName = match[2];
const entity = contract.entities.find(e => e.table === tableName);
if (!entity || selectStr === "*") continue;
const selectCols = selectStr
.split(",")
.map(c => c.trim().split(/\s+as\s+/i)[0].trim())
.filter(c => c.length > 0 && c !== "*");
const contractCols = new Set(entity.fields.filter(f => !f.isSecret).map(f => f.name));
for (const col of selectCols) {
// Column might be aliased or use table prefix
const bareCol = col.split(".").pop().replace(/"/g, "");
if (!contractCols.has(bareCol)) {
test.warnings.push({
section: `Services/${tableName}`,
message: `SQL selected column '${bareCol}' not found in Contract for table '${tableName}'`
});
}
}
}
}
}
// ═══════════════════════════════════════════════════════
// 6. Check 5: Tests → Contract
// ═══════════════════════════════════════════════════════
function checkTestsConsistency(test) {
const { contract } = test;
const projectDir = process.env.PROJECT_DIR || ".";
console.log("── Check 5: Test Payloads ↔ Contract ──");
const testFiles = [
...test.findFiles(resolve(projectDir, "src/__tests__"), /\.test\.ts$/),
...test.findFiles(resolve(projectDir, "apps/api/src/__tests__"), /\.test\.ts$/),
...test.findFiles(projectDir, /\.test\.ts$/),
];
if (testFiles.length === 0) {
test.warnings.push({ section: "Tests", message: "No test files found, skipping test checks" });
return;
}
for (const testFile of testFiles) {
console.log(` Test file: ${testFile}`);
let content;
try {
content = readFileSync(testFile, "utf-8");
} catch (e) { continue; }
// Extract payload objects from .inject() calls
const payloadRegex = /payload:\s*({[^}]+})/gs;
const matches = [...content.matchAll(payloadRegex)];
for (const match of matches) {
let payloadStr = match[1];
try {
// Try to parse as JSON (might have template literals, so this is approximate)
// Extract field names using regex
const fieldRegex = /(\w+)\s*:/g;
const payloadFields = [];
let fm;
while ((fm = fieldRegex.exec(payloadStr)) !== null) {
payloadFields.push(fm[1]);
}
// Check if these fields are valid according to some entity
const allContractFields = new Set();
for (const entity of contract.entities) {
for (const field of entity.fields) {
if (!field.isSecret && !field.isAuto && !field.isPrimary) {
allContractFields.add(toCamel(contract, field.name));
allContractFields.add(field.name); // snake_case
}
}
}
for (const pf of payloadFields) {
if (!allContractFields.has(pf)) {
// Allow special test fields
if (["username", "password", "nickname", "token", "authorization"].includes(pf)) {
continue;
}
test.warnings.push({
section: `Tests/${testFile}`,
message: `Test payload field '${pf}' not found in any Contract entity`
});
}
}
} catch {
// Non-JSON payload, skip
}
}
// Check for hardcoded fake userId values
const fakeIdMatch = content.match(/"00000000-0000-0000-0000-000000000001"/);
if (fakeIdMatch) {
test.warnings.push({
section: `Tests/${testFile}`,
message: "Hardcoded fake userId found in test. Consider using a real registered user ID."
});
}
}
}
// ═══════════════════════════════════════════════════════
// 7. Check 6: Auth Configuration
// ═══════════════════════════════════════════════════════
function checkAuthConsistency(test) {
const { contract } = test;
const projectDir = process.env.PROJECT_DIR || ".";
console.log("── Check 7: Auth Config ↔ Contract ──");
const authFiles = [
...test.findFiles(resolve(projectDir, "src/routes"), /auth\.ts$/),
...test.findFiles(resolve(projectDir, "apps/api/src/routes"), /auth\.ts$/),
];
if (authFiles.length === 0) {
test.warnings.push({ section: "Auth", message: "No auth routes found" });
return;
}
for (const authFile of authFiles) {
let content;
try {
content = readFileSync(authFile, "utf-8");
} catch (e) { continue; }
// Check registration fields
for (const field of contract.auth.registrationFields) {
test.pass();
if (!content.includes(field)) {
test.fail("Auth", `Registration field '${field}' from contract.auth.registrationFields not found in auth routes`);
}
}
// Check JWT payload fields
for (const field of contract.auth.jwtPayload) {
test.pass();
if (!content.includes(field)) {
test.fail("Auth", `JWT payload field '${field}' from contract.auth.jwtPayload not found in auth routes`);
}
}
// Check password policy (minLength)
const pwPolicy = contract.auth.passwordPolicy;
if (pwPolicy.minLength) {
const minLenRegex = new RegExp(`password\\.length\\s*<\\s*${pwPolicy.minLength}`);
if (minLenRegex.test(content)) {
test.pass();
} else {
test.fail("Auth", `Password minLength policy (${pwPolicy.minLength}) not enforced in auth routes`);
}
}
// Check password_hash column is used (not a plain password column)
test.pass();
if (!content.includes("password_hash") && !content.includes("passwordHash")) {
test.fail("Auth", "Auth routes should use 'password_hash' column (not plain password)");
}
}
}
// ═══════════════════════════════════════════════════════
// 8. Main Runner
// ═══════════════════════════════════════════════════════
function generateReport(test, contract) {
const lines = [];
lines.push("# Contract Consistency Report");
lines.push("");
lines.push(`**Generated**: ${new Date().toISOString()}`);
lines.push(`**Project**: ${contract.projectName}`);
lines.push(`**Domain**: ${contract.domain}`);
lines.push(`**Entities**: ${contract.entities.length}`);
lines.push("");
lines.push("## Summary");
lines.push("");
lines.push(`| Metric | Value |`);
lines.push(`|--------|-------|`);
lines.push(`| Total Checks | ${test.checks.total} |`);
lines.push(`| Passed | ${test.checks.passed} |`);
lines.push(`| Failed | ${test.checks.failed} |`);
lines.push(`| Warnings | ${test.warnings.length} |`);
lines.push(`| **Result** | **${test.checks.failed === 0 ? "✅ PASS" : "❌ FAIL"}** |`);
lines.push("");
if (test.failures.length > 0) {
lines.push("## ❌ Failures");
lines.push("");
for (const f of test.failures) {
lines.push(`- **${f.section}**: ${f.message}`);
}
lines.push("");
}
if (test.warnings.length > 0) {
lines.push("## ⚠️ Warnings");
lines.push("");
for (const w of test.warnings) {
lines.push(`- **${w.section}**: ${w.message}`);
}
lines.push("");
}
lines.push("## Contract Entities");
lines.push("");
for (const entity of contract.entities) {
lines.push(`### ${entity.name} (\`${entity.table}\`)`);
lines.push(`*${entity.description}*`);
lines.push("");
lines.push("| Field | Type | Required | Notes |");
lines.push("|-------|------|----------|-------|");
for (const field of entity.fields) {
const notes = [];
if (field.isPrimary) notes.push("PK");
if (field.isAuto) notes.push("auto");
if (field.isSecret) notes.push("secret");
if (field.unique) notes.push("unique");
if (field.enum) notes.push(`enum: ${field.enum.join("|")}`);
if (field.defaultValue !== undefined) notes.push(`default: ${field.defaultValue}`);
if (field.fkEntity) notes.push(`FK→${field.fkEntity}.${field.fkColumn}`);
lines.push(`| \`${field.name}\` | ${field.type} | ${field.required ? "✓" : ""} | ${notes.join(", ")} |`);
}
lines.push("");
}
// Field mapping reference
lines.push("## Field Mapping");
lines.push("");
lines.push("| snake_case | camelCase |");
lines.push("|------------|-----------|");
for (const [snake, camel] of Object.entries(contract.fieldMapping.snakeToCamel)) {
if (snake !== camel) {
lines.push(`| \`${snake}\` | \`${camel}\` |`);
}
}
lines.push("");
const report = lines.join("\n");
// Write report
const reportPath = resolve(WORKSPACE, "contract-consistency-report.md");
mkdirSync(dirname(reportPath), { recursive: true });
writeFileSync(reportPath, report);
console.log(`\nReport written to: ${reportPath}`);
return report;
}
async function main() {
const args = process.argv.slice(2);
let projectDir = null, prdPath = null, archPath = null;
for (let i = 0; i < args.length; i++) {
if (args[i] === "--project" && args[i + 1]) {
projectDir = resolve(args[++i]);
} else if (args[i] === "--prd" && args[i + 1]) {
prdPath = resolve(args[++i]);
} else if (args[i] === "--arch" && args[i + 1]) {
archPath = resolve(args[++i]);
} else if (args[i] === "--help" || args[i] === "-h") {
console.log(`
Contract Consistency Test
Usage:
node scripts/contract-consistency-test.mjs --project <dir> [--prd <path>] [--arch <path>]
Options:
--project <dir> 项目根目录
--prd <path> PRD JSON 文件路径
--arch <path> Architecture JSON 文件路径
--help 显示帮助
`);
process.exit(0);
}
}
if (!projectDir) {
console.error("Error: --project <dir> is required. Use --help.");
process.exit(1);
}
// Set global PROJECT_DIR for check functions
process.env.PROJECT_DIR = projectDir;
// Load PRD and Architecture if available, or create a minimal contract
let prd = null, arch = null;
if (prdPath && existsSync(prdPath)) {
try {
prd = JSON.parse(readFileSync(prdPath, "utf-8"));
} catch (e) {
console.error(`Warning: Failed to load PRD: ${e.message}`);
}
}
if (archPath && existsSync(archPath)) {
try {
arch = JSON.parse(readFileSync(archPath, "utf-8"));
} catch (e) {
console.error(`Warning: Failed to load Architecture: ${e.message}`);
}
}
// If no PRD/arch, try to derive contract from generated project
if (!prd && !arch) {
// Check if project has a prd.json or arch.json
const projectPrd = resolve(projectDir, "prd.json");
const projectArch = resolve(projectDir, "arch.json");
if (existsSync(projectPrd)) {
try { prd = JSON.parse(readFileSync(projectPrd, "utf-8")); } catch {}
}
if (existsSync(projectArch)) {
try { arch = JSON.parse(readFileSync(projectArch, "utf-8")); } catch {}
}
}
// If still no PRD, try to detect domain from project structure
if (!prd) {
prd = { projectName: "ContractMgmt", domain: "enterprise", features: [], pages: [], apiRequirements: [] };
}
// Create contract
const contract = createContract(prd, arch);
console.log(`\n═══ Contract Consistency Test ═══`);
console.log(`Project: ${contract.projectName}`);
console.log(`Domain: ${contract.domain}`);
console.log(`Entities: ${contract.entities.map(e => e.name).join(", ")}`);
console.log(`Project Dir: ${projectDir}\n`);
// Initialize test
const test = new ContractConsistencyTest(contract);
// Run checks
try { checkSchemaConsistency(test); } catch (e) { test.fail("Schema", `Error: ${e.message}`); }
try { checkTypesConsistency(test); } catch (e) { test.fail("Types", `Error: ${e.message}`); }
try { checkRoutesConsistency(test); } catch (e) { test.fail("Routes", `Error: ${e.message}`); }
try { checkServicesConsistency(test); } catch (e) { test.fail("Services", `Error: ${e.message}`); }
try { checkTestsConsistency(test); } catch (e) { test.fail("Tests", `Error: ${e.message}`); }
try { checkAuthConsistency(test); } catch (e) { test.fail("Auth", `Error: ${e.message}`); }
// Generate report
const report = generateReport(test, contract);
// Print summary
console.log(`\n═══ Results ═══`);
console.log(`Total: ${test.checks.total} | Passed: ${test.checks.passed} | Failed: ${test.checks.failed} | Warnings: ${test.warnings.length}`);
console.log(`Result: ${test.checks.failed === 0 ? "✅ PASS" : "❌ FAIL"}`);
// Exit with appropriate code
process.exit(test.checks.failed > 0 ? 1 : 0);
}
main();