This commit is contained in:
2026-08-25 15:08:34 +08:00
parent e815abcbd7
commit 44198866d9
64 changed files with 1382 additions and 360 deletions
+2 -1
View File
@@ -31,7 +31,8 @@ node scripts/create-pending-review-orders.mjs
| Admin/Client 本地测试与类型检查 | `scripts/frontend-full-regression.ps1` |
| PDA 本地页面和状态测试 | `../apps/pda-uniapp/tests/*.test.mjs` |
| 本地数据库 SQL 静态/受控验收 | `scripts/local-db-full-acceptance.ps1` |
| AGENTS 快速索引一致性 | `scripts/agents-index-contract.ps1`;只读核对根快速表唯一性、部署单元、业务模块、Client active 路由、海外 Gateway 覆盖、PDA 注册页面、Maven 模块、关键入口与排除目录 |
| AGENTS 快速索引一致性 | `scripts/agents-index-contract.ps1`;只读核对根快速表唯一性、部署单元、业务模块、Client active 路由、海外 Gateway 覆盖、PDA 注册页面、Maven 模块、海外安全服务部署映射、关键入口与排除目录 |
| Java 质量聚合摘要 | `node --test scripts/java-quality-summary.test.mjs`、`node scripts/java-quality-summary.mjs --output target/java-quality-summary.json`;聚合 Maven/服务拓扑、OpenAPI 覆盖、JaCoCo 和 SpotBugs 报告,默认只报告存量缺口,CI 仅用显式 `--require-spotbugs` 检查报告完整性且当前允许失败 |
| 单服务部署失败回滚合同 | `scripts/deploy-built-service-rollback.test.mjs`;模拟成功、健康失败和启动失败,确认仅恢复同服务先前健康镜像且部署任务仍失败 |
Admin/Client HTTP 脚本的离线合同只验证脚本结构、参数和模块边界,不访问测试环境:
+58 -2
View File
@@ -156,6 +156,15 @@ foreach ($gatewayPath in @(
Assert-Contains $overseasGatewayConfig 'uri: lb://lastleg-service' 'Overseas Gateway Lastleg 路由'
Assert-Contains $overseasGatewayConfig 'uri: lb://warehouse-overseas-service' 'Overseas Gateway Warehouse 路由'
# 海外安全投影是独立部署单元,服务映射、海外编排和 Jenkins 构建入口必须同时存在。
$serviceMap = Read-RequiredText (Join-Path $backendRoot 'deploy/service-map.yml')
$overseasCompose = Read-RequiredText (Join-Path $backendRoot 'deploy/docker-compose.overseas.yml')
$serviceJenkinsfile = Read-RequiredText (Join-Path $backendRoot 'Jenkinsfile.service')
Assert-Contains $serviceMap 'overseas-security-service:' '服务部署映射'
Assert-Contains $overseasCompose 'overseas-security-service:' '海外 Compose'
Assert-Contains $overseasCompose '/internal/overseas-security/v1/events' '海外安全事件消费地址'
Assert-Contains $serviceJenkinsfile "'overseas-security-service'" '服务 Jenkins 构建入口'
# Maven 聚合声明的全部历史/当前模块必须在后端根指引中可定位。
[xml]$pom = Get-Content -LiteralPath (Join-Path $backendRoot 'pom.xml') -Raw
foreach ($module in $pom.project.modules.module) {
@@ -174,6 +183,8 @@ $criticalPaths = @(
'apps/admin-web/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue',
'apps/admin-web/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue',
'apps/admin-web/src/modules/container-loading/api/platformContainerLoading.ts',
'apps/admin-web/src/modules/iam/pages/InternalUsersView.vue',
'apps/admin-web/src/modules/iam/api/platformIam.ts',
'apps/client-web/src/api/overseasHttp.ts',
'apps/client-web/src/modules/overseas-warehouse/routes.ts',
'apps/client-web/src/modules/overseas-warehouse/api/warehouseServices.ts',
@@ -203,10 +214,35 @@ $criticalPaths = @(
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftContainerMapper.java',
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformSmartLoadingController.java',
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java',
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/port/in/PriceBookQueryUseCase.java',
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/PriceBookQueryService.java',
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/port/out/PriceBookQueryReader.java',
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/adapter/out/persistence/PriceBookQueryMapper.java',
'baiyue-firstleg/firstleg/docs/firstleg-internal-modularization-baseline.md',
'baiyue-firstleg/firstleg/scripts/analyze-firstleg-dependencies.mjs',
'baiyue-firstleg/firstleg/scripts/analyze-firstleg-dependencies.test.mjs',
'baiyue-firstleg/firstleg/src/test/resources/architecture/firstleg-slice-dependencies.json',
'baiyue-firstleg/auth/src/main/java/com/baiyue/auth/service/OverseasTokenService.java',
'baiyue-firstleg/auth/src/main/java/com/baiyue/auth/service/OverseasSessionValidationService.java',
'baiyue-firstleg/iam/src/main/java/com/baiyue/iam/service/PlatformUserManagementService.java',
'baiyue-firstleg/iam/src/main/java/com/baiyue/iam/service/SecurityEventOutboxService.java',
'baiyue-firstleg/iam/src/main/java/com/baiyue/iam/service/SecurityEventOutboxRelay.java',
'baiyue-firstleg/iam/src/main/java/com/baiyue/iam/entity/IamMembership.java',
'baiyue-firstleg/iam/src/main/java/com/baiyue/iam/service/CustomerDimensionRuleInitializationService.java',
'baiyue-firstleg/iam/src/main/java/com/baiyue/iam/service/CustomerDimensionInitializationRelay.java',
'baiyue-firstleg/iam/src/main/java/com/baiyue/iam/repository/CustomerDimensionInitializationRepository.java',
'baiyue-firstleg/iam/src/main/java/com/baiyue/iam/client/CustomerDimensionRuleClient.java',
'baiyue-firstleg/iam/src/main/resources/db/customer-dimension-initialization-task.sql',
'baiyue-firstleg/common-security/src/main/java/com/baiyue/common/security/AuthenticatedUser.java',
'baiyue-firstleg/common-security/src/main/java/com/baiyue/common/security/JwtClaimsMapper.java',
'baiyue-firstleg/common-openapi/src/test/java/com/baiyue/common/openapi/PdaOpenApiGroupContractTest.java',
'baiyue-firstleg/gateway-overseas/src/main/java/com/baiyue/gateway/filter/PdaSessionValidationFilter.java',
'baiyue-firstleg/gateway-overseas/src/main/java/com/baiyue/gateway/filter/HttpPdaSessionValidationClient.java',
'baiyue-firstleg/overseas-security-service/src/main/java/com/baiyue/overseassecurity/controller/OverseasSecurityController.java',
'baiyue-firstleg/overseas-security-service/src/main/java/com/baiyue/overseassecurity/service/SecurityProjectionService.java',
'baiyue-firstleg/overseas-security-service/src/main/java/com/baiyue/overseassecurity/service/OverseasSecurityValidationService.java',
'baiyue-firstleg/overseas-security-service/src/main/resources/openapi/internal-overseas-security-v1.yaml',
'baiyue-firstleg/overseas-security-service/src/main/resources/db/overseas-security.sql',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/controller/ReceivingContainerController.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/controller/PdaStocktakeController.java',
@@ -220,6 +256,18 @@ $criticalPaths = @(
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/controller/PdaWarehouseController.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseService.java',
'baiyue-firstleg/lastleg-service/src/main/resources/openapi/overseas-pda-operations-v1.yaml',
'baiyue-firstleg/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/controller/InternalPdaPalletTransitionController.java',
'baiyue-firstleg/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehousePalletTransitionService.java',
'baiyue-firstleg/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/entity/WarehousePalletOperationEntity.java',
'baiyue-firstleg/lastleg-service/docs/lastleg-internal-modularization-baseline.md',
'baiyue-firstleg/lastleg-service/scripts/analyze-lastleg-dependencies.mjs',
'baiyue-firstleg/lastleg-service/scripts/analyze-lastleg-dependencies.test.mjs',
'baiyue-firstleg/lastleg-service/src/test/resources/architecture/lastleg-slice-dependencies.json',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/adapter/in/web/PlatformVehicleController.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/application/VehicleApplicationService.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/application/port/out/VehicleRepository.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/adapter/out/persistence/VehiclePersistenceAdapter.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/config/LastlegRestClientFactory.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/controller/MerchantLastlegOrderController.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/model/LastlegOrderType.java',
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/model/LastlegOrderStatus.java',
@@ -240,6 +288,8 @@ $criticalPaths = @(
'baiyue-firstleg/lastleg-service/src/test/java/com/baiyue/lastleg/service/TaillegRealConsumerAcceptanceTest.java',
'baiyue-firstleg/deploy/scripts/deploy-built-service.sh',
'baiyue-firstleg/scripts/deploy-built-service-rollback.test.mjs',
'baiyue-firstleg/scripts/java-quality-summary.mjs',
'baiyue-firstleg/scripts/java-quality-summary.test.mjs',
'docs/四期/船节点标准化契约.md'
)
$criticalPaths | ForEach-Object { Assert-Path $_ }
@@ -255,13 +305,19 @@ $indexedAgentFiles = @(
(Join-Path $backendRoot 'firstleg/AGENTS.md'),
(Join-Path $backendRoot 'finance/AGENTS.md'),
(Join-Path $backendRoot 'lastleg-service/AGENTS.md'),
(Join-Path $backendRoot 'iam/AGENTS.md'),
(Join-Path $backendRoot 'common-security/AGENTS.md'),
(Join-Path $backendRoot 'common-openapi/AGENTS.md'),
(Join-Path $backendRoot 'warehouse-overseas-service/AGENTS.md'),
(Join-Path $backendRoot 'overseas-security-service/AGENTS.md'),
(Join-Path $backendRoot 'event-bridge-service/AGENTS.md'),
(Join-Path $backendRoot 'file/AGENTS.md'),
(Join-Path $backendRoot 'common-event-contract/AGENTS.md')
)
$excludedTargetPattern = '(?i)`(?:[^`]*/)?(node_modules|node_modules\.broken-by-path|dist|target|reports|output|outputs|uni_modules|\.git|\.codex[^/`]*|\.runtime[^/`]*)(?:/|`)'
$excludedTargetPattern = '(?i)`(?:[^`]*/)?(node_modules(?:\.[^/`]*)?|dist|target|reports|output|outputs|uni_modules|\.git|\.codex[^/`]*|\.runtime[^/`]*)(?:/|`)'
$excludedPatternSamples = @(
'| 错误入口 | `node_modules/pkg/` |',
'| 错误入口 | `node_modules.corrupt/.bin/vite` |',
'| 错误入口 | `src/target/classes/` |',
'| 错误入口 | `.codex-temp/task/` |',
'| 错误入口 | `.runtime-tailleg/DbQuery.java` |'
@@ -293,5 +349,5 @@ if ($failures.Count -gt 0) {
exit 1
}
Write-Host "AGENTS 索引合同通过:$checked 项检查,覆盖快速表唯一性、部署单元、前端模块、Client active 路由、Gateway 覆盖、PDA 注册页面与能力开关、Maven 模块、关键入口和排除目录。" -ForegroundColor Green
Write-Host "AGENTS 索引合同通过:$checked 项检查,覆盖快速表唯一性、部署单元、前端模块、Client active 路由、Gateway 覆盖、PDA 注册页面与能力开关、Maven 模块、海外安全服务部署映射、关键入口和排除目录。" -ForegroundColor Green
exit 0
+175
View File
@@ -0,0 +1,175 @@
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
export const repositoryRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)))
const normalize = (value) => value.replaceAll('\\', '/')
const attribute = (source, name) => source.match(new RegExp(`\\b${name}="([^"]*)"`))?.[1]
async function exists(file) {
try {
await stat(file)
return true
} catch (error) {
if (error?.code === 'ENOENT') return false
throw error
}
}
async function javaSourceCount(moduleDirectory) {
const root = path.join(moduleDirectory, 'src', 'main', 'java')
if (!await exists(root)) return 0
let count = 0
async function walk(directory) {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const target = path.join(directory, entry.name)
if (entry.isDirectory()) await walk(target)
else if (entry.isFile() && entry.name.endsWith('.java')) count += 1
}
}
await walk(root)
return count
}
async function openApiFiles(moduleDirectory, module) {
const root = path.join(moduleDirectory, 'src', 'main', 'resources', 'openapi')
if (!await exists(root)) return []
const files = []
async function walk(directory) {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const target = path.join(directory, entry.name)
if (entry.isDirectory()) await walk(target)
else if (entry.isFile() && /\.(?:json|ya?ml)$/i.test(entry.name)) {
files.push(normalize(path.join(module, path.relative(moduleDirectory, target))))
}
}
}
await walk(root)
return files.sort()
}
export function parseModules(pom) {
const block = pom.match(/<modules>([\s\S]*?)<\/modules>/)?.[1] ?? ''
return [...block.matchAll(/<module>([^<]+)<\/module>/g)].map((match) => match[1].trim())
}
export function parseServices(serviceMap) {
return [...serviceMap.matchAll(/^ ([a-z][a-z0-9-]+):$/gm)].map((match) => match[1]).sort()
}
export function parseJacocoReport(xml) {
if (!/<report\b/.test(xml) || !/<\/report>/.test(xml)) throw new Error('not a JaCoCo report')
const counters = [...xml.matchAll(/<counter\b([^>]*)\/?\s*>/g)]
.map((match) => match[1])
.filter((source) => attribute(source, 'type') === 'LINE')
if (counters.length === 0) throw new Error('JaCoCo LINE counter is missing')
const summary = counters.at(-1)
const missed = Number(attribute(summary, 'missed'))
const covered = Number(attribute(summary, 'covered'))
if (!Number.isInteger(missed) || !Number.isInteger(covered)) throw new Error('invalid JaCoCo LINE counter')
const total = missed + covered
return { missed, covered, percentage: total === 0 ? null : Number((covered * 100 / total).toFixed(2)) }
}
export function parseSpotbugsReport(xml) {
if (!/<BugCollection\b/.test(xml) || !/<\/BugCollection>/.test(xml)) throw new Error('not a SpotBugs report')
const findings = [...xml.matchAll(/<BugInstance\b([^>]*)>/g)].map((match) => match[1])
const byPriority = {}
const byCategory = {}
for (const finding of findings) {
const priority = attribute(finding, 'priority') ?? 'unknown'
const category = attribute(finding, 'category') ?? 'unknown'
byPriority[priority] = (byPriority[priority] ?? 0) + 1
byCategory[category] = (byCategory[category] ?? 0) + 1
}
return { findings: findings.length, byPriority, byCategory }
}
async function reportState(file, parser, root) {
const relative = normalize(path.relative(root, file))
if (!await exists(file)) return { status: 'missing', path: relative }
try {
return { status: 'valid', path: relative, ...parser(await readFile(file, 'utf8')) }
} catch (error) {
return { status: 'invalid', path: relative, error: error instanceof Error ? error.message : String(error) }
}
}
export async function buildQualitySummary(root = repositoryRoot) {
const modules = parseModules(await readFile(path.join(root, 'pom.xml'), 'utf8'))
const services = parseServices((await readFile(path.join(root, 'deploy', 'service-map.yml'), 'utf8')).replaceAll('\r\n', '\n'))
const moduleRows = []
const documents = []
for (const module of modules) {
const moduleDirectory = path.join(root, module)
const sources = await javaSourceCount(moduleDirectory)
documents.push(...await openApiFiles(moduleDirectory, module))
if (sources === 0) continue
const jacoco = await reportState(path.join(moduleDirectory, 'target', 'site', 'jacoco', 'jacoco.xml'), parseJacocoReport, root)
const spotbugs = await reportState(path.join(moduleDirectory, 'target', 'spotbugsXml.xml'), parseSpotbugsReport, root)
spotbugs.html = await exists(path.join(moduleDirectory, 'target', 'site', 'spotbugs.html'))
moduleRows.push({ module, javaSources: sources, jacoco, spotbugs })
}
const coveredServices = [...new Set(documents.map((file) => file.split('/')[0]))]
.filter((module) => services.includes(module)).sort()
const codeFirstServices = services.filter((service) => !coveredServices.includes(service))
const count = (kind, status) => moduleRows.filter((row) => row[kind].status === status).length
return {
topology: { modules: modules.length, codeModules: moduleRows.length, services: services.length },
openapi: { documents: documents.length, coveredServices, codeFirstServices, files: documents.sort() },
jacoco: {
present: count('jacoco', 'valid'), invalid: count('jacoco', 'invalid'), missing: count('jacoco', 'missing')
},
spotbugs: {
present: count('spotbugs', 'valid'), invalid: count('spotbugs', 'invalid'),
missing: count('spotbugs', 'missing'), findings: moduleRows.reduce((sum, row) => sum + (row.spotbugs.findings ?? 0), 0)
},
modules: moduleRows
}
}
export function requirementFailures(summary, options = {}) {
const failures = []
for (const row of summary.modules) {
if (row.jacoco.status === 'invalid') failures.push(`${row.module}: invalid JaCoCo report (${row.jacoco.error})`)
if (row.spotbugs.status === 'invalid') failures.push(`${row.module}: invalid SpotBugs report (${row.spotbugs.error})`)
if (row.spotbugs.status === 'valid' && !row.spotbugs.html) failures.push(`${row.module}: SpotBugs HTML report is missing`)
if (options.requireJacoco && row.jacoco.status === 'missing') failures.push(`${row.module}: JaCoCo report is missing`)
if (options.requireSpotbugs && row.spotbugs.status === 'missing') failures.push(`${row.module}: SpotBugs report is missing`)
}
return failures
}
function printSummary(summary) {
process.stdout.write(`Java quality summary: ${summary.topology.modules} modules, ${summary.topology.codeModules} code modules, ${summary.topology.services} services.\n`)
process.stdout.write(`OpenAPI: ${summary.openapi.documents} documents, ${summary.openapi.coveredServices.length}/${summary.topology.services} services; gaps: ${summary.openapi.codeFirstServices.join(', ')}.\n`)
process.stdout.write(`JaCoCo: ${summary.jacoco.present} valid, ${summary.jacoco.missing} missing, ${summary.jacoco.invalid} invalid.\n`)
process.stdout.write(`SpotBugs: ${summary.spotbugs.present} valid, ${summary.spotbugs.missing} missing, ${summary.spotbugs.invalid} invalid, ${summary.spotbugs.findings} findings.\n`)
}
async function main() {
const args = process.argv.slice(2)
const rootIndex = args.indexOf('--root')
const outputIndex = args.indexOf('--output')
const root = rootIndex >= 0 ? path.resolve(args[rootIndex + 1]) : repositoryRoot
const summary = await buildQualitySummary(root)
printSummary(summary)
if (outputIndex >= 0) {
const output = path.resolve(args[outputIndex + 1])
await mkdir(path.dirname(output), { recursive: true })
await writeFile(output, `${JSON.stringify(summary, null, 2)}\n`, 'utf8')
}
const failures = requirementFailures(summary, {
requireJacoco: args.includes('--require-jacoco'),
requireSpotbugs: args.includes('--require-spotbugs')
})
if (failures.length > 0) throw new Error(`Quality report contract failed:\n${failures.map((failure) => `- ${failure}`).join('\n')}`)
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((error) => {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
process.exitCode = 1
})
}
+77
View File
@@ -0,0 +1,77 @@
import assert from 'node:assert/strict'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import test from 'node:test'
import {
buildQualitySummary,
parseJacocoReport,
parseSpotbugsReport,
requirementFailures,
repositoryRoot
} from './java-quality-summary.mjs'
test('parses JaCoCo line coverage and SpotBugs findings without quality thresholds', () => {
assert.deepEqual(parseJacocoReport('<report><counter type="LINE" missed="3" covered="7"/></report>'), {
missed: 3, covered: 7, percentage: 70
})
assert.deepEqual(parseSpotbugsReport('<BugCollection><BugInstance priority="1" category="CORRECTNESS"/><BugInstance priority="2" category="STYLE"/></BugCollection>'), {
findings: 2,
byPriority: { 1: 1, 2: 1 },
byCategory: { CORRECTNESS: 1, STYLE: 1 }
})
assert.throws(() => parseJacocoReport('<report/>'), /not a JaCoCo report/)
assert.throws(() => parseSpotbugsReport('<BugCollection/>'), /not a SpotBugs report/)
})
test('aggregates code modules, report presence and the OpenAPI service matrix', async (t) => {
const root = await mkdtemp(path.join(tmpdir(), 'baiyue-quality-summary-'))
t.after(() => rm(root, { recursive: true, force: true }))
await writeFile(path.join(root, 'pom.xml'), '<modules><module>bom</module><module>alpha</module><module>beta</module></modules>', 'utf8')
await mkdir(path.join(root, 'deploy'), { recursive: true })
await writeFile(path.join(root, 'deploy', 'service-map.yml'), 'services:\n alpha:\n port: 1\n beta:\n port: 2\n', 'utf8')
await mkdir(path.join(root, 'alpha', 'src', 'main', 'java'), { recursive: true })
await mkdir(path.join(root, 'beta', 'src', 'main', 'java'), { recursive: true })
await writeFile(path.join(root, 'alpha', 'src', 'main', 'java', 'Alpha.java'), 'class Alpha {}', 'utf8')
await writeFile(path.join(root, 'beta', 'src', 'main', 'java', 'Beta.java'), 'class Beta {}', 'utf8')
await mkdir(path.join(root, 'alpha', 'src', 'main', 'resources', 'openapi'), { recursive: true })
await writeFile(path.join(root, 'alpha', 'src', 'main', 'resources', 'openapi', 'alpha.yaml'), 'openapi: 3.0.3\n', 'utf8')
await mkdir(path.join(root, 'alpha', 'target', 'site', 'jacoco'), { recursive: true })
await writeFile(path.join(root, 'alpha', 'target', 'site', 'jacoco', 'jacoco.xml'), '<report><counter type="LINE" missed="1" covered="9"/></report>', 'utf8')
await mkdir(path.join(root, 'alpha', 'target', 'site'), { recursive: true })
await writeFile(path.join(root, 'alpha', 'target', 'spotbugsXml.xml'), '<BugCollection><BugInstance priority="2" category="STYLE"/></BugCollection>', 'utf8')
await writeFile(path.join(root, 'alpha', 'target', 'site', 'spotbugs.html'), '<html></html>', 'utf8')
const summary = await buildQualitySummary(root)
assert.deepEqual(summary.topology, { modules: 3, codeModules: 2, services: 2 })
assert.equal(summary.openapi.documents, 1)
assert.deepEqual(summary.openapi.coveredServices, ['alpha'])
assert.deepEqual(summary.openapi.codeFirstServices, ['beta'])
assert.deepEqual(summary.jacoco, { present: 1, invalid: 0, missing: 1 })
assert.deepEqual(summary.spotbugs, { present: 1, invalid: 0, missing: 1, findings: 1 })
assert.deepEqual(requirementFailures(summary), [])
assert.deepEqual(requirementFailures(summary, { requireJacoco: true, requireSpotbugs: true }), [
'beta: JaCoCo report is missing', 'beta: SpotBugs report is missing'
])
})
test('repository freezes the current topology and non-blocking aggregate summary wiring', async () => {
const summary = await buildQualitySummary(repositoryRoot)
assert.deepEqual(summary.topology, { modules: 23, codeModules: 22, services: 16 })
assert.equal(summary.openapi.documents, 27)
assert.equal(summary.openapi.coveredServices.length, 8)
assert.equal(summary.openapi.codeFirstServices.length, 8)
const gitlab = (await readFile(path.join(repositoryRoot, '.gitlab-ci.yml'), 'utf8')).replaceAll('\r\n', '\n')
const job = gitlab.match(/^质量摘要:\n([\s\S]*?)(?=^[^\s#].*:\n)/m)?.[1] ?? ''
const packageJob = gitlab.match(/^打包服务:\n([\s\S]*)/m)?.[1] ?? ''
assert.match(job, /^ allow_failure: true$/m)
assert.match(job, /java-quality-summary\.test\.mjs/)
assert.match(job, /java-quality-summary\.mjs --output target\/java-quality-summary\.json --require-jacoco --require-spotbugs/)
assert.match(job, /^ - job: 校验与测试$/m)
assert.match(job, /^ - job: 静态分析报告$/m)
assert.match(job, /^ - target\/java-quality-summary\.json$/m)
assert.doesNotMatch(job, /threshold|ratchet/i)
assert.doesNotMatch(packageJob, /^ - 质量摘要$/m)
})
+2 -1
View File
@@ -88,7 +88,8 @@ export const SYNCHRONOUS_CLIENTS = Object.freeze([
feign('iam/src/main/java/com/baiyue/iam/client/AuthAccountClient.java',
'iam-service', 'auth-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '账号查询可重复,写操作按账号/命令业务键收敛'),
feign('iam/src/main/java/com/baiyue/iam/client/CustomerDimensionRuleClient.java',
'iam-service', 'firstleg-service', ['REQUEST'], 'WRITE', 'BUSINESS_KEY', '初始化/补偿使用 initializationKey'),
'iam-service', 'firstleg-service', ['BACKGROUND'], 'WRITE', 'BUSINESS_KEY',
'IAM 持久化任务租约投递;初始化/补偿复用稳定 initializationKey,不在客户创建请求内调用'),
feign('iam/src/main/java/com/baiyue/iam/client/OverseasStorageFeeClient.java',
'iam-service', 'warehouse-service', ['REQUEST'], 'WRITE', 'BUSINESS_KEY', '初始化/补偿使用 initializationKey'),
feign('iam/src/main/java/com/baiyue/iam/client/SecurityEventBridgeClient.java',
@@ -32,7 +32,6 @@ const expectedOnlineEdges = [
'firstleg-service->print-service',
'firstleg-service->warehouse-service',
'iam-service->auth-service',
'iam-service->firstleg-service',
'iam-service->warehouse-service',
'lastleg-service->file-service',
'lastleg-service->warehouse-overseas-service',
@@ -57,6 +56,7 @@ const expectedBackgroundEdges = [
'firstleg-service->finance-service',
'firstleg-service->integration-service',
'iam-service->event-bridge-service',
'iam-service->firstleg-service',
'lastleg-service->event-bridge-service',
'order-service->firstleg-service'
]
@@ -172,9 +172,10 @@ test('REQUEST, BACKGROUND and full graphs remain separate executable baselines',
'gateway-overseas->auth-service',
'gateway-overseas->overseas-security-service'
])
assert.deepEqual(stronglyConnectedComponents(graphEdges('REQUEST')), [[
'auth-service', 'file-service', 'firstleg-service', 'iam-service', 'order-service'
]])
assert.deepEqual(stronglyConnectedComponents(graphEdges('REQUEST')), [
['auth-service', 'iam-service'],
['firstleg-service', 'order-service']
])
assert.deepEqual(stronglyConnectedComponents(graphEdges('BACKGROUND')), [[
'event-bridge-service', 'finance-service', 'firstleg-service', 'lastleg-service'
]])
@@ -196,34 +197,32 @@ test('known user entries expose every longest simple REQUEST path over the two-h
return [entry.service, { hops: paths[0].length - 1, paths: paths.map(path => path.join('->')) }]
}))
assert.deepEqual(summary, {
'auth-service': { hops: 5, paths: [
'auth-service->iam-service->firstleg-service->order-service->file-service->integration-service'
'auth-service': { hops: 2, paths: [
'auth-service->iam-service->warehouse-service'
] },
'iam-service': { hops: 4, paths: [
'iam-service->firstleg-service->order-service->file-service->integration-service'
'iam-service': { hops: 1, paths: [
'iam-service->auth-service',
'iam-service->warehouse-service'
] },
'order-service': { hops: 4, paths: [
'order-service->file-service->iam-service->firstleg-service->print-service',
'order-service->file-service->iam-service->firstleg-service->warehouse-service',
'order-service->firstleg-service->file-service->iam-service->auth-service',
'order-service->firstleg-service->file-service->iam-service->warehouse-service',
'order-service->iam-service->firstleg-service->file-service->integration-service'
'order-service->firstleg-service->file-service->iam-service->warehouse-service'
] },
'firstleg-service': { hops: 4, paths: [
'firstleg-service->order-service->file-service->iam-service->auth-service',
'firstleg-service->order-service->file-service->iam-service->warehouse-service'
] },
'finance-service': { hops: 5, paths: [
'finance-service->file-service->iam-service->firstleg-service->order-service->warehouse-service',
'finance-service->firstleg-service->order-service->file-service->iam-service->auth-service',
'finance-service->firstleg-service->order-service->file-service->iam-service->warehouse-service',
'finance-service->iam-service->firstleg-service->order-service->file-service->integration-service'
'finance-service->firstleg-service->order-service->file-service->iam-service->warehouse-service'
] },
'file-service': { hops: 4, paths: [
'file-service->iam-service->firstleg-service->order-service->warehouse-service'
'file-service': { hops: 2, paths: [
'file-service->iam-service->auth-service',
'file-service->iam-service->warehouse-service'
] },
'lastleg-service': { hops: 5, paths: [
'lastleg-service->file-service->iam-service->firstleg-service->order-service->warehouse-service'
'lastleg-service': { hops: 3, paths: [
'lastleg-service->file-service->iam-service->auth-service',
'lastleg-service->file-service->iam-service->warehouse-service'
] },
'warehouse-service': { hops: 0, paths: ['warehouse-service'] },
'warehouse-overseas-service': { hops: 0, paths: ['warehouse-overseas-service'] }