test(fleet): close shared trips and verify resource release

This commit is contained in:
2026-10-03 07:51:17 +08:00
parent 2992871975
commit ef1eef3278
3 changed files with 17 additions and 5 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# 后端脚本管理与全量 HTTP 验收索引
`fleet-cross-customer-test-env.mjs` 是最小两客户共享车次的串行 TEST 专项入口,默认 `--plan` 零网络。显式运行参数为 `--execute --environment TEST --run-id <新批次> --warehouse-id <实际海外收货仓ID> --destination-warehouse-id <FBA目的仓ID>`,凭据仅从显式 `node --env-file="<TEST配置>" --env-file="<Jenkins配置>"` 与 `FBA_CORE_JENKINS_AGENT` 提供。通过普通转运来货、PDA实收和客户确认创建两个新客户各一托一箱,分别预排并关联同车;一单未拣齐时验证全车拒绝及完整库存快照不变,再统一发车、原键重放、逐客户签收。完整库存由只读取证器获取,不写业务SQL,不部署或改共享配置。报告位于 `reports/fleet-cross-<新批次>/`,每次请求发送前记录意图;失败保留原现场,不自动重跑或续跑,已存在批次拒绝覆盖。finally停用本轮PDA并退出独立会话;业务样本保留。前置拒绝不代表事务中途回滚,后者另验。对应入口保护合同为 `fleet-cross-customer-test-env.test.mjs`;内部生成与验收函数见模块README。
`fleet-cross-customer-test-env.mjs` 是最小两客户共享车次的串行 TEST 专项入口,默认 `--plan` 零网络。显式运行参数为 `--execute --environment TEST --run-id <新批次> --warehouse-id <实际海外收货仓ID> --destination-warehouse-id <FBA目的仓ID>`,凭据仅从显式 `node --env-file="<TEST配置>" --env-file="<Jenkins配置>"` 与 `FBA_CORE_JENKINS_AGENT` 提供。通过普通转运来货、PDA实收和客户确认创建两个新客户各一托一箱,分别预排并关联同车;一单未拣齐时验证全车拒绝及完整库存快照不变,再统一发车、原键重放、逐客户签收,最后正式结案并分别释放车辆/司机,核对结案前后库存零变化。完整库存由只读取证器获取,不写业务SQL,不部署或改共享配置。报告位于 `reports/fleet-cross-<新批次>/`,每次请求发送前记录意图;失败保留原现场,不自动重跑或续跑,已存在批次拒绝覆盖。finally停用本轮PDA并退出独立会话;业务样本保留。前置拒绝不代表事务中途回滚,后者另验。对应入口保护合同为 `fleet-cross-customer-test-env.test.mjs`;内部生成与验收函数见模块README。
头程订单提交使用首次原始详情的 `expectedVersion` 与原 `Idempotency-Key`。生产单票的 `SUBMIT_INTENT` 必须先持久化 `submitCommand` 再发送,显式续跑不得使用当前版本替换原命令,缺少原命令的提交意图及后续检查点停止并等待人工核对。源 Excel 造数同样在提交前保存订单条目的 `submitCommand`;中断或失败条目保留原检查点且不自动重跑,不重建订单或换版本继续。
+13 -3
View File
@@ -8,6 +8,7 @@ import { createTestEvidenceReaders } from './fba-core-regression-evidence.mjs'
import { redactEvidence } from './fba-core-regression-test-env.mjs'
import { resolvePickerAccess, createPicker, disablePicker } from './admin-test-env-modules/phase4-overseas-pda.mjs'
import { verifyFleetCrossCustomer } from './admin-test-env-modules/fleet-cross-customer-acceptance.mjs'
import { finishFleetTrip } from './admin-test-env-modules/fleet-trip-fulfillment.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const ok = response => response?.http === 200 && String(response.body?.code) === '200'
@@ -33,12 +34,12 @@ export function parseCrossCustomerArguments(args) {
export function crossCustomerPlan(config) {
return { environment: 'TEST', execute: config.execute, runId: config.runId, warehouseId: config.warehouseId || null, destinationWarehouseId: config.destinationWarehouseId || null,
output: `reports/fleet-cross-${config.runId}`, stages: ['two new customers', 'two ordinary transfer receipts', 'two dispatches on one trip',
'unready customer gate rejection', 'shared departure and replay', 'independent receipts'],
'unready customer gate rejection', 'shared departure and replay', 'independent receipts', 'trip completion and separate resource release'],
limits: 'Prerequisite rejection is not mid-transaction rollback. No deployment, configuration change, SQL writes or automatic rerun. Retain business samples; disable temporary PDA in finally.' }
}
export async function runCrossCustomer(config, { env = process.env, outputRoot = ROOT, readers, prepare, verify = verifyFleetCrossCustomer,
contextFactory = options => new AcceptanceRun(options), pickerAccess = resolvePickerAccess, pickerCreate = createPicker, pickerDisable = disablePicker } = {}) {
finish = finishFleetTrip, contextFactory = options => new AcceptanceRun(options), pickerAccess = resolvePickerAccess, pickerCreate = createPicker, pickerDisable = disablePicker } = {}) {
const plan = crossCustomerPlan(config)
if (!config.execute) return plan
if (config.environment !== 'TEST' || env.CHONGCHONG_ENV !== 'test' || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/.test(config.runId)
@@ -107,7 +108,16 @@ export async function runCrossCustomer(config, { env = process.env, outputRoot =
const fixture = await prepare(context, { execute: true, warehouseId: config.warehouseId, destinationWarehouseId: config.destinationWarehouseId, pdaToken: picker.token, readInventory })
save('fixture.json', fixture)
await verify(context, fixture, { execute: true, readInventory })
report = { success: context.createdResources.fleetCrossCustomer?.status === 'VERIFIED' }
const trip = context.createdResources.fleetTrips?.find(row => row.tripNo === fixture.tripNo)
if (!trip) throw Error('Original fixture trip missing for resource completion')
const beforeClosure = []
for (const member of fixture.members) beforeClosure.push(await readInventory({ marker: context.marker, customerId: member.customerId, warehouseId: config.warehouseId }))
await finish(context, 'fleet-cross-customer-closure', trip)
const afterClosure = []
for (const member of fixture.members) afterClosure.push(await readInventory({ marker: context.marker, customerId: member.customerId, warehouseId: config.warehouseId }))
if (JSON.stringify(beforeClosure) !== JSON.stringify(afterClosure)) throw Error('Trip closure changed customer inventory')
report = { success: context.createdResources.fleetCrossCustomer?.status === 'VERIFIED'
&& trip.closure?.status === 'CARGO_COMPLETED_AND_RESOURCES_RELEASED', closureInventoryUnchanged: true }
} catch (error) {
report = { success: false, error: error instanceof Error ? error.message : 'CROSS_CUSTOMER_FAILED' }
} finally {
@@ -24,11 +24,13 @@ function harness(root, fail = false) {
assert.equal(options.pdaToken, 'private-pda')
await context.request('POST', '/api/test-owned-command', { idempotencyKey: 'stable-command', password: 'private-pda-password' })
if (fail) throw Error('FIXTURE_FAILED')
return { pdaToken: options.pdaToken, marker: context.marker }
return { pdaToken: options.pdaToken, marker: context.marker, tripNo: 'TR-OFFLINE', members: [{ customerId: '42' }, { customerId: '43' }] }
}, verify: async (_context, _fixture, options) => {
await options.readInventory({ marker: context.marker, customerId: '42', warehouseId: '900' })
context.createdResources.fleetCrossCustomer = { status: 'VERIFIED' }
context.createdResources.fleetTrips = [{ tripNo: 'TR-OFFLINE' }]
},
finish: async (_context, _module, trip) => { trip.closure = { status: 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' } },
} }
}
test('plan is network-free and execution requires explicit TEST, unique run and warehouse', async () => {