feat(test): continue reviewed return source sample with pre-signature audit

This commit is contained in:
2026-10-03 22:13:15 +08:00
parent 6dbc899626
commit 0c8ede3889
5 changed files with 346 additions and 17 deletions
+85 -2
View File
@@ -50,6 +50,88 @@ function createdVehicleRecoveryFixture() {
return { ...f, snapshot, request }
}
function returnSourceRecoveryFixture() {
const f = afterCostsRecoveryFixture(), { report, trip, responses, base, ctx } = f, run = report.createdResources.fleetAcceptance
run.stage = 'RETURN_REDISPATCH'; run.receipts = []; run.departure.shippedPackages = 5
report.steps = [{ name: '模块执行', passed: false, evidence: 'Fresh redispatch fixture requires source A with at least 3 boxes and a separate domestic source B' }]
const current = { ...responses.get(base).trip, recordVersion: '30', vehicleId: 'V2', driverId: 'D2', vehiclePlate: 'NEW', driverName: 'NEW', driverPhone: 'NEW' }
responses.get(base).trip = current
const original = trip.costAcceptance.departure
original.snapshot.confirmedLoad.cargo = [{ dispatchNo: 'OWN', dispatchItemId: 'IA', warehousePalletId: 'PA', packages: 3 },
{ dispatchNo: 'OWN', dispatchItemId: 'IB', warehousePalletId: 'PB', packages: 1 }, { dispatchNo: 'EXT', dispatchItemId: 'IX', warehousePalletId: 'PX', packages: 1 }]
const history = { records: [structuredClone(original), { action: 'HANDOVER', recordVersion: '28' },
{ action: 'RELEASE', recordVersion: '29' }, { action: 'RELEASE', recordVersion: '30' }], total: 4 }
responses.set(base + '/history?page=1&size=100', history)
const handover = { records: [{ handoverNo: 'HO-CURRENT', recordVersion: '2' }], total: 1 }
const vehicle = { id: 'V2', recordVersion: '0', plateNumber: 'NEW', status: 'IN_SERVICE' }
const driver = { id: 'D2', recordVersion: '2', active: true, reviewStatus: 'APPROVED', licenseFileId: 'FILE', details: {} }
trip.handover = { status: 'VERIFIED', finalTrip: structuredClone(current), handoverNo: 'HO-CURRENT', requests: [
{ method: 'GET', path: base + '/history?page=1&size=100', result: structuredClone(history) },
{ method: 'GET', path: base + '/handovers?page=1&size=10', result: structuredClone(handover) },
{ method: 'POST', path: '/api/platform/v1/last-leg/master-data/vehicles', result: structuredClone(vehicle) },
{ method: 'POST', path: '/api/platform/v1/last-leg/fleet/drivers/D2/review', result: structuredClone(driver) }] }
responses.set(base + '/handovers', handover); responses.set('/api/platform/v1/last-leg/master-data/vehicles/V2', vehicle)
responses.set('/api/platform/v1/last-leg/fleet/drivers/D2', driver)
ctx.context.readRecoveryTripResources = async () => [{ resourceKind: 'VEHICLE', resourceId: 'V2', tripNo: 'TR-OWN' }, { resourceKind: 'DRIVER', resourceId: 'D2', tripNo: 'TR-OWN' }]
const rows = [['A', 3, 'OWN'], ['B', 1, 'OWN'], ['EXT', 1, 'EXT']]
const originals = rows.map(([label, packages, dispatchNo]) => ({ deliveryNo: 'DL-' + label, outboundNo: 'OUT-' + label, dispatchNo,
customerId: 'C1', overseasWarehouseId: 'W1', status: 'WAIT_DELIVERY', version: '2', packageCount: packages,
documentSummary: { categories: [{ fileCount: 0 }] } }))
trip.returnRedispatch = { marker: 'RUN', status: 'FAILED', failure: report.steps[0].evidence, originalDeliveries: originals,
requests: originals.map(order => ({ method: 'PUT', path: `/api/platform/v1/last-leg/delivery-orders/${order.deliveryNo}/status?customerId=C1`,
body: { status: 'DELIVERING', expectedVersion: 2 }, status: 'RESPONDED', http: 200, code: '200', requestId: 'r', traceId: 't',
result: { deliveryNo: order.deliveryNo, deliveryStatus: 'DELIVERING', replayed: false } })) }
report.createdResources.fleetFeeCoverageCases[0].proof.deliveryNos = ['DL-A', 'DL-B']
for (const [label, packages, dispatchNo] of rows) {
const order = originals.find(row => row.deliveryNo === 'DL-' + label)
responses.set(`/api/platform/v1/last-leg/delivery-orders/${order.deliveryNo}?customerId=C1`, { order: { ...structuredClone(order),
status: 'DELIVERING', version: '3', vehiclePlate: 'NEW', driverName: 'NEW', driverPhone: 'NEW' } })
const relation = { lastMileOrderId: label, inboundCode: 'IN-' + label, deliveryNo: order.deliveryNo, outboundNo: order.outboundNo,
returnSources: [{ maxReturnPackages: packages }] }
responses.set(`/api/platform/v1/last-leg/delivery-orders/${order.deliveryNo}/return-inbound-options?customerId=C1`, [relation])
responses.set(`/api/platform/v1/last-leg/orders/${label}?customerId=C1`, { order: { id: label, customerId: 'C1', overseasWarehouseId: 'W1',
orderType: label === 'EXT' ? 'WAREHOUSE_TRANSFER' : 'FIRST_LEG', sourceInboundId: 'SOURCE-' + label, inboundCode: relation.inboundCode, packageCount: packages } })
}
for (const no of ['OWN', 'EXT']) {
const cargo = original.snapshot.confirmedLoad.cargo.filter(row => row.dispatchNo === no), total = cargo.reduce((n,row)=>n+row.packages,0)
responses.set('/api/platform/v1/last-leg/dispatches/' + no, { dispatchNo: no, status: 'DEPARTED', overseasWarehouseId: 'W1',
packageCount: total, pickedPackageCount: total, shippedPackageCount: total,
items: cargo.map(row=>({id:row.dispatchItemId,warehousePalletId:row.warehousePalletId,packageCount:row.packages})) })
}
return f
}
test('source-selection recovery only accepts original three explicit starts and rejects any actual return or unsigned result', () => {
const f = returnSourceRecoveryFixture()
assert.equal(reviewedFullCheckpoint(f.report, 'RETURN_SOURCE_SELECTION_BEFORE_RETURN').returnSourceSelection, true)
for (const change of [x=>x.trip.returnRedispatch.requests.push({ method:'POST',path:'/mark-return' }),
x=>x.trip.returnRedispatch.requests[0].status='UNKNOWN_RESULT',x=>x.trip.returnRedispatch.requests[0].result.replayed=true]) {
const mutated = returnSourceRecoveryFixture(); change(mutated)
assert.throws(()=>reviewedFullCheckpoint(mutated.report,'RETURN_SOURCE_SELECTION_BEFORE_RETURN'), /source recovery/)
}
})
for (const fault of ['none','candidate-empty','delivery-waiting','resource-wrong','cost-changed','handover-changed'])
test(`source recovery validates current business facts and repaired candidates before PDA write: ${fault}`,async()=>{
const f=returnSourceRecoveryFixture()
if(fault==='candidate-empty')f.responses.get('/api/platform/v1/last-leg/delivery-orders/DL-A/return-inbound-options?customerId=C1')[0].returnSources=[]
if(fault==='delivery-waiting')f.responses.get('/api/platform/v1/last-leg/delivery-orders/DL-A?customerId=C1').order.status='WAIT_DELIVERY'
if(fault==='resource-wrong')f.ctx.context.readRecoveryTripResources=async()=>[]
if(fault==='cost-changed')f.responses.get(f.base+'/costs?page=1&size=100').summary.totals[0].directCost='99.00'
if(fault==='handover-changed')f.responses.get(f.base+'/handovers').records[0].recordVersion='3'
if(fault==='none'){await verifyReturnSourceRecovery(f.ctx);assert.equal(f.ctx.createdResources.fleetDepartureRecoveryVerification.returnSourceSelection.a.tail.packageCount,3)}
else await assert.rejects(continueReviewedReturnSources(f.ctx,{makePicker:()=>assert.fail('no PDA write before verification')}))
})
test('source recovery preserves original costs/handover and invokes only actual return continuation and closure',async()=>{
const f=returnSourceRecoveryFixture(), operations=[], costs=f.trip.costAcceptance,hand=f.trip.handover
f.ctx.context.saveFleetReturnRedispatchFixture=()=>operations.push('save-return-proof')
await continueReviewedReturnSources(f.ctx,{resolveAccess:async()=>({}),makePicker:async()=>({token:'new-pda',membershipId:'NEW'}),closePicker:async()=>operations.push('close-pda'),
redispatch:async(_ctx,_module,trip,input)=>{assert.equal(input.sourceSelectionRecovery,true);assert.equal(trip.costAcceptance,costs);assert.equal(trip.handover,hand);operations.push('return-candidates');return {originalSignatures:[{deliveryNo:'DL-A',signedAt:'signed'}]}},
closeOriginal:async()=>{operations.push('close-original');return {status:'CARGO_COMPLETED_AND_RESOURCES_RELEASED'}},cleanup:async()=>operations.push('cleanup')})
assert.deepEqual(operations,['return-candidates','save-return-proof','close-original','cleanup','close-pda'])
})
test('created-vehicle checkpoint requires exact successful uppercase response and no subsequent writes', () => {
const { report } = createdVehicleRecoveryFixture()
const accepted = reviewedFullCheckpoint(report, 'DEPARTED_VEHICLE_CREATED')
@@ -201,7 +283,7 @@ test('changed source report SHA blocks recovery before creating context or loggi
rmSync(temporary, { recursive: true, force: true })
}
})
import { parseFleetArguments, fleetExecutionPlan, runFleetAcceptance, reviewedFullCheckpoint, verifyFleetTerminalState, runFleetHandoverStage, fleetCostShares, prepareFleetCostActors, findFleetActorPhone, guardFleetWrites, redactFleetWrite, verifyDepartedRecovery, continueReviewedDeparture, completeDepartedFleetScenario, readReviewedVehicleOccupation } from './fleet-test-env-acceptance.mjs'
import { parseFleetArguments, fleetExecutionPlan, runFleetAcceptance, reviewedFullCheckpoint, verifyFleetTerminalState, runFleetHandoverStage, fleetCostShares, prepareFleetCostActors, findFleetActorPhone, guardFleetWrites, redactFleetWrite, verifyDepartedRecovery, continueReviewedDeparture, completeDepartedFleetScenario, readReviewedVehicleOccupation, verifyReturnSourceRecovery, continueReviewedReturnSources } from './fleet-test-env-acceptance.mjs'
for (const response of [
{ http: 0 }, { http: 503, body: { code: 'A0500' }, requestId: 'r', traceId: 't' },
@@ -500,7 +582,8 @@ test('phone lookup failure stops without any user creation or guessing', async (
})
test('main chain performs handover after actual DEPART and before receipts, and cleanup follows current-resource closure', async () => {
const source = readFileSync(new URL('./fleet-test-env-acceptance.mjs', import.meta.url), 'utf8')
const fullSource = readFileSync(new URL('./fleet-test-env-acceptance.mjs', import.meta.url), 'utf8')
const source = fullSource.slice(fullSource.indexOf('export async function runFleetScenario'))
const savedDeparture = source.indexOf('proof.departure = departed'), handover = source.indexOf("stage('IN_TRANSIT_HANDOVER')")
const receipt = source.indexOf("stage('RECEIPTS_AND_RESOURCE_RELEASE')"), closure = source.indexOf('proof.closure = await closeTrip')
const cleanup = source.indexOf('await cleanup(context, MODULE, trip)')