chore: require real data api benchmark evidence

This commit is contained in:
Codex
2026-06-30 13:13:21 +08:00
parent 6e17e77aaf
commit 6d5ea62bf5
6 changed files with 136 additions and 5 deletions

View File

@@ -12,6 +12,21 @@ function isoNow() {
return new Date().toISOString();
}
function sampleSummaryValue(expectedValue) {
if (!expectedValue || typeof expectedValue !== 'object' || Array.isArray(expectedValue)) return expectedValue;
if (Object.prototype.hasOwnProperty.call(expectedValue, 'eq')) return expectedValue.eq;
if (Object.prototype.hasOwnProperty.call(expectedValue, 'oneOf')) return expectedValue.oneOf?.[0];
if (Object.prototype.hasOwnProperty.call(expectedValue, 'lte')) return expectedValue.lte;
if (Object.prototype.hasOwnProperty.call(expectedValue, 'lt')) return Number(expectedValue.lt) - 1;
if (Object.prototype.hasOwnProperty.call(expectedValue, 'gte')) return expectedValue.gte;
if (Object.prototype.hasOwnProperty.call(expectedValue, 'gt')) return Number(expectedValue.gt) + 1;
return expectedValue;
}
function sampleSummary(summarySpec) {
return Object.fromEntries(Object.entries(summarySpec || {}).map(([key, value]) => [key, sampleSummaryValue(value)]));
}
function createEvidence(tempDir, overrides = {}) {
const artifactDir = path.join(tempDir, 'launch-artifacts');
fs.mkdirSync(artifactDir, { recursive: true });
@@ -25,7 +40,7 @@ function createEvidence(tempDir, overrides = {}) {
command: `npm run ${spec.commandIncludes} -- recorded-for-launch-gate`,
completedAt: isoNow(),
artifact,
summary: { ...spec.summary },
summary: sampleSummary(spec.summary),
};
});
@@ -105,6 +120,18 @@ assert.ok(
'migration profile mismatch should be reported as a blocker',
);
const slowBenchmark = runGate(tempDir => {
const evidence = createEvidence(tempDir);
const item = evidence.checks.find(check => check.id === 'performance.api-real-data-read');
item.summary.p95Ms = 301;
return evidence;
});
assert.notEqual(slowBenchmark.status, 0, 'slow API benchmark should fail launch gate');
assert.ok(
slowBenchmark.payload.checks?.some(item => item.id === 'check.performance.api-real-data-read.summary' && item.status === 'blocker'),
'slow API benchmark should be reported as a blocker',
);
const missingBusinessSampling = runGate(tempDir => {
const evidence = createEvidence(tempDir);
evidence.checks = evidence.checks.filter(item => item.id !== 'migration.pb-import-sample');

View File

@@ -56,6 +56,20 @@ const gateChecks = [
commandIncludes: 'pb:import:sample',
summary: { fail: 0 },
},
{
id: 'performance.api-real-data-read',
label: 'Real-data API read benchmark',
commandIncludes: 'perf:api:local',
summary: {
errors: 0,
errorRate: { lte: 0.001 },
p95Ms: { lte: 300 },
p99Ms: { lte: 800 },
concurrency: { gte: 30 },
durationSeconds: { gte: 120 },
includeWrites: false,
},
},
{
id: 'api.integration',
label: 'API integration regression',
@@ -235,13 +249,54 @@ function compareSummary(actualSummary, expectedSummary) {
continue;
}
const actualValue = valueAt(actualSummary, key);
if (actualValue !== expectedValue) {
failures.push(`${key} expected ${JSON.stringify(expectedValue)} but got ${JSON.stringify(actualValue)}`);
const comparisonFailure = compareExpectedValue(actualValue, expectedValue);
if (comparisonFailure) {
failures.push(`${key} ${comparisonFailure}`);
}
}
return failures;
}
function isComparatorSpec(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
return ['eq', 'lt', 'lte', 'gt', 'gte', 'oneOf'].some(key => Object.prototype.hasOwnProperty.call(value, key));
}
function compareExpectedValue(actualValue, expectedValue) {
if (!isComparatorSpec(expectedValue)) {
if (actualValue !== expectedValue) {
return `expected ${JSON.stringify(expectedValue)} but got ${JSON.stringify(actualValue)}`;
}
return '';
}
if (Object.prototype.hasOwnProperty.call(expectedValue, 'eq') && actualValue !== expectedValue.eq) {
return `expected ${JSON.stringify(expectedValue.eq)} but got ${JSON.stringify(actualValue)}`;
}
if (Object.prototype.hasOwnProperty.call(expectedValue, 'oneOf')) {
const choices = Array.isArray(expectedValue.oneOf) ? expectedValue.oneOf : [];
if (!choices.includes(actualValue)) {
return `expected one of ${JSON.stringify(choices)} but got ${JSON.stringify(actualValue)}`;
}
}
const numericChecks = [
['lt', (actual, expected) => actual < expected, '<'],
['lte', (actual, expected) => actual <= expected, '<='],
['gt', (actual, expected) => actual > expected, '>'],
['gte', (actual, expected) => actual >= expected, '>='],
];
for (const [key, predicate, label] of numericChecks) {
if (!Object.prototype.hasOwnProperty.call(expectedValue, key)) continue;
const actualNumber = Number(actualValue);
const expectedNumber = Number(expectedValue[key]);
if (!Number.isFinite(actualNumber) || !Number.isFinite(expectedNumber) || !predicate(actualNumber, expectedNumber)) {
return `expected ${label} ${expectedNumber} but got ${JSON.stringify(actualValue)}`;
}
}
return '';
}
function resultCollector() {
const checks = [];
function pass(id, message, details = {}) {