|
|
/**
|
|
|
* Go-No-Go GD-4 + RPA probe 3/3 验收(task-117 + task-134)
|
|
|
* 用法:tsx scripts/go-no-go-check.ts [--mock]
|
|
|
*/
|
|
|
import "dotenv/config";
|
|
|
import {
|
|
|
computeRates,
|
|
|
type MetricsCounters,
|
|
|
} from "@/modules/metrics/collector";
|
|
|
import { METRIC_COUNTER } from "@/lib/constants/metrics";
|
|
|
import { validateProbeItems } from "@/lib/rpa/probe-validation";
|
|
|
import { executeProbeQuote } from "@/lib/rpa/run-probe";
|
|
|
|
|
|
const THRESHOLDS = {
|
|
|
api_success_rate: 99.5,
|
|
|
rpa_success_rate: 85,
|
|
|
realtime_rate: 90,
|
|
|
cache_hit_rate: 90,
|
|
|
p95_latency_ms: 30_000,
|
|
|
probe_success_count: 3,
|
|
|
} as const;
|
|
|
|
|
|
const PROBE_ROUNDS = 3;
|
|
|
|
|
|
type CheckResult = {
|
|
|
name: string;
|
|
|
value: number | string;
|
|
|
threshold: number | string;
|
|
|
pass: boolean;
|
|
|
operator: ">=" | "<" | "—";
|
|
|
skipped?: boolean;
|
|
|
};
|
|
|
|
|
|
function buildMockCounters(): MetricsCounters {
|
|
|
return {
|
|
|
[METRIC_COUNTER.POST_TOTAL]: 1000,
|
|
|
[METRIC_COUNTER.POST_DONE]: 996,
|
|
|
[METRIC_COUNTER.CACHE_HIT_L1]: 200,
|
|
|
[METRIC_COUNTER.CACHE_HIT_L2]: 720,
|
|
|
[METRIC_COUNTER.RPA_TRIGGERED]: 100,
|
|
|
[METRIC_COUNTER.RPA_DONE]: 88,
|
|
|
[METRIC_COUNTER.DONE_TOTAL]: 996,
|
|
|
[METRIC_COUNTER.DONE_REALTIME]: 910,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
function isMockMode(): boolean {
|
|
|
return (
|
|
|
process.argv.includes("--mock") || process.env.RPA_MOCK_MODE === "true"
|
|
|
);
|
|
|
}
|
|
|
|
|
|
async function loadSnapshot(): Promise<{
|
|
|
rates: ReturnType<typeof computeRates>;
|
|
|
p95: number;
|
|
|
}> {
|
|
|
if (process.argv.includes("--mock")) {
|
|
|
const counters = buildMockCounters();
|
|
|
return { rates: computeRates(counters), p95: 12_000 };
|
|
|
}
|
|
|
|
|
|
const { getMetricsSnapshot } = await import("@/modules/metrics/collector");
|
|
|
const snapshot = await getMetricsSnapshot();
|
|
|
return {
|
|
|
rates: {
|
|
|
api_success_rate: snapshot.api_success_rate,
|
|
|
rpa_success_rate: snapshot.rpa_success_rate,
|
|
|
realtime_rate: snapshot.realtime_rate,
|
|
|
cache_hit_rate: snapshot.cache_hit_rate,
|
|
|
},
|
|
|
p95: snapshot.p95_latency_ms,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
function runMetricChecks(
|
|
|
rates: ReturnType<typeof computeRates>,
|
|
|
p95: number,
|
|
|
): CheckResult[] {
|
|
|
return [
|
|
|
{
|
|
|
name: "API success rate",
|
|
|
value: rates.api_success_rate,
|
|
|
threshold: THRESHOLDS.api_success_rate,
|
|
|
pass: rates.api_success_rate >= THRESHOLDS.api_success_rate,
|
|
|
operator: ">=",
|
|
|
},
|
|
|
{
|
|
|
name: "RPA success rate",
|
|
|
value: rates.rpa_success_rate,
|
|
|
threshold: THRESHOLDS.rpa_success_rate,
|
|
|
pass: rates.rpa_success_rate >= THRESHOLDS.rpa_success_rate,
|
|
|
operator: ">=",
|
|
|
},
|
|
|
{
|
|
|
name: "Realtime rate",
|
|
|
value: rates.realtime_rate,
|
|
|
threshold: THRESHOLDS.realtime_rate,
|
|
|
pass: rates.realtime_rate >= THRESHOLDS.realtime_rate,
|
|
|
operator: ">=",
|
|
|
},
|
|
|
{
|
|
|
name: "Cache hit rate",
|
|
|
value: rates.cache_hit_rate,
|
|
|
threshold: THRESHOLDS.cache_hit_rate,
|
|
|
pass: rates.cache_hit_rate >= THRESHOLDS.cache_hit_rate,
|
|
|
operator: ">=",
|
|
|
},
|
|
|
{
|
|
|
name: "P95 latency (ms)",
|
|
|
value: p95,
|
|
|
threshold: THRESHOLDS.p95_latency_ms,
|
|
|
pass: p95 < THRESHOLDS.p95_latency_ms,
|
|
|
operator: "<",
|
|
|
},
|
|
|
];
|
|
|
}
|
|
|
|
|
|
/** PRD §12.7 CHG-P09:真实 env 下 probe 连续 3 次 exit 0 */
|
|
|
async function runProbeGate(): Promise<CheckResult> {
|
|
|
if (isMockMode()) {
|
|
|
return {
|
|
|
name: "RPA probe 3/3",
|
|
|
value: "跳过",
|
|
|
threshold: PROBE_ROUNDS,
|
|
|
pass: true,
|
|
|
operator: "—",
|
|
|
skipped: true,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
let success = 0;
|
|
|
for (let round = 1; round <= PROBE_ROUNDS; round++) {
|
|
|
try {
|
|
|
console.log(`[go-no-go] probe 第 ${round}/${PROBE_ROUNDS} 次…`);
|
|
|
const result = await executeProbeQuote();
|
|
|
const validation = validateProbeItems(result.items);
|
|
|
if (validation.ok) {
|
|
|
success += 1;
|
|
|
} else {
|
|
|
console.error(`[go-no-go] probe 失败: ${validation.message}`);
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error(`[go-no-go] probe 异常:`, error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
name: "RPA probe 3/3",
|
|
|
value: success,
|
|
|
threshold: THRESHOLDS.probe_success_count,
|
|
|
pass: success === PROBE_ROUNDS,
|
|
|
operator: ">=",
|
|
|
};
|
|
|
}
|
|
|
|
|
|
async function main(): Promise<void> {
|
|
|
const { rates, p95 } = await loadSnapshot();
|
|
|
const metricChecks = runMetricChecks(rates, p95);
|
|
|
const probeCheck = await runProbeGate();
|
|
|
const checks = [...metricChecks, probeCheck];
|
|
|
const required = checks.filter((c) => !c.skipped);
|
|
|
const allPass = required.every((c) => c.pass);
|
|
|
|
|
|
console.log("=== Go-No-Go 验收 ===");
|
|
|
for (const check of checks) {
|
|
|
if (check.skipped) {
|
|
|
console.log(`[SKIP] ${check.name}: mock 模式跳过 probe 门禁`);
|
|
|
continue;
|
|
|
}
|
|
|
const status = check.pass ? "PASS" : "FAIL";
|
|
|
console.log(
|
|
|
`[${status}] ${check.name}: ${check.value} (需 ${check.operator} ${check.threshold})`,
|
|
|
);
|
|
|
}
|
|
|
console.log(`\n总体判定: ${allPass ? "PASS" : "FAIL"}`);
|
|
|
|
|
|
if (!allPass) {
|
|
|
process.exit(1);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
main().catch((error) => {
|
|
|
console.error("[go-no-go] 执行失败:", error);
|
|
|
process.exit(1);
|
|
|
});
|