Merge remote-tracking branch 'origin/test' into dev

# Conflicts:
#	scripts/admin-test-env-modules/phase4-overseas-pda.mjs
#	scripts/agents-index-contract.ps1
This commit is contained in:
2026-09-02 15:21:20 +08:00
33 changed files with 147 additions and 171 deletions
+1
View File
@@ -133,6 +133,7 @@ node scripts/create-pending-review-orders.mjs
| Mapper 注册静态合同 | `node scripts/mapper-scan-contract.mjs`、`node --test scripts/mapper-scan-contract.test.mjs`;自动核对全部持久化模块的 `@Mapper`、服务根包注解过滤扫描和唯一扫描入口,不启动 Spring、不连接数据库;`backend-full-regression.ps1` 在 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-jacoco --require-spotbugs` 检查报告完整性且当前允许失败 |
| 单服务部署失败回滚合同 | `scripts/deploy-built-service-rollback.test.mjs`;模拟成功、健康失败和启动失败,确认仅恢复同服务先前健康镜像且部署任务仍失败 |
| 容器日志单写合同 | `scripts/container-logging-contract.test.mjs`;离线确认应用只写标准输出、Compose 使用有界 `local` 驱动且不挂载应用日志目录 |
Admin/Client HTTP 脚本的离线合同只验证脚本结构、参数和模块边界,不访问测试环境:
@@ -195,12 +195,16 @@ export async function run(context, matrix = context.createdResources.manualState
await verify(context, OCEAN_BASE, status, resourceId)
}
const releasedOcean = matrix.pages.oceanBookings.fixtures.RELEASED
if (!releasedOcean.id || !releasedOcean.warehouseId) {
throw new Error('RELEASED 订舱缺少拖车所需的仓库归属')
}
matrix.pages.truckAppointments = { requiredStatuses: TRUCK_STATUSES, fixtures: {} }
for (const [index, status] of TRUCK_STATUSES.entries()) {
const released = await buildOcean(context, template, 'RELEASED', index + 100)
const releasedOcean = {
id: id(released?.oceanBooking?.id),
warehouseId: id(released?.oceanBooking?.warehouseId),
}
if (!releasedOcean.id || !releasedOcean.warehouseId) {
throw new Error(`拖车状态样本 ${index} 缺少独立 RELEASED 订舱`)
}
const value = await buildTruck(context, releasedOcean, status, index)
const resourceId = id(value?.id)
matrix.pages.truckAppointments.fixtures[status] = { id: resourceId, status: value?.status, detailPath: `/truck-appointments/${resourceId}` }
@@ -43,7 +43,7 @@ async function loginMerchant(context) {
const password = account?.password || process.env.MERCHANT_TEST_PASSWORD || ''
if (!username || !password) throw new Error('缺少客户登录凭据')
const response = await context.request('POST', '/api/auth/login', {
username, password,
loginId: username, password,
}, '')
const token = success(response) ? response.body?.data?.accessToken : null
if (!token) throw new Error(`本轮客户登录失败: requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`)
@@ -134,6 +134,16 @@ function routes(context, merchantToken) {
export async function prepare(context) {
const outerMarker = context.marker
const isolatedResourceKeys = ['phase3Loading', 'phase3LoadingCompletedProducer', 'orchestrationFixtures']
const isolatedResourceSnapshot = new Map(isolatedResourceKeys.map((name) => [name, {
present: Object.hasOwn(context.createdResources, name), value: context.createdResources[name],
}]))
const restoreIsolatedResources = () => {
for (const [name, snapshot] of isolatedResourceSnapshot) {
if (snapshot.present) context.createdResources[name] = snapshot.value
else delete context.createdResources[name]
}
}
const restore = await context.preparePhase3LoadingFixture('-CUSTOMS-MIXED', false, {
primary: 'BUY_DECLARATION', incremental: 'TAX_REFUND',
})
@@ -154,6 +164,7 @@ export async function prepare(context) {
if (!source?.loadingPlanId || !source?.containerBookingId || source?.finalStatus !== 'LOADED') {
context.marker = outerMarker
restore()
restoreIsolatedResources()
throw new Error(`混合柜未进入LOADED: planId=${id(source?.loadingPlanId) || 'missing'}, status=${source?.finalStatus || 'missing'}`)
}
const platform = await context.request('GET', `${PLATFORM}/${encodeURIComponent(id(source.loadingPlanId))}`)
@@ -169,6 +180,7 @@ export async function prepare(context) {
if (!buy.length || !refund.length) {
context.marker = outerMarker
restore()
restoreIsolatedResources()
throw new Error(`正式装柜API未形成混合柜: buy=${buy.length}, tax=${refund.length}`)
}
const merchantToken = await loginMerchant(context)
@@ -208,5 +220,6 @@ export async function prepare(context) {
delete context.context.phase3ContainerCustomsRoutes
context.marker = outerMarker
restore()
restoreIsolatedResources()
}
}
@@ -48,6 +48,8 @@ test('outbound-created query supports automated or manual carrier then reaches P
assert.match(pda, /convertReceivingContainers\(context, producers\)/)
assert.match(pda, /last-leg\/orders\/container-summaries/)
assert.match(pda, /last-leg\/inbound-plans\/convert-container/)
assert.match(pda, /Number\(summary\?\.missingInboundCodeCount\) === 0/)
assert.doesNotMatch(pda, /summary\?\.eligible/)
assert.match(pda, /item\?\.shippingReady === true/)
assert.match(pda, /nonBlank\(item\?\.carrierScheduleQueryId\)/)
assert.match(pda, /targetCorrelated/)
+26 -2
View File
@@ -171,6 +171,7 @@ Assert-Contains $firstlegAgents '/loading-plans/{planId}/lifecycle' 'Firstleg
$orderOpenApi = Read-RequiredText (Join-Path $backendRoot 'order/src/main/resources/openapi/order-v1.yaml')
$firstlegOpenApi = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/resources/openapi/firstleg-v1.yaml')
$orderDeclarationService = Read-RequiredText (Join-Path $backendRoot 'order/src/main/java/com/baiyue/order/service/OrderDeclarationUpdateService.java')
$unifiedOrderQueryService = Read-RequiredText (Join-Path $backendRoot 'order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java')
$firstlegDeclarationService = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/java/com/baiyue/firstleg/service/OrderDeclarationSyncService.java')
$adminDeclarationEditor = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/unified-orders/components/OrderDeclarationEditorDialog.vue')
Assert-Contains $rootAgents '平台修改客户原始申报与履约渠道' '根订单申报快速路由'
@@ -186,6 +187,11 @@ Assert-Contains $orderDeclarationService 'ORDER_DECLARATION_UPDATED_V1' 'Order
Assert-Contains $firstlegDeclarationService 'mapper.byEvent(event.tenantId(), event.eventId())' 'Firstleg 申报 eventId 幂等'
Assert-Contains $firstlegDeclarationService 'event.version() < current.orderVersion()' 'Firstleg 申报版本单调性'
Assert-Contains $adminDeclarationEditor '仓库/PDA 实测数据不会被覆盖' 'Admin 申报事实边界提示'
Assert-Contains $rootAgents '统一列表仍固定显示 `PENDING_REVIEW` 并提供审核动作' '根待审核订单优先状态'
Assert-Contains $orderAgents '统一主状态/下一动作仍固定为 `PENDING_REVIEW/APPROVE_ORDER`' 'Order 待审核优先状态'
Assert-Contains $unifiedOrderQueryService 'order.getStatus() == OrderStatus.SUBMITTED' 'Order 待审核状态判断'
Assert-Contains $unifiedOrderQueryService 'pendingReview ? "PENDING_REVIEW" : summary.mainStatus()' 'Order 待审核主状态优先'
Assert-Contains $unifiedOrderQueryService 'pendingReview ? "APPROVE_ORDER" : summary.nextAction()' 'Order 待审核动作优先'
# 企业联系信息与账号登录标识已经解耦,资质审核不得重新把法人身份证设为必填。
$authAgents = Read-RequiredText (Join-Path $backendRoot 'auth/AGENTS.md')
@@ -209,9 +215,18 @@ Assert-Contains $adminCustomersView '不会修改企业管理员登录手机号'
Assert-Contains $clientCompanyProfile '不会修改任何成员的登录手机号' 'Client 公司联系电话提示'
Assert-Contains $clientMembersView '用于该成员手机号登录;修改后会同步认证账号并退出该成员现有会话' 'Client 成员登录手机号提示'
$fileAgents = Read-RequiredText (Join-Path $backendRoot 'file/AGENTS.md')
$qualificationOpenApi = Read-RequiredText (Join-Path $backendRoot 'file/src/main/resources/openapi/qualification-required-package-v1.yaml')
Assert-Contains $rootAgents '法人身份证选填,可在已有营业执照后单独补传或替换' '根法人身份证单独上传语义'
Assert-Contains $fileAgents '法人身份证选填,客户可在已有营业执照后通过 `/api/merchant/v1/files/qualifications` 单独新增或替换' 'File 法人身份证单独上传入口'
Assert-Contains $qualificationOpenApi '/api/merchant/v1/files/qualifications:' '客户单文件资质 OpenAPI'
Assert-Contains $qualificationOpenApi 'LEGAL_PERSON_ID 可在已有营业执照后单独新增或替换' '客户法人身份证单独上传契约'
Assert-Contains $qualificationOpenApi 'BUSINESS_LICENSE 不允许走此入口' '营业执照整包上传边界'
Assert-Contains $clientCompanyProfile '法人身份证已保存,尚未提交审核' 'Client 法人身份证单独上传反馈'
Assert-NotContains $clientCompanyProfile '当前文件服务尚未开放法人身份证单独上传或替换' 'Client 已移除法人身份证旧阻断'
$truckOpenApi = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/resources/openapi/truck-appointments-v1.yaml')
$loadingListsOpenApi = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/resources/openapi/loading-lists-v1.yaml')
$pdaLoadingApi = Read-RequiredText (Join-Path $workspaceRoot 'apps/pda-uniapp/api/pda-loading.js')
$cabinetEvidenceService = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/java/com/baiyue/firstleg/loading/service/CabinetLoadingEvidenceService.java')
Assert-Contains $rootAgents '拖车预约由 `RELEASED` 订舱拥有,不按客户、柜次、装柜计划或真实柜号归属' '根订舱级拖车语义'
Assert-Contains $rootAgents '证据以 `taskId + warehouseId` 进入任务专用 API' '根柜级证据入口'
Assert-Contains $adminAgents '订舱级拖车预约' 'Admin 拖车快速入口'
@@ -236,6 +251,11 @@ Assert-Contains $adminAgents 'PDA 最终完成前必须补齐' 'Admin 真实柜
Assert-Contains $pdaAgents '当前 `taskId + warehouseId` 走任务专用 API' '国内 PDA 柜级证据入口'
Assert-Contains $pdaLoadingApi '/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}/evidence?warehouseId=' '国内 PDA 证据查询 API'
Assert-NotContains $pdaLoadingApi '/inbounds/${encodeURIComponent(inboundId)}/loading/evidence' '国内 PDA 旧入仓证据 API'
Assert-Contains $rootAgents '上传以幂等键绑定包含文件内容的请求哈希' '根装柜证据上传重放边界'
Assert-Contains $firstlegAgents '完全重放返回当前视图且不得再次上传 File' 'Firstleg 装柜证据重放边界'
Assert-Contains $cabinetEvidenceService 'uploadHash(warehouseId, expectedVersion, evidenceType, sealNo, remark, multipart)' '装柜证据上传请求哈希'
Assert-Contains $cabinetEvidenceService 'digest.update(multipart.getBytes())' '装柜证据文件内容哈希'
Assert-Matches $cabinetEvidenceService '(?s)mapper\.byOperation\(taskId, key\).{0,500}?return view\(scope\);.{0,500}?files\.upload' '装柜证据上传先查重放再调用 File'
$mapperModuleAgents = @(
@{ Module = 'audit-service'; Application = 'BaiyueAuditApplication.java' },
@{ Module = 'auth'; Application = 'BaiyueAuthApplication.java' },
@@ -501,7 +521,11 @@ Assert-Contains $lastlegBusinessOpenApi '/api/platform/v1/last-leg/orders/contai
Assert-Contains $lastlegBusinessOpenApi '/api/platform/v1/last-leg/inbound-plans/convert-container:' 'Lastleg 整柜计划转换契约'
Assert-Contains $lastlegBusinessOpenApi 'sourceContainerBookingId' 'Lastleg 稳定来源柜身份'
Assert-Contains $adminAgents '/orders/container-summaries' 'Admin 柜级摘要目标入口'
Assert-Contains $adminAgents 'allPlansReady' 'Admin 柜级计划就绪事实'
Assert-Contains $rootAgents '只分页返回仍有入仓码待生成有效独立计划的柜' '根柜级计划待处理筛选'
Assert-Contains $backendAgents '分页前排除全部入仓码均已生成有效独立计划的柜' '后端柜级计划待处理筛选'
Assert-Contains $adminAgents '返回仍有待生成计划入仓码的柜' 'Admin 柜级计划待处理筛选'
Assert-Contains $lastlegAgents '分页前排除全部入仓码均已生成有效独立计划的柜' 'Lastleg 柜级计划待处理筛选'
Assert-Contains $lastlegBusinessOpenApi '仅返回仍有入仓码待生成有效独立计划的柜' 'Lastleg 柜级计划待处理 OpenAPI'
Assert-Contains $adminAgents 'ACTIVE + FBA' 'Admin FBA 权威仓候选'
Assert-Contains $adminAgents '/inbound-plans/convert-container' 'Admin 整柜转换目标入口'
Assert-Contains $adminLastMileApi '/orders/container-summaries' 'Admin 柜级摘要真实调用'
@@ -837,5 +861,5 @@ if ($failures.Count -gt 0) {
exit 1
}
Write-Host "AGENTS 索引合同通过:$checked 项检查,覆盖快速表唯一性、部署单元、开发/测试任务登记隔离、前端模块、Client active 路由、脚本目录治理、企业联系信息与登录标识解耦、订单申报版本同步与渠道门禁、业务编号与内部 ID 隔离、装柜清单生命周期与真实柜号后补、订舱级拖车与柜级证据、四/五期范围边界、仓库主档唯一权威、订舱候选与柜量语义、整柜出仓与船节点单调性、Gateway 覆盖、PDA 页面/运行配置/独立更新/语言预留/能力开关、人工分段验收链、Maven 模块、Lastleg 海外安全投影、同步 Trace、关键入口和排除目录。" -ForegroundColor Green
Write-Host "AGENTS 索引合同通过:$checked 项检查,覆盖快速表唯一性、部署单元、开发/测试任务登记隔离、前端模块、Client active 路由、脚本目录治理、企业联系信息与登录标识解耦、选填法人身份证独立上传、待审核订单状态优先、订单申报版本同步与渠道门禁、装柜证据内容哈希重放、业务编号与内部 ID 隔离、装柜清单生命周期与真实柜号后补、订舱级拖车与柜级证据、四/五期范围边界、四期柜级待处理计划筛选与范围漂移、仓库主档唯一权威、订舱候选与柜量语义、整柜出仓与船节点单调性、Gateway 覆盖、PDA 页面/运行配置/独立更新/语言预留/能力开关、人工分段验收链、Maven 模块、Lastleg 海外安全投影、同步 Trace、关键入口和排除目录。" -ForegroundColor Green
exit 0
@@ -0,0 +1,54 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { test } from 'node:test';
const applicationConfigs = [
'auth/src/main/resources/application.yml',
'audit-service/src/main/resources/application.yml',
'event-bridge-service/src/main/resources/application.yml',
'file/src/main/resources/application.yml',
'finance/src/main/resources/application.yml',
'firstleg/src/main/resources/application.yml',
'gateway/src/main/resources/application.yml',
'gateway-overseas/src/main/resources/application.yml',
'iam/src/main/resources/application.yml',
'integration-service/src/main/resources/application.yml',
'lastleg-service/src/main/resources/application.yml',
'order/src/main/resources/application.yml',
'print/src/main/resources/application.yml',
'warehouse/src/main/resources/application.yml',
'warehouse-overseas-service/src/main/resources/application.yml',
'config/nacos/cn/baiyue-common.yml',
'config/nacos/local/baiyue-common.yml',
'config/nacos/overseas/baiyue-common.yml',
];
const composeFiles = [
'deploy/docker-compose.yml',
'deploy/docker-compose.overseas.yml',
];
test('deployable applications log only to stdout and stderr', () => {
for (const path of applicationConfigs) {
const source = readFileSync(path, 'utf8');
assert.doesNotMatch(source, /LOG_PATH|LOGGING_FILE_NAME|LOGGING_ROLLING/, path);
}
});
test('Compose uses one bounded local log stream without application log mounts', () => {
for (const path of composeFiles) {
const source = readFileSync(path, 'utf8');
assert.match(source, /driver:\s+local/, path);
assert.match(source, /DOCKER_LOG_MAX_SIZE/, path);
assert.match(source, /DOCKER_LOG_MAX_FILE/, path);
assert.doesNotMatch(source, /LOG_PATH|LOG_ROOT|\/app\/logs/, path);
}
const domesticCompose = readFileSync(composeFiles[0], 'utf8');
assert.match(domesticCompose, /FILE_UPLOADS_HOST_DIR[^\n]+:\/data\/files/);
});
test('runtime image does not prepare an application log directory', () => {
const dockerfile = readFileSync('deploy/docker/Dockerfile', 'utf8');
assert.doesNotMatch(dockerfile, /\/app\/logs/);
});