🎉 init: 小龙的工作空间

This commit is contained in:
大海
2026-06-06 10:40:48 +08:00
commit a188ee1426
3201 changed files with 231817 additions and 0 deletions
+776
View File
@@ -0,0 +1,776 @@
#!/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();