40 lines
2.9 KiB
JavaScript
40 lines
2.9 KiB
JavaScript
import test from 'node:test'
|
|||
|
|
import assert from 'node:assert/strict'
|
||
|
|
import { validateRollbackScope, rollbackSql, buildRollbackRemoteScript, assertMidTransactionObservation, runTripRollback } from './fleet-trip-rollback-test-env.mjs'
|
||
|
|
const scope={marker:'OWN_RUN',tripNo:'TR-OWN',firstCustomerId:'101',secondCustomerId:'102',firstDispatchId:'201',secondDispatchId:'202',firstOutboundId:'301',secondPalletId:'401',firstDispatchNo:'DS-OWN',expectedImage:'sha256:'+'a'.repeat(64)}
|
||
|
|
const rows=()=>[{collection:'lease',connectionId:11,lockId:'99'},{collection:'wait',requester:12,blocker:11,lockId:'99'},
|
||
|
|
{collection:'firstDispatch',id:'201',status:'DEPARTED',shipped:1},{collection:'firstOutbound',id:'301',status:'OUTBOUNDED'},{collection:'firstReplay',count:1}]
|
||
|
|
test('plan and import are network-free; execution rejects missing TEST profile before any network',async()=>{
|
||
|
|
const plan=await runTripRollback({execute:false},{env:{}})
|
||
|
|
assert.equal(plan.businessSqlWrites,false);assert.equal(plan.deployment,false)
|
||
|
|
await assert.rejects(runTripRollback({execute:true},{env:{CHONGCHONG_ENV:'prod'}}),/TEST/)
|
||
|
|
})
|
||
|
|
test('scope rejects reused customers, injection and unpinned runtime',()=>{
|
||
|
|
assert.equal(validateRollbackScope(scope),scope)
|
||
|
|
for(const changed of [{secondCustomerId:'101'},{secondDispatchId:'201'},{firstDispatchId:'1 OR 1=1'},{marker:"RUN';DELETE"},{expectedImage:'latest'}])assert.throws(()=>validateRollbackScope({...scope,...changed}))
|
||
|
|
})
|
||
|
|
test('lease contains only owned locking reads, fixed duration and rollback, without mutation or schema change',()=>{
|
||
|
|
const sql=rollbackSql(scope,'LEASE')
|
||
|
|
assert.match(sql,/dispatch_order_id=202/);assert.match(sql,/warehouse_pallet_id=401/);assert.match(sql,/FOR UPDATE;.*SLEEP\(65\)\);ROLLBACK;/)
|
||
|
|
assert.doesNotMatch(sql,/\b(?:INSERT|DELETE|ALTER|DROP|TRUNCATE|REPLACE|KILL|SET GLOBAL)\b/)
|
||
|
|
assert.equal((sql.match(/UPDATE/g)||[]).length,1)
|
||
|
|
assert.throws(()=>rollbackSql(scope,'CUSTOM_SQL'))
|
||
|
|
assert.throws(()=>buildRollbackRemoteScript(scope,'LEASE',"agent');evil()"))
|
||
|
|
})
|
||
|
|
test('dirty observation is separate from final consistent cleanup evidence',()=>{
|
||
|
|
assert.match(rollbackSql(scope,'OBSERVE'),/READ UNCOMMITTED/)
|
||
|
|
assert.match(rollbackSql(scope,'RELEASED'),/REPEATABLE READ/)
|
||
|
|
assert.match(rollbackSql(scope,'RELEASED'),/READ ONLY/)
|
||
|
|
assert.equal(assertMidTransactionObservation(rows()).dirtyObservationOnly,true)
|
||
|
|
})
|
||
|
|
for(const fault of ['foreign blocker','no wait','two waiters','not yet written','no outbound write','no replay write'])test(`refuses indirect mid-transaction evidence: ${fault}`,()=>{
|
||
|
|
const value=rows()
|
||
|
|
if(fault==='foreign blocker')value[1].blocker=77
|
||
|
|
if(fault==='no wait')value.splice(1,1)
|
||
|
|
if(fault==='two waiters')value.push({...value[1],requester:13})
|
||
|
|
if(fault==='not yet written')value[2].status='READY_TO_DEPART'
|
||
|
|
if(fault==='no outbound write')value[3].status='WAIT_OUTBOUND'
|
||
|
|
if(fault==='no replay write')value[4].count=0
|
||
|
|
assert.throws(()=>assertMidTransactionObservation(value))
|
||
|
|
})
|