Files
16gagent/scripts/regression-gate.mjs
T
2026-06-06 10:40:48 +08:00

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);
});