2026-10-04 10:32:26 +08:00
import test from 'node:test'
import assert from 'node:assert/strict'
2026-10-20 10:35:52 +08:00
import fs from 'node:fs'
import { tmpdir } from 'node:os'
import { join , resolve } from 'node:path'
import { validateRollbackRequest , validateRollbackScope , resolveRollbackScope , rollbackSql , buildRollbackRemoteScript ,
fullRollbackSnapshot , assertRollbackBaseline , assertTargetTransferUpdate , assertMidTransactionObservation , assertRestoredSnapshot ,
runTripRollback , requestRollbackDeparture , assertDatabaseLockConflict , createDepartureRollbackVerifier } from './fleet-trip-rollback-test-env.mjs'
const sqlSource = fs . readFileSync ( new URL ( '../lastleg-service/src/main/resources/db/lastleg.sql' , import . meta . url ), 'utf8' )
// Read actual CREATE columns (including multiple columns on a line), respecting quoted comments and nested type parentheses.
function ddlColumns ( table ) {
const body = sqlSource . match ( new RegExp ( `CREATE TABLE(?: IF NOT EXISTS)? ${ table } \\(([\\s\\S]*?)\\) ENGINE=` , 'i' )) ? .[ 1 ]
assert . ok ( body , `actual schema table ${ table } ` )
const parts = []; let start = 0 , depth = 0 , quoted = false
for ( let i = 0 ; i < body . length ; i ++ ) {
if ( body [ i ] === "'" && body [ i - 1 ] !== '\\' ) quoted = ! quoted
if ( ! quoted ) {
if ( body [ i ] === '(' ) depth ++
if ( body [ i ] === ')' ) depth --
if ( body [ i ] === ',' && depth === 0 ) { parts . push ( body . slice ( start , i )); start = i + 1 }
}
}
parts . push ( body . slice ( start ))
const primary = body . match ( /PRIMARY KEY\s*\(([^)]+)\)/i )[ 1 ]. split ( ',' ). map ( c => c . trim ())
return parts . map ( part => part . trim (). match ( /^([a-z][a-z0-9_]*)\s+((?:BIGINT|INT|VARCHAR|CHAR|TEXT|LONGTEXT|MEDIUMTEXT|DATETIME|TIMESTAMP|DATE|TIME|JSON|DECIMAL|TINYINT|SMALLINT|BOOLEAN|BIT|DOUBLE|FLOAT)(?:\([^)]*\))?)/i ))
. filter ( Boolean ). map (( match , i ) => ({ collection : 'column' , table , column : match [ 1 ], ordinal : i + 1 , type : match [ 2 ]. toLowerCase (), key : primary . includes ( match [ 1 ]) ? 'PRI' : '' }))
}
const tableNames = [ 'll_dispatch_order' , 'll_dispatch_history' , 'll_dispatch_item' , 'll_dispatch_command' , 'll_dispatch_pallet_lock' , 'll_dispatch_physical_pallet_lock' , 'll_physical_pallet_guard' , 'll_dispatch_item_source' ,
'll_fleet_trip' , 'll_fleet_trip_dispatch' , 'll_fleet_trip_history' , 'll_fleet_pool_order_link' , 'll_outbound_order' , 'll_delivery_order' ,
'll_order' , 'll_loading_order_status_history' , 'll_warehouse_transfer_order' , 'll_dispatch_pda_task' , 'll_dispatch_pda_pick' ,
'll_dispatch_pda_pick_order' , 'll_inventory_balance' , 'll_inventory_position' , 'll_inventory_ledger' ]
const columns = [... tableNames ]. sort (). flatMap ( ddlColumns )
const request = { marker : 'OWN_RUN' , tripNo : 'TR-OWN' , dispatchNo : 'DS-OWN' , warehouseId : '51' , commandKey : 'ORIGINAL-DEPART' , commandVersion : '11' , expectedImage : 'sha256:' + 'a' . repeat ( 64 ),
// Reverse input and numeric ordering deliberately differ from Java string ordering.
members : [{ customerId : '99' , incomingNo : 'WT-99' , dispatchItemId : '302' , palletId : '402' , positionId : '502' , balanceId : '602' , outboundNo : 'OB-99' , deliveryNo : 'DL-99' },
{ customerId : '101' , incomingNo : 'WT-101' , dispatchItemId : '301' , palletId : '401' , positionId : '501' , balanceId : '601' , outboundNo : 'OB-101' , deliveryNo : 'DL-101' }] }
function resolution () {
return [{ collection : 'root' , id : '201' , dispatchNo : request . dispatchNo , warehouseId : '51' , status : 'ACTIVE' , stage : 'AFTER_LOADING' , version : '11' },
{ collection : 'trip' , id : '211' , tripNo : request . tripNo , warehouseId : '51' , status : 'PLANNED' },
{ collection : 'binding' , dispatchId : '201' , dispatchNo : request . dispatchNo , tripId : '211' , tripNo : request . tripNo },
{ collection : 'task' , id : '221' , dispatchId : '201' , warehouseId : '51' }, ... columns ,
... request . members . flatMap ( m => {
const suffix = m . customerId === '101' ? '1' : '2'
return [{ collection : 'item' , id : m . dispatchItemId , dispatchId : '201' , customerId : m . customerId , palletId : m . palletId , orderId : '70' + suffix , sourceType : null , sourceNo : null , incomingNo : m . incomingNo , packages : '1' },
{ collection : 'transfer' , id : '80' + suffix , customerId : m . customerId , incomingNo : m . incomingNo , orderId : '70' + suffix , warehouseId : '51' , needRelabel : '0' , relabelNo : null , outboundAt : null },
{ collection : 'outbound' , id : '90' + suffix , customerId : m . customerId , dispatchId : '201' , outboundNo : m . outboundNo , status : 'WAIT_OUTBOUND' },
{ collection : 'delivery' , id : '100' + suffix , customerId : m . customerId , dispatchId : '201' , deliveryNo : m . deliveryNo , outboundNo : m . outboundNo , status : 'WAIT_DELIVERY' }]
})]
}
const scope = resolveRollbackScope ( request , resolution ())
const row = ( table , values ) => {
const actual = columns . filter ( c => c . table === table ). map ( c => c . column )
for ( const key of Object . keys ( values )) assert . ok ( actual . includes ( key ), `real ${ table } . ${ key } ` )
return { ... Object . fromEntries ( actual . map ( c => [ c , null ])), ... values }
}
function baseline () {
const value = Object . fromEntries ( tableNames . map ( table => [ table , []]))
value . ll_dispatch_order . push ( row ( 'll_dispatch_order' , { id : '201' , dispatch_no : request . dispatchNo , trip_id : '211' , overseas_warehouse_id : '51' , status : 'ACTIVE' , stage : 'AFTER_LOADING' , record_version : '11' , shipped_package_count : '0' }))
value . ll_fleet_trip . push ( row ( 'll_fleet_trip' , { id : '211' , trip_no : request . tripNo , status : 'PLANNED' , record_version : '7' }))
value . ll_fleet_trip_dispatch . push ( row ( 'll_fleet_trip_dispatch' , { dispatch_id : '201' , dispatch_no : request . dispatchNo , trip_id : '211' , trip_no : request . tripNo }))
value . ll_dispatch_pda_task . push ( row ( 'll_dispatch_pda_task' , { id : '221' , dispatch_order_id : '201' }))
for ( const [ index , m ] of scope . members . entries ()) {
const suffix = String ( index + 1 )
value . ll_dispatch_item . push ( row ( 'll_dispatch_item' , { id : m . dispatchItemId , dispatch_order_id : '201' , customer_id : m . customerId }))
value . ll_outbound_order . push ( row ( 'll_outbound_order' , { id : m . outboundId , customer_id : m . customerId , dispatch_order_id : '201' , outbound_no : m . outboundNo , status : 'WAIT_OUTBOUND' }))
value . ll_delivery_order . push ( row ( 'll_delivery_order' , { id : m . deliveryId , customer_id : m . customerId , dispatch_order_id : '201' , delivery_no : m . deliveryNo , status : 'WAIT_DELIVERY' , version : '0' }))
value . ll_order . push ( row ( 'll_order' , { id : m . orderId , customer_id : m . customerId , picked_package_count : '1' , shipped_package_count : '0' }))
value . ll_warehouse_transfer_order . push ( row ( 'll_warehouse_transfer_order' , { id : m . transferId , customer_id : m . customerId , lastleg_order_id : m . orderId , transfer_no : m . incomingNo , status : 'RECEIVED' }))
value . ll_dispatch_pallet_lock . push ( row ( 'll_dispatch_pallet_lock' , { id : '110' + suffix , physical_lock_id : '170' + suffix , customer_id : m . customerId , dispatch_order_id : '201' , warehouse_pallet_id : m . palletId , status : 'ACTIVE' , active_key : 'OWN-' + suffix }))
value . ll_dispatch_physical_pallet_lock . push ( row ( 'll_dispatch_physical_pallet_lock' , { id : '170' + suffix , warehouse_id : '51' ,
warehouse_pallet_id : m . palletId , active_warehouse_pallet_id : m . palletId , dispatch_order_id : scope . dispatchId , status : 'ACTIVE' }))
value . ll_physical_pallet_guard . push ( row ( 'll_physical_pallet_guard' , { warehouse_pallet_id : m . palletId , created_at : `2026-10-19 11:00:00.00 ${ suffix } ` }))
value . ll_dispatch_pda_pick . push ( row ( 'll_dispatch_pda_pick' , { id : '120' + suffix , customer_id : m . customerId , task_id : '221' , dispatch_item_id : m . dispatchItemId , inventory_position_id : m . positionId , packages : '1' }))
value . ll_dispatch_pda_pick_order . push ( row ( 'll_dispatch_pda_pick_order' , { id : '130' + suffix , customer_id : m . customerId , pick_id : '120' + suffix , receipt_id : '140' + suffix , order_id : m . orderId , packages : '1' }))
value . ll_inventory_balance . push ( row ( 'll_inventory_balance' , { id : m . balanceId , customer_id : m . customerId , overseas_warehouse_id : '51' , available_packages : '0' }))
value . ll_inventory_position . push ( row ( 'll_inventory_position' , { id : m . positionId , balance_id : m . balanceId , customer_id : m . customerId , overseas_warehouse_id : '51' , available_packages : '0' }))
value . ll_inventory_ledger . push ( row ( 'll_inventory_ledger' , { id : '150' + suffix , customer_id : m . customerId , overseas_warehouse_id : '51' , business_type : 'SHIP' , business_order_no : request . dispatchNo , balance_id : m . balanceId , position_id : m . positionId , delta_packages : '-1' }))
}
return value
}
function snapshotRows ( snapshot ) {
return [... structuredClone ( columns ), ... tableNames . flatMap ( table => [{ collection : 'coverage' , table , count : snapshot [ table ]. length },
... snapshot [ table ]. map ( value => ({ collection : 'row' , table , row : structuredClone ( value ) }))])]
}
const updateSql = "UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=?,updated_at=? WHERE customer_id=? AND lastleg_order_id=? AND outbound_at IS NULL"
const lease = () => ({ collection : 'lease' , connectionId : '11' , transactionId : '111' , lockId : '802' , mode : 'X,REC_NOT_GAP' , ageSeconds : 1 })
function midRows () {
const dirty = baseline ()
for ( const [ index , m ] of scope . members . entries ()) {
Object . assign ( dirty . ll_outbound_order [ index ], { status : 'OUTBOUNDED' })
Object . assign ( dirty . ll_delivery_order [ index ], { status : 'DELIVERING' , dispatched_at : '2026-10-19 12:00:00.000' , version : '1' })
Object . assign ( dirty . ll_order [ index ], { picked_package_count : '0' , shipped_package_count : '1' , outbound_order_no : m . outboundNo })
dirty . ll_loading_order_status_history . push ( row ( 'll_loading_order_status_history' , { id : '160' + ( index + 1 ), customer_id : m . customerId , order_id : m . orderId , event_id : 'DISPATCH_DEPART:' + scope . dispatchNo }))
}
Object . assign ( dirty . ll_warehouse_transfer_order [ 0 ], { status : 'OUTBOUNDED' , outbound_at : '2026-10-19 12:00:00.000' })
return [ lease (), { collection : 'wait' , requester : '12' , blocker : '11' , requestingTransactionId : '112' , blockingTransactionId : '111' , lockId : '802' , blockingLockId : '802' , objectName : 'll_warehouse_transfer_order' , indexName : 'PRIMARY' ,
requestStatus : 'WAITING' , blockingStatus : 'GRANTED' , ownsRoot : 1 , statement : updateSql , sessionLockWaitSeconds : '50' }, ... snapshotRows ( dirty )]
}
const conflict = () => ({ http : 200 , body : { code : 'A0409' , message : '资源正在处理中,请稍后重试' }, requestId : 'request' , traceId : 'trace' })
test ( 'business-only request resolves one native root and real customer children in Java string order' , () => {
assert . equal ( scope . dispatchId , '201' ); assert . equal ( scope . members [ 0 ]. customerId , '101' ); assert . equal ( scope . members [ 1 ]. customerId , '99' )
assert . equal ( scope . members [ 1 ]. transferId , '802' ); assert . equal ( request . members [ 0 ]. customerId , '99' )
assert . doesNotThrow (() => validateRollbackScope ( scope ))
const sql = rollbackSql ( request , 'RESOLVE' )
assert . match ( sql , /ll_dispatch_order WHERE dispatch_no=/ )
assert . match ( sql , /'incomingNo',CAST\(inbound_code AS CHAR\)/ )
assert . doesNotMatch ( sql , /FOR UPDATE|firstDispatchId|secondDispatchId/ )
2026-10-04 10:37:43 +08:00
})
2026-10-20 10:35:52 +08:00
for ( const [ name , mutate ] of [
[ 'two roots' , rows => rows . push ({ ... rows [ 0 ], id : '999' })],
[ 'two Fleet bindings' , rows => rows . push ({ ... rows . find ( r => r . collection === 'binding' ), dispatchId : '999' })],
[ 'wrong root version' , rows => { rows [ 0 ]. version = '12' }],
[ 'invented ordinary source alias' , rows => { rows . find ( r => r . collection === 'item' ). sourceType = 'WAREHOUSE_TRANSFER' }],
[ 'relabel source' , rows => { rows . find ( r => r . collection === 'transfer' ). needRelabel = '1' }],
[ 'wrong incoming' , rows => { rows . find ( r => r . collection === 'transfer' ). incomingNo = 'BORROWED' }],
[ 'foreign child' , rows => { rows . find ( r => r . collection === 'delivery' ). dispatchId = '999' }],
[ 'missing private ID' , rows => { rows . find ( r => r . collection === 'transfer' ). id = undefined }],
[ 'truncated schema' , rows => rows . splice ( rows . findIndex ( r => r . collection === 'column' && r . table === 'll_order' ), 1 )],
]) test ( `native resolution blocks ${ name } ` , () => {
const rows = resolution (); mutate ( rows ); assert . throws (() => resolveRollbackScope ( request , rows ))
2026-10-04 10:44:33 +08:00
})
2026-10-20 10:35:52 +08:00
test ( 'scope rejects injection, repeated ownership, reversed sort and unpinned image' , () => {
for ( const change of [{ marker : "OWN';DELETE" }, { expectedImage : 'latest' }, { dispatchId : '1 OR 1=1' }, { members : [... scope . members ]. reverse () }])
assert . throws (() => validateRollbackScope ({ ... scope , ... change }))
const bad = structuredClone ( request ); bad . members [ 1 ]. customerId = bad . members [ 0 ]. customerId
assert . throws (() => validateRollbackRequest ( bad ))
2026-10-04 10:32:26 +08:00
})
2026-10-20 10:35:52 +08:00
test ( 'lease is only the second actual transfer PK, fixed read-only lease and automatic rollback' , () => {
const sql = rollbackSql ( scope , 'LEASE' )
assert . match ( sql , /FROM ll_warehouse_transfer_order WHERE id=802 AND customer_id=/ )
assert . match ( sql , /lastleg_order_id=702 AND transfer_no=/ )
assert . match ( sql , /outbound_at IS NULL FOR UPDATE;.*SLEEP\(65\)\);ROLLBACK;/ )
assert . doesNotMatch ( sql , /\b(?:INSERT|DELETE|ALTER|DROP|TRUNCATE|REPLACE|KILL|SET GLOBAL)\b|ll_dispatch_pallet_lock/ )
assert . equal (( sql . match ( /\bUPDATE\b/g ) || []). length , 1 )
assert . throws (() => rollbackSql ( scope , 'CUSTOM_SQL' ))
2026-10-04 10:32:26 +08:00
})
2026-10-20 10:35:52 +08:00
test ( 'wait proof joins exact granted/blocking PK, real transaction SQL, root ownership and requester session variable' , () => {
const sql = rollbackSql ( scope , 'OBSERVE' )
for ( const text of [ 'READ UNCOMMITTED' , 'w.BLOCKING_ENGINE_LOCK_ID' , "bl.LOCK_DATA=l.LOCK_DATA" , "l.LOCK_DATA='802'" , 'r.trx_query' ,
'v.THREAD_ID=th.THREAD_ID' , 'th.PROCESSLIST_ID=r.trx_mysql_thread_id' , "v.VARIABLE_NAME='innodb_lock_wait_timeout'" , "dl.LOCK_DATA='201'" ])
assert . ok ( sql . includes ( text ), text )
assert . doesNotMatch ( sql , /@@GLOBAL|ll_dispatch_pda_operation|DEPARTED/ )
assert . match ( rollbackSql ( scope , 'RESTORED' ), /REPEATABLE READ;\nSTART TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY;/ )
assert . doesNotMatch ( rollbackSql ( scope , 'RESTORED' ), /READ UNCOMMITTED/ )
2026-10-04 10:32:26 +08:00
})
2026-10-20 10:35:52 +08:00
test ( 'generated resolution and snapshot projections use current native schema columns' , () => {
const sql = rollbackSql ( request , 'RESOLVE' ) + '\n' + rollbackSql ( scope , 'BASELINE' )
for ( const query of sql . split ( ';' )) {
const match = query . match ( /SELECT JSON_OBJECT\([\s\S]*? FROM (ll_[a-z_]+) WHERE/ )
if ( ! match ) continue
const actual = new Set ( ddlColumns ( match [ 1 ]). map ( c => c . column ))
for ( const column of query . slice ( 0 , match . index + match [ 0 ]. indexOf ( ' FROM ' )). matchAll ( /CAST\(`?([a-z_]+)`? AS CHAR/g ))
assert . ok ( actual . has ( column [ 1 ]), ` ${ match [ 1 ] } . ${ column [ 1 ] } ` )
}
assert . ok ( columns . filter ( c => c . table === 'll_order' ). length > 50 )
assert . doesNotThrow (() => assertRollbackBaseline ( fullRollbackSnapshot ( snapshotRows ( baseline ()), scope ), scope ))
2026-10-04 10:32:26 +08:00
})
2026-10-20 10:35:52 +08:00
test ( 'remote envelope pins TEST container, healthy image, DB identity and preflights observation permissions' , () => {
function decode ( script ) { const encoded = script . match ( /decoder.decode\(\[((?:'[A-Za-z0-9+/=]*',?)+)\]\.join\(''\)\)/ )[ 1 ]; return Buffer . from ([... encoded . matchAll ( /'([^']*)'/g )]. map ( m => m [ 1 ]). join ( '' ), 'base64' ). toString () }
const python = decode ( decode ( buildRollbackRemoteScript ( scope , 'OBSERVE' , 'test-agent' )))
for ( const text of [ 'firstleg-test-lastleg' , 'firstleg-test' , "e.get('DEPLOY_ENV')!='test'" , "get('Status')!='healthy'" , scope . expectedImage , 'MYSQL_PWD' , 'variables_by_thread' ]) assert . ok ( python . includes ( text ), text )
assert . ok ( buildRollbackRemoteScript ({}, 'RUNTIME' , 'test-agent' ))
assert . throws (() => buildRollbackRemoteScript ( scope , 'LEASE' , "agent');evil()" ))
})
test ( 'full-field baseline cannot accept missing columns, rows, coverage or changed schema' , () => {
for ( const mutate of [
rows => { delete rows . find ( r => r . collection === 'row' ). row . updated_at },
rows => rows . splice ( rows . findIndex ( r => r . collection === 'row' ), 1 ),
rows => rows . splice ( rows . findIndex ( r => r . collection === 'coverage' ), 1 ),
rows => { rows . find ( r => r . collection === 'column' ). type = 'changed' },
rows => rows . push ({ ... rows . find ( r => r . collection === 'coverage' ) }),
]) { const rows = snapshotRows ( baseline ()); mutate ( rows ); assert . throws (() => fullRollbackSnapshot ( rows , scope )) }
})
for ( const [ name , mutate ] of [
[ 'no committed SHIP' , b => { b . ll_inventory_ledger = [] }],
[ 'wrong scan source' , b => { b . ll_dispatch_pda_pick_order [ 0 ]. order_id = '999' }],
[ 'prior departure command' , b => { b . ll_dispatch_command . push ( row ( 'll_dispatch_command' , { id : '999' , action : 'DEPART' })) }],
[ 'source contribution on ordinary transfer' , b => { b . ll_dispatch_item_source . push ( row ( 'll_dispatch_item_source' , { id : '991' , dispatch_order_id : '201' , dispatch_item_id : scope . members [ 0 ]. dispatchItemId })) }],
[ 'ordinary receipt also claims relabel contribution' , b => { b . ll_dispatch_pda_pick_order [ 0 ]. dispatch_item_source_id = '991' }],
[ 'ordinary receipt also claims native return carton' , b => { b . ll_dispatch_pda_pick_order [ 0 ]. return_carton_id = '991' }],
[ 'missing physical lock' , b => { b . ll_dispatch_physical_pallet_lock . pop () }],
[ 'missing permanent physical guard' , b => { b . ll_physical_pallet_guard . pop () }],
[ 'guard for a foreign pallet' , b => { b . ll_physical_pallet_guard [ 0 ]. warehouse_pallet_id = '991' }],
[ 'extra foreign permanent guard' , b => { b . ll_physical_pallet_guard . push ( row ( 'll_physical_pallet_guard' , { warehouse_pallet_id : '991' , created_at : '2026-10-19 11:00:00.001' })) }],
[ 'guard without original created timestamp' , b => { b . ll_physical_pallet_guard [ 0 ]. created_at = null }],
[ 'guard with invalid creation timestamp' , b => { b . ll_physical_pallet_guard [ 0 ]. created_at = 'NOT-A-CREATION-TIME' }],
[ 'logical claim points to other physical pallet' , b => { b . ll_dispatch_pallet_lock [ 0 ]. physical_lock_id = b . ll_dispatch_physical_pallet_lock [ 1 ]. id }],
[ 'logical claim on other main' , b => { b . ll_dispatch_pallet_lock [ 0 ]. dispatch_order_id = '991' }],
[ 'physical claim on other main' , b => { b . ll_dispatch_physical_pallet_lock [ 0 ]. dispatch_order_id = '991' }],
[ 'physical claim on other warehouse' , b => { b . ll_dispatch_physical_pallet_lock [ 0 ]. warehouse_id = '991' }],
[ 'physical claim for other pallet' , b => { b . ll_dispatch_physical_pallet_lock [ 0 ]. warehouse_pallet_id = '991' }],
[ 'physical active key mismatch' , b => { b . ll_dispatch_physical_pallet_lock [ 0 ]. active_warehouse_pallet_id = '991' }],
[ 'physical claim released' , b => { b . ll_dispatch_physical_pallet_lock [ 0 ]. status = 'RELEASED' ; b . ll_dispatch_physical_pallet_lock [ 0 ]. active_warehouse_pallet_id = null }],
[ 'physical draft ownership' , b => { b . ll_dispatch_physical_pallet_lock [ 0 ]. draft_token = 'OTHER-DRAFT' ; b . ll_dispatch_physical_pallet_lock [ 0 ]. draft_container_id = '991' }],
[ 'pool fixture' , b => { b . ll_fleet_pool_order_link . push ( row ( 'll_fleet_pool_order_link' , { id : '999' })) }],
]) test ( `baseline rejects ${ name } ` , () => { const value = baseline (); mutate ( value ); assert . throws (() => assertRollbackBaseline ( value , scope )) })
test ( 'late dirty observer proves first completed transfer, both source writes, unchanged native roots and prior SHIP' , () => {
const proof = assertMidTransactionObservation ( midRows (), scope , baseline ())
assert . equal ( proof . dirtyObservationOnly , true ); assert . equal ( proof . firstCustomerId , '101' ); assert . equal ( proof . sessionLockWaitSeconds , '50' )
assert . doesNotThrow (() => assertTargetTransferUpdate ( updateSql . replaceAll ( '=' , ' = ' ), scope ))
assert . doesNotThrow (() => assertTargetTransferUpdate ( updateSql . replace ( 'customer_id=?' , "customer_id='99'" ). replace ( 'lastleg_order_id=?' , 'lastleg_order_id=702' ), scope ))
})
for ( const [ name , mutate ] of [
[ 'foreign blocker' , r => { r [ 1 ]. blocker = '77' }],
[ 'missing wait' , r => r . splice ( 1 , 1 )],
[ 'multiple waits' , r => r . push ({ ... r [ 1 ], requester : '13' })],
[ 'wrong target row' , r => { r [ 1 ]. lockId = '801' }],
[ 'wrong blocking row' , r => { r [ 1 ]. blockingLockId = '801' }],
[ 'wrong blocking transaction' , r => { r [ 1 ]. blockingTransactionId = '113' }],
[ 'unrelated transaction' , r => { r [ 1 ]. ownsRoot = 0 }],
[ 'unavailable session variable' , r => { delete r [ 1 ]. sessionLockWaitSeconds }],
[ 'global timeout stand-in' , r => { delete r [ 1 ]. sessionLockWaitSeconds ; r [ 1 ]. globalLockWaitSeconds = '50' }],
[ 'session timeout beyond lease' , r => { r [ 1 ]. sessionLockWaitSeconds = '120' }],
[ 'unavailable requesting SQL' , r => { r [ 1 ]. statement = null }],
[ 'early child lock' , r => { r [ 1 ]. statement = 'SELECT * FROM ll_outbound_order WHERE dispatch_order_id=201 FOR UPDATE' }],
[ 'different transfer UPDATE' , r => { r [ 1 ]. statement = updateSql . replace ( 'lastleg_order_id=?' , 'lastleg_order_id=701' ) }],
[ 'first outbound not written' , r => { r . find ( x => x . table === 'll_outbound_order' && x . row ? . id === '901' ). row . status = 'WAIT_OUTBOUND' }],
[ 'first order not written' , r => { r . find ( x => x . table === 'll_order' && x . row ? . id === '701' ). row . shipped_package_count = '0' }],
[ 'no first source history' , r => { r . find ( x => x . table === 'll_loading_order_status_history' && x . row ? . order_id === '701' ). row . event_id = 'OTHER' }],
[ 'first transfer not written' , r => { r . find ( x => x . table === 'll_warehouse_transfer_order' && x . row ? . id === '801' ). row . outbound_at = null }],
[ 'second transfer already changed' , r => { r . find ( x => x . table === 'll_warehouse_transfer_order' && x . row ? . id === '802' ). row . status = 'OUTBOUNDED' }],
[ 'root changed too soon' , r => { r . find ( x => x . table === 'll_dispatch_order' && x . row ). row . stage = 'DELIVERING' }],
[ 'physical lock released before late source update' , r => { r . find ( x => x . table === 'll_dispatch_physical_pallet_lock' && x . row ). row . status = 'RELEASED' }],
[ 'permanent guard deleted in departure transaction' , r => { r . splice ( r . findIndex ( x => x . table === 'll_physical_pallet_guard' && x . row ), 1 ); r . find ( x => x . collection === 'coverage' && x . table === 'll_physical_pallet_guard' ). count -- }],
[ 'permanent guard recreated in departure transaction' , r => { r . find ( x => x . table === 'll_physical_pallet_guard' && x . row ). row . created_at = '2026-10-19 12:00:00.999' }],
[ 'permanent guard key replaced in departure transaction' , r => { r . find ( x => x . table === 'll_physical_pallet_guard' && x . row ). row . warehouse_pallet_id = '991' }],
[ 'source contribution added before late source update' , r => { const value = row ( 'll_dispatch_item_source' , { id : '991' , dispatch_order_id : '201' }); r . find ( x => x . collection === 'coverage' && x . table === 'll_dispatch_item_source' ). count = 1 ; r . push ({ collection : 'row' , table : 'll_dispatch_item_source' , row : value }) }],
[ 'prior SHIP changed' , r => { r . find ( x => x . table === 'll_inventory_ledger' && x . row ). row . delta_packages = '0' }],
]) test ( `rejects insufficient middle evidence: ${ name } ` , () => { const value = midRows (); mutate ( value ); assert . throws (() => assertMidTransactionObservation ( value , scope , baseline ())) })
test ( 'rollback compares every full field, not just business status or inventory totals' , () => {
const before = baseline (), after = structuredClone ( before )
const proof = assertRestoredSnapshot ( before , after )
assert . equal ( proof . beforeSha256 , proof . afterSha256 ); assert . equal ( proof . committedRead , true )
for ( const table of tableNames . filter ( t => after [ t ]. length )) {
const changed = structuredClone ( after ); changed [ table ][ 0 ][ Object . keys ( changed [ table ][ 0 ]). at ( - 1 )] = 'CHANGED'
assert . throws (() => assertRestoredSnapshot ( before , changed ), table )
}
})
test ( 'database conflict requires exact handler result and trace; timeout or precondition rejection fails' , () => {
assert . doesNotThrow (() => assertDatabaseLockConflict ( conflict ()))
for ( const wrong of [{ http : 0 }, { http : 500 }, { body : { code : 'S0500' , message : 'error' } }, { body : { code : 'A0409' , message : '请完成拣货' } }, { traceId : null }])
assert . throws (() => assertDatabaseLockConflict ({ ... conflict (), ... wrong }))
})
test ( 'single-attempt request keeps original key/version/body on the 60-second channel' , async () => {
const body = { recordVersion : '11' , idempotencyKey : 'ORIGINAL' }, calls = []
const context = { request : () => { throw Error ( '30_SECOND_CHANNEL' ) }, requestWithBody : async (... args ) => { calls . push ( args ); return { http : 0 , body : null } } }
assert . equal (( await requestRollbackDeparture ( context , '/owned/depart' , body , 'owned-token' )). http , 0 )
assert . deepEqual ( calls , [[ 'POST' , '/owned/depart' , body , 'owned-token' ]]); assert . equal ( calls [ 0 ][ 2 ], body )
})
test ( 'plan has no network; non-TEST execution fails before any fetch' , async () => {
const fetch = globalThis . fetch ; globalThis . fetch = () => { throw Error ( 'NETWORK_FORBIDDEN' ) }
try {
const plan = await runTripRollback ({ execute : false }, { env : {} })
assert . equal ( plan . businessSqlWrites , false ); assert . equal ( plan . deployment , false ); assert . match ( plan . fault , /One native dispatch/ )
await assert . rejects ( runTripRollback ({ execute : true , environment : 'TEST' }, { env : { CHONGCHONG_ENV : 'prod' } }), /TEST/ )
} finally { globalThis . fetch = fetch }
})
test ( 'offline verifier preserves original command and requires all phases before caller can depart/sign' , async () => {
const original = process . cwd (), temp = fs . mkdtempSync ( join ( tmpdir (), 'fleet-rollback-offline-' ))
try {
process . chdir ( temp ); fs . mkdirSync ( 'reports/fleet-cross-offline' , { recursive : true })
const calls = [], ready = { trip : { dispatches : [{ dispatchNo : request . dispatchNo }] }, dispatch : { dispatchNo : request . dispatchNo }, task : { recordVersion : '11' } }
const fixture = { ... request , pdaToken : 'synthetic-offline-token' }, body = { recordVersion : '11' , idempotencyKey : request . commandKey , remark : request . marker }
const runtime = { imageId : request . expectedImage , rows : [{ collection : 'databaseRuntime' , serverUuid : 'offline' , databaseName : 'offline' , performanceSchema : 1 }] }
const read = async ( _env , _scope , mode ) => {
calls . push ( mode )
if ( mode === 'RESOLVE' ) return { rows : resolution () }
if ( mode === 'BASELINE' || mode === 'RESTORED' ) return { rows : snapshotRows ( baseline ()) }
if ( mode === 'LEASE' ) return { rows : [{ collection : 'held' , lockId : '802' , connectionId : '11' , customerId : '99' }, { released : true }] }
if ( mode === 'LOCKS' ) return { rows : [ lease ()] }
if ( mode === 'OBSERVE' ) return { rows : midRows () }
if ( mode === 'RELEASED' ) return { rows : [] }
throw Error ( 'UNEXPECTED_MODE' )
}
const context = { marker : request . marker , requestWithBody : async (... args ) => { calls . push ( args ); return conflict () } }
const proof = await createDepartureRollbackVerifier ({}, { runId : 'offline' }, runtime , { read , wait : () => { throw Error ( 'NO_REAL_WAIT' ) } })
({ context , fixture , ready , departPath : '/owned/depart' , departBody : body , snapshot : async () => ready })
assert . equal ( proof . verified , true ); assert . equal ( proof . consistentSnapshot . fullFields , true )
assert . deepEqual ( calls . filter ( Array . isArray ), [[ 'POST' , '/owned/depart' , body , fixture . pdaToken ]])
assert . deepEqual ( calls . filter ( x => typeof x === 'string' ), [ 'RESOLVE' , 'BASELINE' , 'LEASE' , 'LOCKS' , 'OBSERVE' , 'RELEASED' , 'RESTORED' ])
assert . deepEqual ( proof . command . body , body )
const saved = JSON . parse ( fs . readFileSync ( 'reports/fleet-cross-offline/rollback.json' , 'utf8' ))
assert . equal ( saved . verified , true ); assert . ok ( saved . finishedAt )
const caller = fs . readFileSync ( new URL ( './admin-test-env-modules/fleet-cross-customer-acceptance.mjs' , import . meta . url ), 'utf8' )
2026-10-21 15:10:44 +08:00
assert . match ( caller , /await confirmFleetLoading\([^\n]+async command => \{/ )
assert . match ( caller , /await verifyDepartureRollback\(\{ context, fixture, ready, departPath: command.path, departBody: command.body, snapshot \}\)/ )
assert . match ( caller , /Rollback restores both complete inventory snapshots before loading completion/ )
const loading = fs . readFileSync ( new URL ( './admin-test-env-modules/fleet-trip-fulfillment.mjs' , import . meta . url ), 'utf8' )
const hook = loading . indexOf ( 'if (beforeComplete) await beforeComplete(proof.loadingCommand)' )
const completion = loading . indexOf ( "const confirmed = await command(context, module, 'POST', proof.loadingCommand.path" )
assert . ok ( hook >= 0 && completion > hook , '回滚验证必须在真正提交装车完成命令前结束' )
assert . match ( caller , /Original PDA actor exact replay of loading completion/ ); assert . match ( caller , /This customer independently signed/ )
2026-10-20 10:35:52 +08:00
} finally {
process . chdir ( original )
assert . ok ( resolve ( temp ). startsWith ( resolve ( tmpdir ()) + '\\' ) || resolve ( temp ). startsWith ( resolve ( tmpdir ()) + '/' ))
fs . rmSync ( temp , { recursive : true , force : true })
}
})
test ( '23 full snapshots include batch-only permanent guards, empty ordinary contributions and both customer-free physical owners from actual DDL' , () => {
const full = fullRollbackSnapshot ( snapshotRows ( baseline ()), scope )
assert . equal ( tableNames . length , 23 ); assert . equal ( Object . keys ( full ). length , 23 )
assert . equal ( snapshotRows ( baseline ()). filter ( row => row . collection === 'coverage' ). length , 23 )
assert . equal ( full . ll_dispatch_item_source . length , 0 )
assert . deepEqual ( full . ll_physical_pallet_guard . map ( row => row . warehouse_pallet_id ). sort (), [ '401' , '402' ])
assert . deepEqual ( ddlColumns ( 'll_physical_pallet_guard' ). map ( column => [ column . column , column . type , column . key ]), [[ 'warehouse_pallet_id' , 'bigint' , 'PRI' ], [ 'created_at' , 'datetime(3)' , '' ]])
assert . deepEqual ( Object . keys ( full . ll_physical_pallet_guard [ 0 ]). sort (), [ 'created_at' , 'warehouse_pallet_id' ])
assert . equal ( full . ll_dispatch_physical_pallet_lock . length , 2 )
assert . deepEqual ( Object . keys ( full . ll_dispatch_physical_pallet_lock [ 0 ]). sort (), ddlColumns ( 'll_dispatch_physical_pallet_lock' ). map ( column => column . column ). sort ())
assert . equal ( Object . hasOwn ( full . ll_dispatch_physical_pallet_lock [ 0 ], 'customer_id' ), false )
assert . equal ( ddlColumns ( 'll_dispatch_pallet_lock' ). some ( column => column . column === 'active_warehouse_pallet_id' ), false )
assert . ok ( ddlColumns ( 'll_dispatch_pallet_lock' ). some ( column => column . column === 'physical_lock_id' ))
assert . ok ( ddlColumns ( 'll_dispatch_pda_pick_order' ). some ( column => column . column === 'dispatch_item_source_id' ))
assert . doesNotThrow (() => assertRollbackBaseline ( full , scope ))
const sql = rollbackSql ( scope , 'BASELINE' )
assert . match ( sql , /FROM ll_dispatch_physical_pallet_lock WHERE dispatch_order_id=201 OR warehouse_pallet_id IN \(401,402\)/ )
assert . match ( sql , /FROM ll_dispatch_item_source WHERE dispatch_order_id=201 OR dispatch_item_id IN \(301,302\)/ )
for ( const mode of [ 'BASELINE' , 'OBSERVE' , 'RESTORED' ]) {
const guardQueries = rollbackSql ( scope , mode ). split ( ';' ). filter ( query => query . includes ( 'FROM ll_physical_pallet_guard' ))
assert . equal ( guardQueries . length , 2 )
for ( const query of guardQueries ) assert . ok ( query . endsWith ( 'FROM ll_physical_pallet_guard WHERE warehouse_pallet_id IN (401,402)' ))
assert . ok ( guardQueries . some ( query => query . includes ( 'COUNT(*)' )))
assert . ok ( guardQueries . some ( query => query . includes ( "'created_at',CAST(`created_at` AS CHAR CHARACTER SET utf8mb4)" )))
}
assert . match ( sql , /'dispatch_item_source_id',CAST\(`dispatch_item_source_id` AS CHAR CHARACTER SET utf8mb4\)/ )
})
test ( 'missing source-table zero coverage or partial new-table schema cannot masquerade as a full rollback snapshot' , () => {
for ( const table of [ 'll_dispatch_item_source' , 'll_dispatch_physical_pallet_lock' , 'll_physical_pallet_guard' ]) {
const rows = snapshotRows ( baseline ()). filter ( row => ! ( row . collection === 'coverage' && row . table === table ))
assert . throws (() => fullRollbackSnapshot ( rows , scope ), /EXACT_COVERAGE_REQUIRED/ )
const resolvedRows = resolution (). filter ( row => ! ( row . collection === 'column' && row . table === table ))
assert . throws (() => resolveRollbackScope ( request , resolvedRows ), /COMPLETE_ACTUAL_SCHEMA_REQUIRED/ )
}
const changed = snapshotRows ( baseline ())
const physical = changed . find ( row => row . table === 'll_dispatch_physical_pallet_lock' && row . row )
delete physical . row . released_at
assert . throws (() => fullRollbackSnapshot ( changed , scope ), /COMPLETE_FULL_FIELD_SNAPSHOT_REQUIRED/ )
})
test ( 'permanent guard schema cannot introduce customer, owner, state or TTL or change the original PK/time types' , () => {
for ( const mutate of [
rows => { rows . find ( row => row . table === 'll_physical_pallet_guard' && row . column === 'warehouse_pallet_id' ). key = '' },
rows => { rows . find ( row => row . table === 'll_physical_pallet_guard' && row . column === 'created_at' ). type = 'datetime' },
...[ 'customer_id' , 'owner' , 'state' , 'expires_at' ]. map ( column => rows => rows . push ({ collection : 'column' , table : 'll_physical_pallet_guard' , column , ordinal : 3 , type : 'varchar(128)' , key : '' })),
]) {
const rows = resolution (); mutate ( rows )
assert . throws (() => resolveRollbackScope ( request , rows ))
}
})
for ( const fault of [ 'delete' , 'recreate' , 'created timestamp' , 'key replacement' ]) test ( `committed rollback rejects permanent guard ${ fault } ` , () => {
const before = baseline (), restored = structuredClone ( before )
if ( fault === 'delete' ) restored . ll_physical_pallet_guard . pop ()
if ( fault === 'recreate' ) { const old = restored . ll_physical_pallet_guard . shift (); restored . ll_physical_pallet_guard . push ({ ... old , created_at : '2026-10-19 12:00:00.123' }) }
if ( fault === 'created timestamp' ) restored . ll_physical_pallet_guard [ 0 ]. created_at = '2026-10-19 11:00:00.999'
if ( fault === 'key replacement' ) restored . ll_physical_pallet_guard [ 0 ]. warehouse_pallet_id = '991'
assert . throws (() => assertRestoredSnapshot ( before , restored ), /FULL_FIELD_COMMITTED_ROLLBACK_SNAPSHOT_CHANGED/ )
2026-10-04 10:32:26 +08:00
})