test: expose acceptance coverage metrics

This commit is contained in:
2026-08-25 09:50:02 +08:00
parent 46dd0b54eb
commit b211121365
2 changed files with 122 additions and 4 deletions
+54 -4
View File
@@ -281,6 +281,56 @@ function derived(length, width, height, edge) {
return { outerLength, outerWidth, outerHeight, volume: halfUp3(outerLength * outerWidth * outerHeight / 1_000_000) }
}
export function buildAcceptanceSummary(steps = [], modules = [], coverageGaps = []) {
const failed = steps.filter((step) => !step.passed).length
const executedModules = modules.filter((module) => !module.skipped)
const uniqueModuleNames = new Set(executedModules.map((module) => module.name).filter(Boolean))
const uniqueRequestKeys = new Set()
const uniqueStepNameSet = new Set()
for (const step of steps) {
if (step.name) uniqueStepNameSet.add(step.name)
const method = step.request?.method
const path = step.request?.path
if (!method || !path) continue
uniqueRequestKeys.add(`${method}\u001f${path}\u001f${step.requestId || ''}`)
}
const coverageGapStats = {
total: coverageGaps.length,
blocking: 0,
nonBlocking: 0,
byCategory: {},
}
for (const gap of coverageGaps) {
const category = gap.category || 'uncategorized'
const blocking = gap.blocking === true
const categoryStats = coverageGapStats.byCategory[category]
|| { total: 0, blocking: 0, nonBlocking: 0 }
categoryStats.total += 1
if (blocking) {
coverageGapStats.blocking += 1
categoryStats.blocking += 1
} else {
coverageGapStats.nonBlocking += 1
categoryStats.nonBlocking += 1
}
coverageGapStats.byCategory[category] = categoryStats
}
return {
total: steps.length,
passed: steps.length - failed,
failed,
moduleRuns: executedModules.length,
uniqueModules: uniqueModuleNames.size,
uniqueRequests: uniqueRequestKeys.size,
uniqueStepNames: uniqueStepNameSet.size,
skippedModules: modules.length - executedModules.length,
coverageGaps: coverageGapStats,
}
}
export class AcceptanceRun {
constructor(options) {
this.options = options
@@ -1141,13 +1191,13 @@ export class AcceptanceRun {
this.addStep('report-security', '报告敏感信息扫描', null, !sensitive,
sensitive ? '检测到密码、Token、二进制/base64 或 OCR 原文字段' : '未发现密码、Token、文件二进制/base64 或 OCR 原文字段')
const failed = this.steps.filter((step) => !step.passed).length
const summary = buildAcceptanceSummary(this.steps, this.modules, this.coverageGaps)
return {
reportName: '后台管理测试环境 HTTP API 验收', marker: this.marker,
gateway: this.options.gateway, overseasGateway: this.options.overseasGateway,
generatedAt: new Date().toISOString(), startedAt: this.startedAt,
success: this.fatal === null && failed === 0, fatal: this.fatal,
summary: { total: this.steps.length, passed: this.steps.length - failed, failed },
success: this.fatal === null && summary.failed === 0, fatal: this.fatal,
summary,
modules: this.modules, scenarios: this.scenarios, deploymentBlockers: this.deploymentBlockers,
coverageGaps: this.coverageGaps,
createdResources: this.createdResources,
@@ -1169,7 +1219,7 @@ async function main() {
await mkdir(dirname(outputPath), { recursive: true })
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8')
console.log(`REPORT=${outputPath}`)
console.log(`RESULT=${report.success ? 'PASS' : 'FAIL'} total=${report.summary.total} passed=${report.summary.passed} failed=${report.summary.failed}`)
console.log(`RESULT=${report.success ? 'PASS' : 'FAIL'} total=${report.summary.total} passed=${report.summary.passed} failed=${report.summary.failed} metric=assertions moduleRuns=${report.summary.moduleRuns} uniqueModules=${report.summary.uniqueModules} uniqueRequests=${report.summary.uniqueRequests} skippedModules=${report.summary.skippedModules}`)
process.exitCode = report.success ? 0 : 1
}
@@ -0,0 +1,68 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { buildAcceptanceSummary } from './admin-test-env-acceptance.mjs'
test('report summary separates assertion, module-run and unique-request metrics', () => {
const sharedResponse = {
method: 'GET', path: '/api/platform/v1/orders?page=1', requestId: 'request-1',
}
const steps = [
{ module: 'orders', name: '订单列表成功', passed: true,
request: { method: sharedResponse.method, path: sharedResponse.path }, requestId: sharedResponse.requestId },
{ module: 'orders', name: '订单列表数据结构正确', passed: true,
request: { method: sharedResponse.method, path: sharedResponse.path }, requestId: sharedResponse.requestId },
{ module: 'orders', name: '订单列表成功', passed: false,
request: { method: 'GET', path: '/api/platform/v1/orders?page=2' }, requestId: 'request-2' },
{ module: 'report-security', name: '报告敏感信息扫描', passed: true, request: null, requestId: null },
]
const modules = [
{ name: 'orders' },
{ name: 'orders' },
{ name: 'finance', skipped: 'owned by finance full chain' },
]
const summary = buildAcceptanceSummary(steps, modules, [])
assert.deepEqual(summary, {
total: 4,
passed: 3,
failed: 1,
moduleRuns: 2,
uniqueModules: 1,
uniqueRequests: 2,
uniqueStepNames: 3,
skippedModules: 1,
coverageGaps: { total: 0, blocking: 0, nonBlocking: 0, byCategory: {} },
})
})
test('request evidence without requestId is deduplicated by method and path', () => {
const summary = buildAcceptanceSummary([
{ name: 'CSV 状态', passed: true, request: { method: 'GET', path: '/api/export' }, requestId: null },
{ name: 'CSV 表头', passed: true, request: { method: 'GET', path: '/api/export' }, requestId: null },
{ name: 'CSV POST', passed: true, request: { method: 'POST', path: '/api/export' }, requestId: null },
])
assert.equal(summary.uniqueRequests, 2)
})
test('coverage gaps are counted by category and blocking status without copying details', () => {
const summary = buildAcceptanceSummary([], [], [
{ category: 'device', blocking: false, reason: 'camera', secretEvidence: 'must-not-copy' },
{ category: 'device', blocking: true, reason: 'scanner' },
{ category: 'external-third-party', blocking: false, reason: 'captcha' },
{ blocking: true, reason: 'fixture' },
])
assert.deepEqual(summary.coverageGaps, {
total: 4,
blocking: 2,
nonBlocking: 2,
byCategory: {
device: { total: 2, blocking: 1, nonBlocking: 1 },
'external-third-party': { total: 1, blocking: 0, nonBlocking: 1 },
uncategorized: { total: 1, blocking: 1, nonBlocking: 0 },
},
})
assert.equal(JSON.stringify(summary).includes('must-not-copy'), false)
})