From b2111213650e493a851af56bbdd3e7281239429c Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 25 Aug 2026 09:50:02 +0800 Subject: [PATCH] test: expose acceptance coverage metrics --- scripts/admin-test-env-acceptance.mjs | 58 ++++++++++++++-- ...-test-env-report-summary-contract.test.mjs | 68 +++++++++++++++++++ 2 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 scripts/admin-test-env-report-summary-contract.test.mjs diff --git a/scripts/admin-test-env-acceptance.mjs b/scripts/admin-test-env-acceptance.mjs index 753f2b0db..51ba55c58 100644 --- a/scripts/admin-test-env-acceptance.mjs +++ b/scripts/admin-test-env-acceptance.mjs @@ -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 } diff --git a/scripts/admin-test-env-report-summary-contract.test.mjs b/scripts/admin-test-env-report-summary-contract.test.mjs new file mode 100644 index 000000000..1171aecce --- /dev/null +++ b/scripts/admin-test-env-report-summary-contract.test.mjs @@ -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) +})