This commit is contained in:
2026-10-20 14:22:53 +08:00
parent 19745e46c1
commit bf2508b6d6
24 changed files with 1222 additions and 88 deletions
+39
View File
@@ -273,7 +273,13 @@ export async function run(context) {
| 订舱与拖车服务中心跨链路 | `service-center-flow.mjs`(依赖 `ocean-bookings,truck-appointments`;复用本轮 `RELEASED` 订舱创建订舱级拖车,验证派车单、离仓前状态链、历史、审计和关联零漂移) | 由入口依赖合同和拖车直接合同共同覆盖;真实 HTTP 缺本轮订舱或承运商时 fail-closed |
| Admin 补充能力与保护边界 | `supplemental.mjs`、`supplemental-boundaries.mjs` | `supplemental-boundaries-contract.test.mjs`;只按本轮资源验证补充能力、负向和保护边界 |
| 装柜清单三备注 | `orders.mjs`、`phase3-loading.mjs`(客户备注、最新内部备注、仓库备注;仓库备注 PATCH/清空/精确重放/旧版本零写) | `loading-list-remarks-contract.test.mjs` |
| 预约工作日历与工作事项 | `appointment-work.mjs` | `appointment-work-contract.test.mjs`;默认只读核对月历、汇总、分页、详情与不存在事项拒绝,正向命令待本轮可编辑工作项接入 |
| PDA 版本发布 | `pda-releases.mjs` | `pda-audit-sailing-contract.test.mjs`;只读双端发布列表与空上传/不存在版本拒绝,真实 APK/WGT 发布需人工准备安装包 |
| 平台操作审计 | `audit.mjs` | `pda-audit-sailing-contract.test.mjs`;分页查询、列表不返回快照、详情与不存在日志拒绝 |
| 船期日历 | `sailing-calendar.mjs` | `pda-audit-sailing-contract.test.mjs`;月历、渠道、模板与异常分配只读核对 |
| 首页实时指标 | `dashboard.mjs` | `dashboard-contract.test.mjs`;默认只读核对实时柜量/清关指标、今日新运单及启用客户的服务端分页总数 |
| 自营经营分析 | `self-operated-analytics.mjs` | `fleet-analytics-contract.test.mjs`;空参探测看板/货主目录,缺采集账号上下文时记录非阻断缺口,不虚构指标 |
| 车队车次与出车记录 | `fleet-management.mjs` | `fleet-analytics-contract.test.mjs`;只读核对车次列表/详情、出车记录、车队报价与拼车需求,车次写命令待本轮排车资源接入 |
| 消息中心管理 | `notifications.mjs` | `notifications-contract.test.mjs` |
| Finance 真实前置、对账、结算与下载中心 | `finance-full-fixtures.mjs`、`finance-reconciliation-full.mjs`、`finance-settlement-full.mjs`、`finance-downloads.mjs` | `finance-full-fixtures-contract.test.mjs`、`finance-reconciliation-full-contract.test.mjs`、`finance-settlement-full-contract.test.mjs`、`finance-downloads-contract.test.mjs` |
| Finance 扩展隔离与跨区费用 | `firstleg-container-fee-allocation.mjs`(柜次跨客户分摊)、`finance-direct-paid-isolation.mjs`(海外仓服务直接已付)、`phase4-overseas-finance-cross-region.mjs`(五类海外费用跨区进入 Finance) | 对应同名 `*-contract.test.mjs`;跨区事件持久化链在本轮获授权时另以只读数据库证据核对 |
@@ -442,4 +448,37 @@ node --test scripts/admin-test-env-modules/merchant-qualification-fixture-contra
下载中心导出共用内部助手:`download-center.mjs` 的 `verifyDownloadCenterExport` 由尾程订单、海外库存及主数据模块调用,创建 File XLSX 任务、等待终态、下载并核对字段及本轮业务数据;环境和写入授权沿所属模块。离线 `download-center-fixture.mjs` 仅生成合同测试使用的 XLSX 字节;`download-center-contract.test.mjs` 校验失败不继续下载及文件内容验证,不访问环境。
## 新增模块的授权后定向运行清单
以下模块已注册 `CONFIGURED_MODULES`,离线合同通过;真实 HTTP 仍需当前轮测试环境授权和对应业务前置。默认全量已包含它们,定向排查时按模块名运行,依赖仍由总入口展开。
| 模块 | 定向命令 | 关键前置 | 报告校验点 |
|---|---|---|---|
| `appointment-work` | `--modules phase4-carrier-pickup-ready,appointment-work` | 本轮柜、船节点或排车产生工作事项 | 日历/汇总/分页/详情结构;不存在事项命令拒绝 |
| `pda-releases` | `--modules pda-releases` | 平台 `platform:pda-release:list` | 双端发布列表结构;空上传与不存在版本拒绝 |
| `audit` | `--modules audit` | 平台 `platform:audit:list`,且本轮已有业务写入 | 分页、列表不返回快照、详情与不存在日志拒绝 |
| `sailing-calendar` | `--modules sailing-calendar` | 当月已有订舱或模板生成的船期 | 月历、渠道、模板、异常分配;无船期时记录非阻断缺口 |
| `self-operated-analytics` | `--modules self-operated-analytics` | 采集账号与仓库上下文 | 空参不可用时记录非阻断缺口,不虚构指标 |
| `fleet-management` | `--modules phase4-overseas-master-data,fleet-management` | 本轮车次或排车样本 | 车次、出车记录、报价、拼车需求只读;无车次时记录缺口 |
Client 全量为单文件,新增的客户账单、海外仓自助、跨境采购/卡车/散板、入库与派送订单、船期价格和报关待办随默认全量执行,不能按模块定向。
上述模块与 Client 新增端点已按当前 Controller 静态核对存在,包括 `/api/merchant/v1/customs/pending-summary`、`/orders/{orderId}`、`/first-leg/pricing/channels`、`/api/merchant/v1/warehouses`、`/inbound-plans`、`/delivery-orders`、`/api/platform/v1/files/pda-releases` 和 `fleet/operations|quotes|capacity-offers`;真实响应结构仍以 HTTP 验收为准。所需平台权限(`platform:appointment:list`、`platform:pda-release:list`、`platform:audit:list`、`platform:firstleg:sailing-calendar:list`、`platform:self-operated-analytics:list`、`platform:fleet:*:list`)已在 IAM 与服务权限种子中定义并授予 `PLATFORM_ADMIN`,HTTP 403 不应被误判为脚本失败。
已知待验收缺口(不计作已覆盖通过):
- `phase4-overseas-dispatch` 要求两个 FBA 目的地及同目的地双实托;默认主链当前只产生单目的地(9/23 证据 `destinations=ABE4`),前置不足按 `notRun` 阻断,报告中 `dispatchStatus=NOT_RUN`,并连带 `phase5-overseas-fulfillment` 阻断。补齐主链多目的仓样本后重跑。
- `appointment-work` 写命令(`SAVE_FOLLOWUP`、`CONFIRM_ROUTE`、`SAVE_REQUIREMENT`)需要本轮可编辑工作项与操作人 ID。
- `self-operated-analytics` 数据采集任务与重试需要领星账号和真实采集批次。
- `fleet-management` 车次写命令需要本轮排车、车辆与司机资源。
- Client 海外仓贴标、仓储转运、货物废弃和退货换标的创建链路需要平台海外 PDA 链产出的库存;当前只覆盖客户范围列表和退货预报写链路。
- `pda-releases` 真实 APK/WGT 上传、发布和停发需人工准备有效安装包。
## 本地离线合同运行边界
- 必须在 `baiyue-firstleg` 根运行批量合同:`auth-login-payload` 等合同按 `scripts/...` 相对路径读取,从工作区根运行会产生 `ENOENT` 假失败。
- `appointment-work-test-env`、`agents-index-registry`、`ci-maven-resource`、`deploy-built-service-rollback` 会 `spawn` 子进程;受限沙箱下 `EPERM` 属于运行环境限制,不是合同失败。
- `database-schema-diff`、`prod-release-*`、`fleet-*-test-env` 等需要数据库或测试环境的合同不进入本地批量扫描;按各自 README 单独授权执行。
- 当前 134 个模块合同中有 4 个 `fleet-*` 合同失败(`fleet-cross-customer-fixture`、`fleet-external-dispatch-acceptance`、`fleet-fee-coverage-acceptance`、`fleet-return-redispatch-acceptance`),失败点在 fleet 业务模拟断言,与脚本新增模块和本次 `phase4-overseas-dispatch`/`finance-downloads` 修改无关,另行处理。
采购费用不再使用通用费用草稿、审核和发布链。`phase4-overseas-finance-cross-region.mjs` 对采购改为验证通用创建被拒绝,其他四类费用继续验证既有投递链;采购成功来源由完整样品流程核对实扣与来源关联。
@@ -65,3 +65,9 @@ test('未部署时能力探测后记录部署前置,不借历史业务数据',
assert.match(gaps[0][3], /部署后重跑完整链/)
assert.equal(steps.some((step) => step.name === '读取客户账户筛选事实'), false)
})
test('同参导出只合并进行中任务,已完成任务允许按同参重新导出', () => {
assert.match(source, /60 秒同参请求合并/)
assert.match(source, /String\(originalStatus\) === 'SUCCEEDED'/)
assert.match(source, /File 只合并 PENDING\/RUNNING 的同参任务/)
})
@@ -127,8 +127,13 @@ async function createTask(context, type, parameters, suffix, expectedHeaders, ex
const duplicateResponse = await context.request('POST', '/api/platform/v1/files/export-tasks', body,
context.token, { 'Idempotency-Key': idempotencyKey })
const duplicate = context.apiSuccess(MODULE, `${type} 重复请求`, duplicateResponse, '', OPTIONS)
context.assert(MODULE, `${type} 60 秒同参请求合并`, duplicateResponse, id(duplicate?.id) === taskId,
`original=${taskId}, duplicate=${id(duplicate?.id) || 'missing'}`, OPTIONS)
const originalRead = await context.request('GET', `/api/platform/v1/files/export-tasks/${encodeURIComponent(taskId)}`)
const originalStatus = originalRead.body?.data?.status
context.assert(MODULE, `${type} 60 秒同参请求合并`, duplicateResponse,
// File 只合并 PENDING/RUNNING 的同参任务;已完成任务允许按同参重新导出。
id(duplicate?.id) === taskId || String(originalStatus) === 'SUCCEEDED',
`original=${taskId}, originalStatus=${originalStatus || 'missing'}, duplicate=${id(duplicate?.id) || 'missing'}`,
OPTIONS)
const listResponse = await context.request('GET', `/api/platform/v1/files/export-tasks?page=1&size=100&exportType=${type}`)
const page = context.apiSuccess(MODULE, `${type} 下载中心列表可见`, listResponse, '', OPTIONS)
@@ -101,6 +101,8 @@ export function markerCandidates(context, candidates) {
}
export function notRun(context, proof, reason) {
proof.status = 'NOT_RUN'; proof.reason = reason
// 未运行时保留既有终态;否则显式标记 NOT_RUN,便于下游区分前置不足与代码失败。
proof.finalStatus ??= 'NOT_RUN'
context.addCoverageGap?.('phase4-overseas-dispatch', 'native-dispatch', '缺少当前生产前置', reason, 'fixture', { deploymentBlocker: true })
return proof
}
@@ -706,7 +706,7 @@ test('container overview always follows phase3 loading in default and custom mod
const entry = source('../admin-test-env-acceptance.mjs')
assert.match(entry, /const DEFAULT_MODULES = normalizeModuleOrder\(CONFIGURED_MODULES\)/)
assert.match(entry, /const modules = normalizeModuleOrder\(/)
assert.match(entry, /return \{[\s\S]{0,500}modules,/)
assert.match(entry, /return \{[\s\S]{0,1200}modules,/)
})
test('overview does not fall back to a container fixture when loading evidence was requested', () => {
@@ -311,3 +311,11 @@ test('platform scope zero completes the same detail and manual-node lifecycle as
assert.ok(fixture.assertions.every((check) => check.passed))
assert.ok(fixture.steps.every((check) => check.passed))
})
test('还柜终态拆分为节点覆盖与停止语义,人工节点下限扣除出仓自动节点和拖车回写节点', () => {
assert.match(source, /拖车业务回写后覆盖十标准实际节点/)
assert.match(source, /还柜终态停止语义与本轮货柜关联/)
assert.match(source, /STANDARD_NODES\.length - 2/)
assert.match(source, /stopReason=\$\{finalTask\.stopReason/)
assert.doesNotMatch(source, /覆盖十标准实际节点且EMPTY_RETURNED停止/)
})
@@ -348,16 +348,23 @@ export async function run(context) {
// PICKED_UP; the immutable carrier history therefore contains the eight
// pre-pickup nodes plus EMPTY_RETURNED.
const carrierNodesWithoutInferredPickup = STANDARD_NODES.filter((node) => node !== 'PICKED_UP')
context.assert(MODULE, '拖车业务回写后覆盖十标准实际节点且EMPTY_RETURNED停止', detailResponse,
carrierNodesWithoutInferredPickup.every((node) => actualNodes.has(node)) && finalTask.status === 'STOPPED'
&& finalTask.latestActualNode === 'EMPTY_RETURNED' && finalTask.stopReason === '已还柜'
context.assert(MODULE, '拖车业务回写后覆盖十标准实际节点', detailResponse,
carrierNodesWithoutInferredPickup.every((node) => actualNodes.has(node)),
`actualNodes=${[...actualNodes].join(',')}, missing=${carrierNodesWithoutInferredPickup.filter((node) => !actualNodes.has(node)).join(',')}`,
OPTIONS)
context.assert(MODULE, '还柜终态停止语义与本轮货柜关联', detailResponse,
finalTask.status === 'STOPPED' && finalTask.latestActualNode === 'EMPTY_RETURNED'
&& finalTask.stopReason === '已还柜'
&& id(finalTask.containerBookingId) === id(drayage.containerBookingId),
`actualNodes=${[...actualNodes].join(',')}, status=${finalTask.status}`, OPTIONS)
`status=${finalTask.status}, latest=${finalTask.latestActualNode || 'none'}, `
+ `stopReason=${finalTask.stopReason || 'none'}, taskBooking=${id(finalTask.containerBookingId)}, `
+ `drayageBooking=${id(drayage.containerBookingId)}`, OPTIONS)
const historyResponse = await context.request('GET', `${BASE}/${queryId}/history`)
const history = context.apiSuccess(MODULE, '查询本轮任务不可变历史', historyResponse, `queryId=${queryId}`, OPTIONS)
context.assert(MODULE, '历史包含码头拖车回写节点且结构完整', historyResponse,
Array.isArray(history) && history.every(validHistory)
&& history.filter((item) => item.action === 'MANUAL_NODE').length >= STANDARD_NODES.length - 1,
// 首个标准节点由国内出仓自动写入,PICKED_UP 由码头拖车业务回写,人工补录最多 8 条。
&& history.filter((item) => item.action === 'MANUAL_NODE').length >= STANDARD_NODES.length - 2,
`history=${Array.isArray(history) ? history.length : 'missing'}`, OPTIONS)
}