166 lines
7.7 KiB
JavaScript
166 lines
7.7 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Regression Gate — SF-08
|
|
*
|
|
* 执行所有验证 gate,全部通过才允许发布。
|
|
* Exit 0 = PASS, Exit 1 = FAIL.
|
|
*
|
|
* Usage:
|
|
* node scripts/regression-gate.mjs [--strict]
|
|
*
|
|
* Gates:
|
|
* 1. 10 Domain E2E Benchmark
|
|
* 2. 170 Regression Tests
|
|
* 3. 5 Domain Semantic Spot-Check
|
|
*
|
|
* @module regression-gate
|
|
*/
|
|
|
|
import { execSync } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = dirname(resolve(fileURLToPath(import.meta.url)));
|
|
const WORKSPACE = resolve(__dirname, "..");
|
|
|
|
const STRICT = process.argv.includes("--strict");
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// Gate Definitions
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
const SEMANTIC_SPOTCHECK = [
|
|
{ name: "BookShelf", input: "做一个图书管理系统,支持书籍库、阅读状态、阅读进度、书评和统计", expectTables: ["books", "reading_status"] },
|
|
{ name: "TeamFlow", input: "做一个团队协作工具,支持看板视图、任务管理、团队成员和活动日志", expectTables: ["boards", "tasks"] },
|
|
{ name: "Contract", input: "做一个合同管理工具,支持合同创建、审批流程、到期提醒和合同归档", expectTables: ["contracts", "approvals"] },
|
|
{ name: "Inspection", input: "做一个设备巡检系统,支持巡检计划、巡检记录、故障上报和设备台账", expectTables: ["equipment", "faults"] },
|
|
{ name: "Warehouse", input: "做一个仓库出入库工具,支持入库登记、出库审批、库存盘点和库存预警", expectTables: ["stock_in", "stock_out"] },
|
|
];
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// Helpers
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
function run(cmd, label) {
|
|
const start = Date.now();
|
|
try {
|
|
const out = execSync(cmd, { cwd: WORKSPACE, encoding: "utf8", stdio: "pipe", timeout: 180_000 });
|
|
const ms = Date.now() - start;
|
|
return { pass: true, ms, output: out };
|
|
} catch (e) {
|
|
const ms = Date.now() - start;
|
|
return { pass: false, ms, output: e.stderr || e.stdout || e.message };
|
|
}
|
|
}
|
|
|
|
function gateHeader(n, label) {
|
|
console.log(`\n${"═".repeat(60)}`);
|
|
console.log(` GATE ${n}: ${label}`);
|
|
console.log(`${"═".repeat(60)}`);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// Gate 1: 10 Domain E2E Benchmark
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
function gateBenchmark() {
|
|
gateHeader(1, "10 Domain E2E Benchmark");
|
|
const r = run("node scripts/e2e-benchmark-v1.mjs", "benchmark");
|
|
const passMatch = r.output.match(/(\d+)\/(\d+) PASS/);
|
|
const passCount = passMatch ? parseInt(passMatch[1]) : 0;
|
|
const total = passMatch ? parseInt(passMatch[2]) : 0;
|
|
const pass = passCount === 10 && total === 10;
|
|
console.log(` Result: ${passCount}/${total} PASS (${r.ms}ms)`);
|
|
console.log(` Status: ${pass ? "✅ PASS" : "❌ FAIL"}`);
|
|
return pass;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// Gate 2: 170 Regression Tests
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
function gateRegression() {
|
|
gateHeader(2, "170 Regression Tests");
|
|
const r = run("node --test test/e2e-regression.test.mjs", "regression");
|
|
const passMatch = r.output.match(/pass (\d+)/);
|
|
const failMatch = r.output.match(/fail (\d+)/);
|
|
const passCount = passMatch ? parseInt(passMatch[1]) : 0;
|
|
const failCount = failMatch ? parseInt(failMatch[1]) : 0;
|
|
const pass = passCount === 170 && failCount === 0;
|
|
console.log(` Result: ${passCount} pass, ${failCount} fail (${r.ms}ms)`);
|
|
console.log(` Status: ${pass ? "✅ PASS" : "❌ FAIL"}`);
|
|
return pass;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// Gate 3: Semantic Spot-Check
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
function gateSemantic() {
|
|
gateHeader(3, "5 Domain Semantic Spot-Check");
|
|
let allPass = true;
|
|
|
|
for (const tc of SEMANTIC_SPOTCHECK) {
|
|
const tmpDir = `/tmp/gate-${tc.name.toLowerCase()}`;
|
|
try {
|
|
execSync(`node scripts/project-intake-agent.mjs --input "${tc.input}" --output ${tmpDir}/prd.json`, { cwd: WORKSPACE, encoding: "utf8", stdio: "pipe" });
|
|
execSync(`node scripts/architecture-agent.mjs --input ${tmpDir}/prd.json --output ${tmpDir}/arch.json`, { cwd: WORKSPACE, encoding: "utf8", stdio: "pipe" });
|
|
|
|
const arch = JSON.parse(readFileSync(`${tmpDir}/arch.json`, "utf8"));
|
|
const tables = arch.databaseSchema.map(t => t.table);
|
|
const hasChinese = tables.some(t => /[\u4e00-\u9fff]/.test(t));
|
|
const hasExpected = tc.expectTables.every(et => tables.includes(et));
|
|
const hasDomainFields = arch.databaseSchema.filter(t => t.table !== "users").some(t => {
|
|
const fields = t.fields.filter(f => !["id", "user_id", "created_at", "updated_at"].includes(f.name));
|
|
return fields.some(f => !["title", "description", "status", "data"].includes(f.name));
|
|
});
|
|
|
|
const pass = !hasChinese && hasExpected && hasDomainFields;
|
|
console.log(` ${pass ? "✅" : "❌"} ${tc.name}: tables=[${tables.join(", ")}]${hasChinese ? " [CHINESE!]" : ""}${!hasExpected ? " [MISSING TABLES!]" : ""}${!hasDomainFields ? " [GENERIC FIELDS!]" : ""}`);
|
|
if (!pass) allPass = false;
|
|
} catch (e) {
|
|
console.log(` ❌ ${tc.name}: ${e.message.split("\n")[0]}`);
|
|
allPass = false;
|
|
}
|
|
}
|
|
|
|
console.log(`\n Status: ${allPass ? "✅ PASS" : "❌ FAIL"}`);
|
|
return allPass;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// Main
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
async function main() {
|
|
console.log("🔒 Regression Gate — v1.1.0-semantic");
|
|
console.log(` Mode: ${STRICT ? "STRICT" : "NORMAL"}`);
|
|
console.log(` Time: ${new Date().toISOString()}`);
|
|
|
|
const results = {
|
|
benchmark: gateBenchmark(),
|
|
regression: gateRegression(),
|
|
semantic: gateSemantic(),
|
|
};
|
|
|
|
console.log(`\n${"═".repeat(60)}`);
|
|
console.log(" SUMMARY");
|
|
console.log(`${"═".repeat(60)}`);
|
|
for (const [name, pass] of Object.entries(results)) {
|
|
console.log(` ${pass ? "✅" : "❌"} ${name}`);
|
|
}
|
|
|
|
const allPass = Object.values(results).every(Boolean);
|
|
console.log(`\n ${allPass ? "🟢 ALL GATES PASS — Release approved" : "🔴 GATE FAILED — Release blocked"}`);
|
|
console.log("");
|
|
|
|
process.exit(allPass ? 0 : 1);
|
|
}
|
|
|
|
main().catch(e => {
|
|
console.error("Gate runner crashed:", e);
|
|
process.exit(1);
|
|
});
|