This commit is contained in:
2026-10-21 13:22:35 +08:00
parent b4ff441a70
commit 337da6d67c
25 changed files with 528 additions and 43 deletions
+2 -1
View File
@@ -10,7 +10,8 @@
| 类别 | 正式入口 | 管理边界 |
|---|---|---|
| 本机重型检查限流 | `local-check.ps1` | 所有工作目录、对话共用一个用户级执行名额;PowerShell 7,状态/超时/并发与 Job Object 回收规则见外层 `docs/AI工程执行与协作约束.md`;只读状态不申请重型名额 |
| 本机重型检查限流 | `local-check.ps1` | 所有工作目录、对话共用两个用户级执行名额;PowerShell 7,状态/超时/并发与 Job Object 回收规则见外层 `docs/AI工程执行与协作约束.md`;只读状态不申请重型名额 |
| 后端分层检查 | `backend-check.ps1` | 默认指定模块回归,支持测试源码编译和精确测试类;必填模块,自动接入重型限流;全量触发条件见后端根指引,参数及副作用见 README |
| 测试/灰度单票验收 | `prod-release-acceptance.mjs`、`prod-release-delivery-stage.mjs`、内部 `prod-release-order-stage.mjs` | 显式 test/gray、统一 Gateway/accessToken;同一票义乌到洛杉矶、前置同时核对东莞;full-flow 串行覆盖买单FBA正常链至签收回传及车次资源释放,保留分阶段入口与独立检查点;不调用测试造数模块,不创建账号/主档或删除数据;配置和边界见 README |
| Admin 全量 HTTP 验证 | `admin-test-env-acceptance.mjs` | 通过显式环境文件运行统一 TEST Gateway/会话,拒绝旧海外网关参数;默认模块覆盖平台全链路,定向验证使用 `--modules`,模块注册和依赖以总入口及 `admin-test-env-modules/README.md` 为准 |
| Client 全量 HTTP 验证 | `client-test-env-acceptance.mjs` | 通过显式环境文件运行统一 TEST Gateway/会话,拒绝旧海外网关参数;覆盖客户工作台完整链路,`client-phase2-test-env-acceptance.mjs` 只是入库测量与计费专项,不替代全量入口 |
+54 -2
View File
@@ -1,5 +1,29 @@
# 后端脚本管理与全量 HTTP 验收索引
## 先看范围,再看证据
`acceptance-overview.mjs` 是离线只读总览入口,不需要凭据、不联网、不创建报告或测试数据。默认范围直接读取 Admin 实际注册表,不维护另一份模块枚举:
```powershell
node scripts/acceptance-overview.mjs
node scripts/acceptance-overview.mjs --report reports/<明确的一次验收报告>.json
```
| 验收层 | 正式入口 | 结论边界 |
| --- | --- | --- |
| Admin 默认 HTTP | `admin-test-env-acceptance.mjs` | 总览的 defaultModules;optionalModules 需显式指定,不自动计入 |
| Client 全量 HTTP | `client-test-env-acceptance.mjs` | 独立执行,跳过详情不得视为详情已验证 |
| 测量计费专项 | `client-phase2-test-env-acceptance.mjs` | 不替代 Client 全量,使用本轮样本 |
| 车辆等专项 | 本索引对应的专项入口 | 只覆盖各自声明范围,不能合并宣称默认全量一次通过 |
| 离线检查 | `local-check.ps1` 包装受影响测试 | 证明源码检查,不证明部署或真实 HTTP |
| 页面与真机 | 应用验收规则 | 单独提供操作和视觉证据,HTTP 不代替 |
总览显式读取一份报告,不遍历历史目录挑选绿灯。`FAILED` 表示有失败、fatal 或阻断缺口;`INCOMPLETE` 表示有跳过、非阻断缺口、计划模块未出现或仅准备/局部模式;`RECORDED_CHECKS_PASSED` 只表示所记录检查通过,**不是当前版本全量通过**。旧报告没有执行计划时显示 `UNKNOWN_LEGACY_REPORT`,不反推范围。报告损坏或格式不支持时退出1,不自动重跑;正常分析退出0,即使报告失败也不作为 CI 放行命令。
新 Admin 报告的 `executionScope` 保存申请模块、依赖展开后的计划、当轮默认范围及模式限制;这里只记录计划,不以注册即视为已执行。总览将有请求失败与无请求失败分开统计,后者包含模块退出/前置不足,不能把失败条数直接当作独立 Bug 数。缺少模块断言也仅表示缺少证据,不猜测未执行原因。
最新部署与已验/未验状态仍只维护在外层 `docs/测试环境单机部署与配置契约.md`;本索引不复制每日进度。总览不校验实际部署版本、不读取环境文件,执行前仍须经正式入口校验凭据和环境。直接离线测试为 `acceptance-overview.test.mjs`。
`local-dispatch-transport-acceptance.mjs`:仅本机 LOCAL 的现有排车运输任务 HTTP 验收。显式 `node --env-file=<本机凭据文件>` 注入 `ADMIN_TEST_GATEWAY`、`ADMIN_TEST_USERNAME/PASSWORD`、`LOCAL_TRANSFER_PDA_PASSWORD`;拒绝远端网关。创建3张 TEST 仓储转运单、1张本土卡车原单、5张排车(完成/取消/重新安排),以及本轮专用调度/PDA账号、角色、车辆司机;缺失的正式排车权限仅补资源并授予专用角色,不修改既有角色。经正式网关验证原单审核、跨客户取货、资源冲突、凭证隔离、取消重排、发车完成回写、独立仓库清点和车辆司机释放。合成 PNG 只验文件链路,不代表实际运输或有效证照。默认报告 `reports/local-dispatch-transport.json`,可用 `TRANSPORT_ACCEPTANCE_OUTPUT` 指定;每步保留请求键、脱敏意图、错误码和关联标识,失败停止且保留数据。确认已知失败结果后显式 `TRANSPORT_ACCEPTANCE_RESUME=true` 复用同一报告继续;网络结果未知必须先核对,不自动清理或删除,不使用业务 SQL。已成功批次禁止重复运行;不能替代远端部署或人工截图验收。
`local-check.ps1`:Windows / PowerShell 7 本机重型检查统一入口,适用于前后端测试、类型检查、构建、安装依赖及 Maven。示例:`& ./scripts/local-check.ps1 -WorkingDirectory ../apps/admin-web -Label 'Admin typecheck' -Command @('pnpm', 'typecheck')`;`-Status` 查看占用。当前用户所有工作目录共用一个文件句柄锁,默认排队900秒、执行1200秒;超时分别退出75/124,其余透传命令退出码。Vitest workers=2,Node堆2048MB,Java堆1536MB/处理器2;直接 Maven 自动单线程/单fork,直接 Node 内置测试自动并发2。Windows Job Object 仅回收本次后代,不接管旧进程;嵌套调用拒绝,批次内部执行原始命令。无凭据、不主动联网或写业务数据;被包装命令的环境授权不变。状态和最近结果仅写 `%LOCALAPPDATA%/Chongchong/LocalChecks/`,不存命令参数或输出;不自动缓存成功结果。失败保留最近摘要,确认输入后重新排队;锁文件无需手工删除,状态不是验收报告。完整规则见 `../../docs/AI工程执行与协作约束.md` 的“本机重型检查与多对话限流”;离线行为测试为 `local-check.test.mjs`。
@@ -309,7 +333,8 @@ BOL长托盘码按现行模板省略,自动核验使用完整入仓码与本
| 订单与客户本地验收 | `phase1-acceptance.ps1` | 订单与客户本地服务/API 综合验收,会创建本地业务数据 |
| 订单与客户本地验收 | `phase1-business-maturity-regression.ps1` | 订单与客户业务成熟度回归,调用本地服务并生成报告 |
| 订单与客户本地验收 | `phase1-onboarding-order-e2e.ps1` | 注册、资质、审核到下单的本地 E2E,会创建客户、仓库、往来单位和订单 |
| 本地质量 | `backend-full-regression.ps1` | Gateway 路由与 Mapper 门禁后执行根 POM 全部有效模块 Maven 测试;`-DryRun` 只打印与校验 |
| 本地质量 | `backend-check.ps1` | 必填模块,默认模块回归;Compile 编译测试源码,Focused 运行精确测试类并检查本轮报告;`-DryRun` 只校验选择并打印 |
| 本地质量 | `backend-full-regression.ps1` | Gateway 路由与 Mapper 门禁后执行根 POM 全部有效模块 Maven 测试;`-DryRun` 只打印与校验;全量触发条件见后端根 AGENTS |
| 本地质量 | `frontend-full-regression.ps1` | 编排 Admin/Client 本地测试、类型检查及国内/海外 PDA Node 全量测试,不访问 Gateway |
| 本地质量 | `database-comment-audit.ps1` | 离线扫描数据库中文注释,不连接数据库 |
| Jenkins 只读数据库结构 | `database-schema-contract.mjs` | 只查询 `information_schema` 核对区域初始化 SQL 的表和字段;不执行 SQL、不造业务数据、不执行迁移 |
@@ -402,7 +427,7 @@ Gateway 路由防漏:`node --test scripts/gateway-route-contract.test.mjs`。
| 检查 | 入口 |
|---|---|
| 后端 Maven 契约/单元测试 | `scripts/backend-full-regression.ps1` |
| 后端 Maven 契约/单元测试 | 日常 `scripts/backend-check.ps1`;全仓 `scripts/backend-full-regression.ps1` |
| Admin/Client/国内与海外 PDA 本地全量回归 | `scripts/frontend-full-regression.ps1` |
| PDA 本地页面和状态定向测试 | `../apps/pda-uniapp/tests/*.test.mjs` |
| 本地数据库 SQL 静态/受控验收 | `scripts/local-db-full-acceptance.ps1` |
@@ -420,6 +445,33 @@ Admin/Client HTTP 脚本的离线合同只验证脚本结构、参数和模块
| Client 消息中心调用 | `scripts/client-notifications-contract.test.mjs` |
| 验收入口与模块注册 | `scripts/full-regression-entry-contract.test.mjs`、`scripts/controller-pipeline-contract.test.mjs` |
## 后端分层检查
`backend-check.ps1` 是本地日常入口,PowerShell 7 执行,自动经 `local-check.ps1` 占用一个重型名额。全量触发条件唯一见 `../AGENTS.md`;不自动根据 Git diff 猜测跨服务消费者,不自动缓存或跳过测试。
在后端根目录执行,`-Modules` 使用逗号分隔的当前根 POM 模块名:
```powershell
# 只查看范围,不运行 Maven。
.\scripts\backend-check.ps1 -Modules order -DryRun
# 签名变化:保留生产及测试源码编译。
.\scripts\backend-check.ps1 -Modules order -Mode Compile
# 开发迭代:精确类名,支持逗号分隔和全限定名。
.\scripts\backend-check.ps1 -Modules common-core -Mode Focused -Tests ApiResponseTest
# 单服务收尾:指定模块及上游依赖测试。
.\scripts\backend-check.ps1 -Modules order
# 跨服务收尾:显式列出受影响生产者、消费者。
.\scripts\backend-check.ps1 -Modules 'order,firstleg'
```
三种模式均使用根 reactor 的 `-pl <modules> -am`,不会安装制品到本地仓库,不默认 `clean`。`Compile` 运行 `test-compile`;`Focused`/`Module` 运行 `test`。`-am` 只补上游构建依赖,HTTP/事件下游消费者须由调用者显式列入。公共模块变更也须核对下游,不能只测试公共模块自身。Focused 只用于迭代,不能替代有行为变化时的模块收尾;API、Mapper、索引等相关静态合同仍按其规则单独执行。
Focused 不接受通配符或方法选择,每个类必须唯一命中所选模块的 `src/test/java`。为允许上游无同名测试,Maven 设置 `surefire.failIfNoSpecifiedTests=false`,随后逐类核对本轮更新的 Surefire XML;报告缺失、陈旧、全部跳过或含失败均不能通过,避免零测试假绿。
无环境凭据,不调用真实 HTTP、部署或共享数据库,不启动常驻服务;Maven 可读取依赖缓存、下载缺失依赖,并写各模块 `target/`,测试中的隔离内存数据库行为不变。沿用现有 Maven 测试配置,不额外启用环境集成测试。报告仍在各模块 `target/surefire-reports/`,不创建额外报告目录、不删除旧报告。失败保留产物,修复后重新执行相同范围;报告新鲜度校验仅防止复用旧结果,不支持同一模块在多个对话并发执行测试。
`-MavenCommand` 可指定 Maven 可执行文件,默认 `mvn`。退出码:0 为所选范围通过(DryRun 仅预检通过),2 为模块/测试选择无效;Maven 非零退出码透传,报告核验失败返回非零。排队/执行超时沿用 `local-check.ps1` 的 75/124。入口行为测试为 `backend-check.test.mjs`。
## 后端全量回归
## 入口
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env node
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { describeAcceptanceScope } from './admin-test-env-acceptance.mjs'
/** 只输出白名单统计和模块名,不转抄报告中的账号、地址、凭据或业务载荷。 */
export function summarizeReport(report) {
if (!report || !Array.isArray(report.steps) || report.steps.length === 0
|| report.steps.some((step) => typeof step.passed !== 'boolean')) {
throw new Error('报告缺少有效 steps,不能认定验收结果')
}
const steps = report.steps
const failed = steps.filter((step) => !step.passed && !step.skipped)
const skipped = steps.filter((step) => step.skipped)
const skippedModules = Array.isArray(report.modules) ? report.modules.filter((module) => module.skipped).length : 0
const gaps = Array.isArray(report.coverageGaps) ? report.coverageGaps : []
const blockers = gaps.filter((gap) => gap.blocking || gap.deploymentBlocker)
const scope = report.executionScope
// 历史报告不包含计划信息,不能从断言数量或模块名字推定完整执行。
const scopeKnown = Array.isArray(scope?.plannedModules) && scope.plannedModules.length > 0
&& Array.isArray(scope.defaultModules) && scope.defaultModules.length > 0
const observedModules = new Set(steps.map((step) => step.module))
const missingModules = scopeKnown ? scope.plannedModules.filter((name) => !observedModules.has(name)) : []
const limited = Boolean(scope?.preparationOnly || scope?.returnMainChainOnly || report.fullAcceptance === false)
const failedResult = Boolean(report.fatal) || report.success === false || failed.length > 0 || blockers.length > 0
return {
result: failedResult ? 'FAILED' : skipped.length || skippedModules || gaps.length || missingModules.length || limited ? 'INCOMPLETE' : 'RECORDED_CHECKS_PASSED',
passed: steps.filter((step) => step.passed && !step.skipped).length,
failed: failed.length,
skipped: skipped.length,
skippedModules,
blockingGaps: blockers.length,
nonBlockingGaps: gaps.length - blockers.length,
failedModules: [...new Set(failed.map((step) => step.module).filter(Boolean))],
// 无请求的失败包含前置不足和模块退出,不能计作独立接口缺陷。
failuresWithRequest: failed.filter((step) => step.request).length,
failuresWithoutRequest: failed.filter((step) => !step.request).length,
scope: !scopeKnown ? 'UNKNOWN_LEGACY_REPORT' : limited ? 'LIMITED_SCOPE'
: scope.defaultModules.every((name) => scope.plannedModules.includes(name)) ? 'DEFAULT_ADMIN_PLAN' : 'SELECTED_MODULES',
missingPlannedModules: missingModules,
currentVersionVerified: false,
pageAcceptance: 'NOT_VERIFIED_BY_HTTP',
}
}
/** 默认仅展示范围;显式报告只读分析,不发现历史目录、不联网、不执行验收。 */
async function main(argv) {
if (argv.length && !(argv.length === 2 && argv[0] === '--report')) {
throw new Error('用法:node scripts/acceptance-overview.mjs [--report <明确JSON路径>]')
}
const scope = describeAcceptanceScope()
const result = {
purpose: '离线验收范围总览,不执行HTTP,不证明当前部署通过',
admin: { entry: 'scripts/admin-test-env-acceptance.mjs', ...scope },
otherEntries: [
{ entry: 'scripts/client-test-env-acceptance.mjs', scope: 'Client全量HTTP,独立执行' },
{ entry: 'scripts/client-phase2-test-env-acceptance.mjs', scope: '测量计费专项,不替代Client全量' },
{ entry: 'scripts/fleet-test-env-acceptance.mjs', scope: '车辆专项,不计入Admin默认全量' },
],
boundaries: ['可选模块不等于默认执行;其他专项入口详见scripts/README.md',
'本地合同、HTTP、页面与真机验收分别报告',
'报告通过只代表该次记录;需另核对源码、配置与实际部署版本',
'环境与凭据前置仍由实际入口校验;本工具不读取环境文件'],
}
if (argv.length) result.report = summarizeReport(JSON.parse(await readFile(resolve(argv[1]), 'utf8')))
console.log(JSON.stringify(result, null, 2))
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
main(process.argv.slice(2)).catch((error) => { console.error(error.message); process.exitCode = 1 })
}
+46
View File
@@ -0,0 +1,46 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { summarizeReport } from './acceptance-overview.mjs'
import { describeAcceptanceScope } from './admin-test-env-acceptance.mjs'
test('注册表返回独立副本,默认与可选不重复', () => {
const scope = describeAcceptanceScope()
assert.ok(scope.defaultModules.includes('appointment-work'))
assert.ok(scope.optionalModules.includes('phase4-overseas-return-followups'))
assert.ok(scope.optionalModules.every((name) => !scope.defaultModules.includes(name)))
scope.defaultModules.length = 0
assert.ok(describeAcceptanceScope().defaultModules.length > 0)
})
test('旧报告全绿也不推断默认全量或当前版本通过', () => {
const result = summarizeReport({ success: true, steps: [{ module: 'orders', passed: true }] })
assert.equal(result.scope, 'UNKNOWN_LEGACY_REPORT')
assert.equal(result.currentVersionVerified, false)
assert.equal(result.result, 'RECORDED_CHECKS_PASSED')
})
test('跳过、覆盖缺口和未执行模块不能被success掩盖', () => {
const base = { success: true, steps: [{ module: 'orders', passed: true }],
executionScope: { plannedModules: ['orders', 'finance'], defaultModules: ['orders', 'finance'] } }
assert.equal(summarizeReport(base).result, 'INCOMPLETE')
assert.deepEqual(summarizeReport(base).missingPlannedModules, ['finance'])
assert.equal(summarizeReport({ ...base, coverageGaps: [{ blocking: true }] }).result, 'FAILED')
assert.equal(summarizeReport({ steps: [{ passed: true, skipped: true }] }).result, 'INCOMPLETE')
assert.equal(summarizeReport({ steps: [{ passed: true }], modules: [{ name: 'orders', skipped: true }] }).result, 'INCOMPLETE')
})
test('前置退出与接口失败分开统计,输出不携带敏感载荷', () => {
const result = summarizeReport({ success: false, gateway: 'SECRET', steps: [
{ module: 'exports', passed: false, request: { path: 'SECRET' }, message: 'SECRET' },
{ module: 'dependent', passed: false, evidence: 'SECRET' },
] })
assert.equal(result.failed, 2)
assert.equal(result.failuresWithRequest, 1)
assert.equal(result.failuresWithoutRequest, 1)
assert.equal(JSON.stringify(result).includes('SECRET'), false)
})
test('准备数据和局部模式不冒充完整验收,空报告拒绝', () => {
assert.equal(summarizeReport({ fullAcceptance: false, steps: [{ passed: true }] }).result, 'INCOMPLETE')
assert.throws(() => summarizeReport({ success: true, steps: [] }), /steps/)
})
+18
View File
@@ -281,6 +281,14 @@ export function normalizeModuleOrder(modules) {
}
const DEFAULT_MODULES = normalizeModuleOrder(CONFIGURED_MODULES)
/** 从执行注册表返回范围,供离线总览使用,避免维护第二套模块清单。 */
export function describeAcceptanceScope() {
return {
defaultModules: [...DEFAULT_MODULES],
optionalModules: OPTIONAL_MODULES.filter((name) => !DEFAULT_MODULES.includes(name)),
}
}
const WEIGHT_IMAGE_PATH = process.env.ADMIN_TEST_WEIGHT_IMAGE_PATH || ''
const CUSTOMER_FIELDS = [
'id', 'tenantCode', 'tenantName', 'status', 'legalName', 'legalRepresentative',
@@ -1940,6 +1948,16 @@ export class AcceptanceRun {
return {
reportName: this.options.prepareDomesticPdaH5 ? '国内 PDA H5 待拣任务准备(非全链路验收)' : this.options.environment === 'gray' ? '灰度环境 FBA 定向闭环 HTTP API 验收' : '后台管理测试环境 HTTP API 验收', marker: this.marker,
environment: this.options.environment || 'test', gateway: this.options.gateway,
executionScope: {
requestedModules: [...(this.options.requestedModules || [])],
plannedModules: [...this.options.modules],
defaultModules: [...DEFAULT_MODULES],
defaultPlanSelected: DEFAULT_MODULES.every((name) => this.options.modules.includes(name)),
preparationOnly: this.options.prepareDomesticPdaH5 === true || this.options.prepareDropshipManualCount === true,
returnMainChainOnly: this.options.returnMainChainOnly === true,
evidenceType: 'HTTP',
pageAcceptance: 'NOT_VERIFIED',
},
generatedAt: new Date().toISOString(), startedAt: this.startedAt,
success: this.fatal === null && summary.failed === 0, fatal: this.fatal,
...(this.options.prepareDomesticPdaH5 ? { fullAcceptance: false, purpose: 'DOMESTIC_PDA_H5_FIXTURE' } : {}),
+2
View File
@@ -1,5 +1,7 @@
# Admin 测试环境 HTTP 模块
先运行 `node scripts/acceptance-overview.mjs`(后端根目录,无网络)查看当前默认与可选模块。范围来自真实注册表;本文件描述各模块用途与依赖,不代表所有模块默认执行或已通过。报告解读及跨端/专项边界见 [总索引](../README.md#先看范围再看证据),最新运行状态只查外层部署契约。
`dispatch-workflow-extension-acceptance.mjs` 由 `../dispatch-workflow-extensions-test-env.mjs` 显式调用,不是独立CLI。双客户双目的仓扩展先从正式目录核验两FBA仓,复用本轮独立来货和PDA实收;派送费用按真实group客户使用不同合成金额,完整正常链后核对逐客户目的仓、费用、单据和交叉读取拒绝。`fleet-cross-customer-fixture.mjs` 可接收两个明确 `destinationWarehouseIds`,在任何造数写前核验全部目的仓;原单目的仓调用不变。所有写经过既有逐请求持久化和未知结果停写边界,不修改共享runner;中途故障注入无工具时明确NOT_TESTED。直接合同 `dispatch-workflow-extension-contract.test.mjs` 不证明环境通过。
其 `competition` 扩展只并发两个已经成功装车命令的跨端原键重放,并回查完整库存、主单、货物和车次;不是首次执行竞争。占用资源与未安排派送发车分别串行验证拒绝后快照不变。签收成功后的自动完结只做有界只读等待,保持真实写回执,等待失败阻断下一请求而不伪报已知写为未知结果。
+117
View File
@@ -0,0 +1,117 @@
[CmdletBinding()]
param(
[ValidateSet('Compile', 'Focused', 'Module')][string]$Mode = 'Module',
[string]$Modules,
[string]$Tests,
[switch]$DryRun,
[string]$MavenCommand = 'mvn'
)
$ErrorActionPreference = 'Stop'
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$pom = Join-Path $repoRoot 'pom.xml'
# 显式选择当前聚合模块,避免漏传参数时意外触发全仓测试。
function Read-Selection([string]$Value, [string]$Name) {
if ([string]::IsNullOrWhiteSpace($Value)) { throw "$Name is required." }
$items = @($Value.Split(',') | ForEach-Object { $_.Trim() })
if ($items -contains '') { throw "$Name contains an empty selection." }
return @($items | Select-Object -Unique)
}
try {
[xml]$rootPom = Get-Content -LiteralPath $pom -Raw
$selectedModules = @(Read-Selection $Modules 'Modules')
foreach ($module in $selectedModules) {
if ($module -cnotin @($rootPom.project.modules.module)) {
throw "Unknown active module: $module"
}
}
$selectedTests = @()
if ($Mode -eq 'Focused') {
$requestedTests = @(Read-Selection $Tests 'Tests')
# 只接受明确类名,不接受通配符或方法筛选;每个请求必须唯一命中所选模块。
foreach ($requested in $requestedTests) {
if ($requested -cnotmatch '^[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*$') {
throw "Tests must contain exact Java class names: $requested"
}
$matches = @(foreach ($module in $selectedModules) {
$testRoot = Join-Path $repoRoot "$module/src/test/java"
if (-not (Test-Path -LiteralPath $testRoot)) { continue }
foreach ($file in Get-ChildItem -LiteralPath $testRoot -Recurse -File -Filter "$($requested.Split('.')[-1]).java") {
$source = Get-Content -LiteralPath $file.FullName -Raw
$package = [regex]::Match($source, '(?m)^\s*package\s+([\w.]+)\s*;').Groups[1].Value
$className = if ($package) { "$package.$($file.BaseName)" } else { $file.BaseName }
if ($requested -ceq $file.BaseName -or $requested -ceq $className) {
[pscustomobject]@{ module = $module; className = $className }
}
}
})
if ($matches.Count -ne 1) { throw "Test must match exactly one source in selected modules: $requested (matches: $($matches.Count))" }
$selectedTests += $matches[0]
}
} elseif ($Tests) {
throw 'Tests is only supported in Focused mode.'
}
} catch {
Write-Host "INVALID CHECK: $($_.Exception.Message)"
exit 2
}
$mavenArgs = @('-T', '1', '-DforkCount=1', '-DreuseForks=true', '-f', $pom, '-pl', ($selectedModules -join ','), '-am')
if ($Mode -eq 'Compile') {
$mavenArgs += 'test-compile'
} else {
$mavenArgs += 'test'
}
if ($Mode -eq 'Focused') {
# 上游依赖可能没有所选类,允许该模块零命中;执行后逐类核对本轮报告。
$mavenArgs += "-Dtest=$((@($selectedTests.className | Select-Object -Unique)) -join ',')"
$mavenArgs += '-Dsurefire.failIfNoSpecifiedTests=false'
}
Write-Host "Backend check: $Mode; selected modules: $($selectedModules -join ','); upstream dependencies included"
Write-Host 'Downstream consumers are NOT auto-selected; include affected consumers explicitly.'
Write-Host ("Command: {0} {1}" -f $MavenCommand, ($mavenArgs -join ' '))
if ($DryRun) {
Write-Host 'DRY-RUN PASS: selection validated; no tests or compilation executed.'
exit 0
}
# 整批占用现有重型名额;已受保护的批次直接执行,不嵌套申请锁。
if (-not $env:CHONGCHONG_LOCAL_CHECK_OWNER) {
$childArgs = @('pwsh', '-NoProfile', '-File', $PSCommandPath, '-Mode', $Mode, '-Modules', ($selectedModules -join ','), '-MavenCommand', $MavenCommand)
if ($Mode -eq 'Focused') { $childArgs += @('-Tests', $Tests) }
& (Join-Path $PSScriptRoot 'local-check.ps1') -WorkingDirectory $repoRoot -Label "Backend $Mode check" -Command $childArgs
exit $LASTEXITCODE
}
$startedAt = [DateTime]::UtcNow
Push-Location $repoRoot
try {
& $MavenCommand @mavenArgs
$code = $LASTEXITCODE
if ($code -ne 0) { exit $code }
if ($Mode -eq 'Focused') {
foreach ($selection in $selectedTests) {
$reportDir = Join-Path $repoRoot "$($selection.module)/target/surefire-reports"
$executed = 0
if (Test-Path -LiteralPath $reportDir) {
foreach ($report in Get-ChildItem -LiteralPath $reportDir -File -Filter 'TEST-*.xml') {
if ($report.LastWriteTimeUtc -lt $startedAt) { continue }
[xml]$xml = Get-Content -LiteralPath $report.FullName -Raw
$suite = $xml.testsuite
if ($suite.name -ceq $selection.className -or ([string]$suite.name).StartsWith($selection.className + '$', [StringComparison]::Ordinal)) {
if ([int]$suite.failures -gt 0 -or [int]$suite.errors -gt 0) { throw "Failed test report: $($suite.name)" }
$executed += [int]$suite.tests - [int]$suite.skipped
}
}
}
if ($executed -le 0) { throw "No executed tests in fresh reports for $($selection.className). Missing, stale or all-skipped reports cannot pass." }
Write-Host "Verified $($selection.className): $executed executed tests"
}
}
} finally {
Pop-Location
}
Write-Host "Backend $Mode check PASS (not a full regression or HTTP acceptance)"
exit 0
+82
View File
@@ -0,0 +1,82 @@
import assert from 'node:assert/strict'
import { spawn } from 'node:child_process'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
const launcher = fileURLToPath(new URL('./backend-check.ps1', import.meta.url))
const quote = value => `'${String(value).replaceAll("'", "''")}'`
// 运行真实 PowerShell 参数解析与入口,执行分支使用内存 Maven 替身,不启动 Java。
function run(args, mockExit) {
if (mockExit !== undefined) assert.ok(process.env.CHONGCHONG_LOCAL_CHECK_OWNER, 'Run tests through local-check.ps1')
const mock = mockExit === undefined ? '' : `function global:Mock-Maven { Write-Host ('MOCK_ARGS:' + (ConvertTo-Json -Compress -InputObject @($args))); $global:LASTEXITCODE = ${mockExit} }; `
const parameters = []
for (let i = 0; i < args.length; i++) {
const key = args[i].slice(1)
parameters.push(`${quote(key)}=${key === 'DryRun' ? '$true' : quote(args[++i])}`)
}
if (mockExit !== undefined) parameters.push("MavenCommand='Mock-Maven'")
const command = `$ErrorActionPreference='Stop'; ${mock}$parameters=@{${parameters.join(';')}}; & ${quote(launcher)} @parameters; exit $LASTEXITCODE`
return new Promise((resolve, reject) => {
const child = spawn('pwsh', ['-NoProfile', '-NonInteractive', '-OutputFormat', 'Text', '-EncodedCommand', Buffer.from(command, 'utf16le').toString('base64')])
let output = ''
child.stdout.on('data', chunk => { output += chunk })
child.stderr.on('data', chunk => { output += chunk })
child.on('error', reject)
child.on('close', code => resolve({ code, output }))
})
}
test('module selection is mandatory and rejects inactive modules and injected Maven options', async () => {
for (const args of [[], ['-Modules', 'gateway-overseas'], ['-Modules', 'order,'], ['-Modules', '-DskipTests']]) {
const result = await run([...args, '-DryRun'])
assert.equal(result.code, 2, result.output)
assert.doesNotMatch(result.output, /DRY-RUN PASS/)
}
})
test('default selection runs only requested modules plus upstream dependencies', async () => {
const result = await run(['-Modules', 'order,firstleg,order', '-DryRun'])
assert.equal(result.code, 0, result.output)
assert.match(result.output, /-pl order,firstleg -am test/)
assert.doesNotMatch(result.output, /skipTests|maven.test.skip|backend-full-regression/)
})
test('compile retains compilation of test sources without executing tests', async () => {
const result = await run(['-Mode', 'Compile', '-Modules', 'order'], 0)
assert.equal(result.code, 0, result.output)
assert.match(result.output, /MOCK_ARGS:.*"-pl","order","-am","test-compile"/)
assert.match(result.output, /Compile check PASS/)
})
test('focused selection resolves exact classes and rejects missing or broad selectors', async () => {
const good = await run(['-Mode', 'Focused', '-Modules', 'common-core', '-Tests', 'ApiResponseTest,BoundedRetryPolicyTest', '-DryRun'])
assert.equal(good.code, 0, good.output)
assert.match(good.output, /-Dtest=com.baiyue.common.core.ApiResponseTest,com.baiyue.common.core.BoundedRetryPolicyTest/)
for (const name of ['MissingTest', '*Test', 'ApiResponseTest#method']) {
const result = await run(['-Mode', 'Focused', '-Modules', 'common-core', '-Tests', name, '-DryRun'])
assert.equal(result.code, 2, result.output)
}
})
test('test filter cannot silently apply to compile or module mode', async () => {
for (const mode of ['Compile', 'Module']) {
const result = await run(['-Mode', mode, '-Modules', 'common-core', '-Tests', 'ApiResponseTest', '-DryRun'])
assert.equal(result.code, 2, result.output)
}
const missing = await run(['-Mode', 'Focused', '-Modules', 'common-core', '-DryRun'])
assert.equal(missing.code, 2, missing.output)
})
test('Maven failure is propagated without claiming success', async () => {
const result = await run(['-Modules', 'common-core'], 7)
assert.equal(result.code, 7, result.output)
assert.doesNotMatch(result.output, /check PASS/)
})
test('Maven success without freshly executed selected tests cannot pass', async () => {
const result = await run(['-Mode', 'Focused', '-Modules', 'common-core', '-Tests', 'ApiResponseTest'], 0)
assert.notEqual(result.code, 0, result.output)
assert.match(result.output, /No executed tests in fresh reports/)
assert.doesNotMatch(result.output, /check PASS/)
})