test(fleet): verify two-customer departure rollback in TEST
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
|
||||
`fleet-cross-customer-fixture.mjs` 导出 `createFleetCrossCustomerFixture`,由上述专用 TEST 入口显式调用。默认只返回准备契约;执行时重新核对指定实际收货仓和FBA目的仓,通过正式客户资质、普通转运来货、PDA一托一箱测量/绑定/实收、客户确认及预排接口生成两套独立本轮货物,再共用一个真实车次并各自转派送,停在尚未拣货。客户上下文独立且不覆盖外层客户环境,退出本函数创建的客户会话,PDA启停归外层入口;无外接预订循环依赖、业务SQL、部署或自动恢复。`fleet-cross-customer-fixture-contract.test.mjs` 只证明生成编排及拒绝边界,真实运行另行留证。
|
||||
|
||||
`fleet-cross-customer-acceptance.mjs` 是跨客户共享车次的内部后半链验收函数,不是独立入口。默认调用只返回 fixture 契约、零网络;显式 `execute:true` 需要同批次两个独立客户、同仓各一托一箱已实收并分别预排/转派送、关联同一合格车次且未拣货,以及调用方提供的完整库存只读取证器。逐客户审核和拣货,验证第二单未就绪时整车拒绝且全部快照不变,再验证统一发车、原键重放和分别签收。前置门禁证据与事务中途故障回滚严格区分,后者保持 `NOT_TESTED`。失败保留当前样本与原请求,不自动续跑、补造或清理。对应合同 `fleet-cross-customer-acceptance-contract.test.mjs` 仅证明编排与反例断言,不能代替真实运行。临时账号生命周期、最小来货生成及报告保存由外层显式编排负责。
|
||||
`fleet-cross-customer-acceptance.mjs` 是跨客户共享车次的内部后半链验收函数,不是独立入口。默认调用只返回 fixture 契约、零网络;显式 `execute:true` 需要同批次两个独立客户、同仓各一托一箱已实收并分别预排/转派送、关联同一合格车次且未拣货,以及调用方提供的完整库存只读取证器。逐客户审核和拣货,验证第二单未就绪时整车拒绝且全部快照不变,再验证统一发车、原键重放和分别签收。前置门禁与事务中途回滚分别记录;默认后者为 `NOT_TESTED`,只有显式注入`verifyDepartureRollback`且返回精确本轮验证、完整一致性快照恢复相同后才记VERIFIED。专用`../fleet-trip-rollback-test-env.mjs`负责租约/脏读观察与持久化,普通入口不注入故障;钩子异常、未验证或快照漂移必须停止在正常发车前。失败保留当前样本与原请求,不自动续跑、补造或清理。对应合同 `fleet-cross-customer-acceptance-contract.test.mjs` 仅证明编排与反例断言,不能代替真实运行。临时账号生命周期、最小来货生成及报告保存由外层显式编排负责。
|
||||
|
||||
`TERMINAL_READONLY` 用于全部业务写入和结案助手均完成、仅专项入口汇总状态断言错误的已复核报告。结案助手成功状态为 `CARGO_COMPLETED_AND_RESOURCES_RELEASED`。该检查点登录后仅 GET 原车次、每张排车/派送、资源占用测试计划与精确财务来源版本,不重放任何业务写入,不覆盖原失败报告;真实状态、数量、签收或费用不符仍失败。它不补充生产者 ACK 或页面验收证据。
|
||||
|
||||
|
||||
@@ -78,6 +78,29 @@ test('current DTO-shaped two-customer trip rejects unready gate, departs once an
|
||||
assert.ok(m.calls.filter(c=>c.raw.includes('/api/pda/')).every(c=>c.token===m.fixture.pdaToken))
|
||||
assert.ok(!JSON.stringify(proof).includes(m.fixture.pdaToken))
|
||||
})
|
||||
test('explicit rollback verifier runs after both picks and before successful departure',async()=>{
|
||||
const m=model();let calls=0
|
||||
const proof=await verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory,
|
||||
verifyDepartureRollback:async({ready,departPath,departBody})=>{
|
||||
calls++;assert.equal(ready.trip.trip.status,'PLANNED')
|
||||
assert.ok(ready.members.every(row=>row.dispatch.status==='READY_TO_DEPART'))
|
||||
assert.equal(m.calls.filter(call=>call.raw.endsWith('/depart')).length,1)
|
||||
assert.ok(departPath.includes(m.fixture.members[0].dispatchNo));assert.equal(departBody.idempotencyKey,'RUN-CROSS-DEPART')
|
||||
return {verified:true,scope:'OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK'}
|
||||
}})
|
||||
assert.equal(calls,1);assert.equal(proof.midTransactionRollback,'VERIFIED')
|
||||
})
|
||||
for(const failure of ['unverified','throws','mutates'])test(`rollback verification ${failure} prevents successful departure`,async()=>{
|
||||
const m=model()
|
||||
await assert.rejects(verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory,
|
||||
verifyDepartureRollback:async()=>{
|
||||
if(failure==='throws')throw Error('Observed rollback failed')
|
||||
if(failure==='mutates')m.state[0].stock.balances[0].availablePackages='-1'
|
||||
return {verified:failure!=='unverified',scope:'OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK'}
|
||||
}}))
|
||||
assert.equal(m.calls.filter(call=>call.raw.endsWith('/depart')).length,1)
|
||||
assert.ok(m.state.every(row=>row.delivery.order.status==='WAIT_DELIVERY'))
|
||||
})
|
||||
for(const fault of ['wrong stock scope','missing PDA position','duplicate ship','gate mutates second customer','gate reorders route','outage disguised as rejection','unrelated gate rejection','partial departure','depart deducts again','signs both']) {
|
||||
test(`fails closed without widening the claimed evidence: ${fault}`,async()=>{
|
||||
const m=model(fault);await assert.rejects(verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory}),
|
||||
|
||||
@@ -33,7 +33,7 @@ export const fixtureContract = Object.freeze({
|
||||
/** Import/default call is network-free. Explicit execute:true requires separately authorized TEST execution.
|
||||
* Tokens stay in the caller, never in the persisted proof. Failure retains fixture and exact request keys.
|
||||
*/
|
||||
export async function verifyFleetCrossCustomer(context, fixture, { execute = false, readInventory } = {}) {
|
||||
export async function verifyFleetCrossCustomer(context, fixture, { execute = false, readInventory, verifyDepartureRollback } = {}) {
|
||||
if (!execute) return fixtureContract
|
||||
if (!context?.marker || fixture?.marker !== context.marker || fixture.stage !== fixtureContract.stage
|
||||
|| !fixture.tripNo || !fixture.warehouseId || !fixture.pdaToken || typeof readInventory !== 'function'
|
||||
@@ -159,6 +159,13 @@ export async function verifyFleetCrossCustomer(context, fixture, { execute = fal
|
||||
const ready=await snapshot('READY')
|
||||
const departBody={recordVersion:reviewed.trip.recordVersion,idempotencyKey:key('DEPART'),remark:context.marker}
|
||||
const departPath=`${PDA}/${encode(fixture.members[0].dispatchNo)}/depart`
|
||||
if(verifyDepartureRollback!==undefined) {
|
||||
check('Explicit rollback verifier is callable',typeof verifyDepartureRollback==='function')
|
||||
const rollback=await verifyDepartureRollback({context,fixture,ready,departPath,departBody,snapshot})
|
||||
check('Mid-transaction rollback has exact owned verification',rollback?.verified===true && rollback.scope==='OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK')
|
||||
equal(ready,await snapshot('ROLLBACK_RESTORED'),'Mid-transaction rollback restores both complete customer execution and inventory snapshots')
|
||||
proof.rollbackEvidence=rollback;proof.midTransactionRollback='VERIFIED'
|
||||
}
|
||||
const departed=await request('POST',departPath,departBody,'Depart whole trip once',true)
|
||||
check('Departure includes both customer dispatches',departed.tripNo===fixture.tripNo && departed.status==='IN_TRANSIT' && integer(departed.shippedPackages)===2
|
||||
&& canonical(departed.dispatches.map(row=>row.dispatchNo).sort())===canonical(fixture.members.map(row=>row.dispatchNo).sort()))
|
||||
|
||||
Reference in New Issue
Block a user