test(order): verify warehouse faults and await recovery projection

This commit is contained in:
2026-10-04 00:04:36 +08:00
parent 3f6083ff02
commit e8cf5e70ba
8 changed files with 229 additions and 51 deletions
@@ -3,16 +3,47 @@ import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { buildOrderDependencyFaultPlan } from './admin-test-env-modules/orders.mjs'
import { scopeFor, renderOrderFaultAction, createOrderFaultRuntime, runOrderFaultWindow } from './order-submit-dependency-fault-executor.mjs'
import { scopeFor, renderOrderFaultAction, createOrderFaultRuntime, runOrderFaultWindow, waitForRecoveredSubmission } from './order-submit-dependency-fault-executor.mjs'
const image = 'sha256:' + 'a'.repeat(64)
function fixture() {
const detail = id => ({order:{id,tenantId:'20',orderNo:`FL${id}`,version:2,status:'DRAFT'},cargoes:[],parties:[],internalNotes:[],statusHistory:[]})
const detail = id => ({order:{id,tenantId:'20',receivingWarehouseId:'30',orderNo:`FL${id}`,version:2,status:'DRAFT'},cargoes:[],parties:[],internalNotes:[],statusHistory:[]})
const raw={marker:'TEST-DEPENDENCY',customerId:'20',registeredCustomerId:'20',merchant:detail('101'),platform:detail('102')}
return {raw,plan:buildOrderDependencyFaultPlan(raw)}
}
const env={CHONGCHONG_ENV:'test',JENKINS_URL:'https://offline.invalid/',JENKINS_USER:'offline',JENKINS_API_TOKEN:'never-print-this',FBA_CORE_JENKINS_AGENT:'test-agent'}
function recoverySnapshot(projected=true) {
const {raw,plan}=fixture(),detail=structuredClone(raw.merchant)
detail.order.status='SUBMITTED';detail.order.version=3
detail.statusHistory=[{action:'CREATE'},{action:'SUBMITTED'}]
return {plan,snapshot:{detail,task:null,inbounds:projected?[{id:'1001',orderId:'101',orderNo:'FL101',tenantId:'20',orderStatusSnapshot:'SUBMITTED'}]:[],inboundPageComplete:true}}
}
test('recovery waits for the actual inbound projection even when two empty reads are stable',async()=>{
const {plan,snapshot}=recoverySnapshot(),empty=recoverySnapshot(false).snapshot
const values=[empty,empty,snapshot,snapshot];let reads=0
const value=await waitForRecoveredSubmission({plan,role:'merchant',snapshot:async()=>values[Math.min(reads++,values.length-1)],wait:async()=>{}})
assert.equal(reads,4);assert.deepEqual(value,snapshot)
})
test('missing submission projection cannot pass recovery after the bounded wait',async()=>{
const {plan,snapshot}=recoverySnapshot(false);let reads=0
await assert.rejects(waitForRecoveredSubmission({plan,role:'merchant',snapshot:async()=>{reads++;return snapshot},wait:async()=>{}}),/RECOVERY_PROJECTION_TIMEOUT/)
assert.equal(reads,61)
})
test('recovery rejects duplicate or foreign projected inbounds instead of waiting them away',async()=>{
for(const change of [s=>s.inbounds.push({...s.inbounds[0],id:'1002'}),s=>s.inbounds[0].tenantId='21',s=>s.inbounds[0].orderId='102']) {
const {plan,snapshot}=recoverySnapshot();change(snapshot)
await assert.rejects(waitForRecoveredSubmission({plan,role:'merchant',snapshot:async()=>snapshot,wait:async()=>{}}),/RECOVERY_SUBMISSION_FACTS_INVALID/)
}
})
test('recovery projection wait rejects order fact drift and incomplete pagination',async()=>{
const {plan,snapshot}=recoverySnapshot(),changed=structuredClone(snapshot);changed.detail.order.remark='late mutation'
let reads=0
await assert.rejects(waitForRecoveredSubmission({plan,role:'merchant',snapshot:async()=>reads++?changed:snapshot,wait:async()=>{}}),/RECOVERY_ORDER_DRIFT/)
const incomplete=structuredClone(snapshot);incomplete.inboundPageComplete=false
await assert.rejects(waitForRecoveredSubmission({plan,role:'merchant',snapshot:async()=>incomplete,wait:async()=>{}}),/RECOVERY_SUBMISSION_FACTS_INVALID/)
})
test('runtime classifies dependency HTTP stages without exposing tokens, URLs or response bodies',()=>{
const script=String.raw`
import importlib.util,sys,urllib.error
@@ -42,6 +73,23 @@ test('renderer binds TEST scope and emits no Jenkins credential; ambiguous remot
await assert.rejects(runtime('activate'),/OUTCOME_UNKNOWN_USE_STATUS/)
assert.equal(calls,1)
})
test('renderer chunks every embedded Groovy literal and preserves the complete runtime payload',()=>{
const {plan}=fixture(),scope=scopeFor(plan)
const routing={configurationReviewed:true,nacos:{baseUrl:'http://nacos.invalid:8848',loginPath:'/nacos/v3/auth/user/login',configPath:'/nacos/v3/admin/cs/config'},upstreams:{IAM:'http://iam.invalid:9201',FINANCE:'http://finance.invalid:9203',WAREHOUSE:'http://warehouse.invalid:9205'}}
const rendered=renderOrderFaultAction('preflight',scope,'test-agent',{routing})
const arrays=text=>Array.from(text.matchAll(/\[('[A-Za-z0-9+/=]+'(?:,'[A-Za-z0-9+/=]+')*)\]\.join\(''\)/g),match=>Array.from(match[1].matchAll(/'([^']+)'/g),part=>part[1]))
const outer=arrays(rendered)
assert.equal(outer.length,1)
const inner=Buffer.from(outer[0].join(''),'base64').toString('utf8')
assert.ok(Array.from(inner.matchAll(/'([^']*)'/g),match=>match[1].length).every(length=>length<=4000))
const embedded=arrays(inner)
assert.equal(embedded.length,2)
const config=JSON.parse(Buffer.from(embedded[1].join(''),'base64').toString('utf8'))
assert.deepEqual(config.scope,scope)
assert.deepEqual(config.routing,routing)
assert.equal(config.action,'preflight')
assert.ok(Buffer.from(config.commonSource,'base64').length>0)
})
function windowFixture(fault='') {
const {raw,plan}=fixture(),calls=[],records=[]
@@ -55,7 +103,7 @@ function windowFixture(fault='') {
if(action==='health')return {healthy:true}
if(action==='mode'){mode=extra.mode;return {mode}}
if(action==='records')return {records}
if(action==='verify-restored')return {originalImageId:image,currentImageId:image,originalFingerprint:'same',currentFingerprint:'same',upstreamReads:{IAM:true,FINANCE:true},nacosSourcesUnchanged:true}
if(action==='verify-restored')return {originalImageId:image,currentImageId:image,originalFingerprint:'same',currentFingerprint:'same',upstreamReads:{IAM:true,FINANCE:true,WAREHOUSE:true},nacosSourcesUnchanged:true}
if(action==='finish')return {restored:true,healthy:true,proxyStopped:true,orderRoutesVerified:true}
return {}
},
@@ -72,9 +120,9 @@ function windowFixture(fault='') {
return {options,calls}
}
test('four observations restore original routes and verify business recovery before stopping proxy',async()=>{
test('six observations restore original routes and verify business recovery before stopping proxy',async()=>{
const {options,calls}=windowFixture(),report=await runOrderFaultWindow(options)
assert.equal(report.success,true);assert.equal(report.observations.length,4)
assert.equal(report.success,true);assert.equal(report.observations.length,6)
assert.ok(calls.indexOf('restore')<calls.indexOf('verify-restored'))
assert.ok(calls.indexOf('verify-restored')<calls.indexOf('business-recovery'))
assert.ok(calls.indexOf('business-recovery')<calls.indexOf('finish'))
@@ -84,6 +132,28 @@ test('failed business assertion still restores, but never reports fault verifica
assert.equal(report.success,false);assert.equal(report.restoration.restored,true)
assert.ok(calls.includes('finish'))
})
test('a redirected public warehouse plan stops before any runtime mutation',async()=>{
const {options,calls}=windowFixture()
options.plan.receivingWarehouseId='31'
options.plan.phases[2].targetPath='/internal/warehouses/0/31/active'
const report=await runOrderFaultWindow(options)
assert.equal(report.success,false)
assert.ok(!calls.includes('prepare'))
assert.ok(!calls.includes('activate'))
})
test('unverified warehouse restoration blocks business recovery and proxy finish',async()=>{
const {options,calls}=windowFixture(),original=options.action
options.action=async(action,extra)=>{
const result=await original(action,extra)
if(action==='verify-restored')result.upstreamReads.WAREHOUSE=false
return result
}
const report=await runOrderFaultWindow(options)
assert.equal(report.success,false)
assert.ok(calls.includes('restore'))
assert.ok(!calls.includes('business-recovery'))
assert.ok(!calls.includes('finish'))
})
for(const fault of ['prepare-unknown','restore-unknown','business-recovery-failed'])test(`unsafe cleanup blocks finish: ${fault}`,async()=>{
const {options,calls}=windowFixture(fault),report=await runOrderFaultWindow(options)
assert.equal(report.success,false);assert.equal(calls.filter(action=>action==='prepare').length,1)
@@ -131,15 +201,37 @@ class Fake:
class RuntimeTests(unittest.TestCase):
def fixture(self):
f=Fake();tools=dict(common);tools.update(inspect=f.inspect,docker=f.docker)
scope={'marker':'OFFLINE','customerId':'7','orderIds':{'merchant':'10','platform':'11'},'planSha256':'b'*64}
scope={'marker':'OFFLINE','customerId':'7','receivingWarehouseId':'30','orderIds':{'merchant':'10','platform':'11'},'planSha256':'b'*64}
r=m.Runtime(scope,tools,Path('offline-not-created'));saved=[];controls=[]
r.save=lambda state:saved.append(copy.deepcopy(state));r.control=lambda state,mode=None,ttl=0:(controls.append(mode) or {'mode':'PASS'})
r.probes=lambda state:{'IAM':True,'FINANCE':True}
r.probes=lambda state:{'IAM':True,'FINANCE':True,'WAREHOUSE':True}
r.config_fingerprint=lambda container,routing:'reviewed-config'
state={'scope':scope,'phase':'PROXY_STARTED','originalId':f.original['Id'],'originalImageId':f.original['Image'],
'originalFingerprint':common['fingerprint'](f.original),'originalBody':common['recreation'](f.original,common['env_of'](f.original),full=True),
'proxyPort':12345,'controlPort':12346,'mutation':None,'routing':{},'routingSourceSha256':'reviewed-config'}
return f,r,state,saved,controls
def test_warehouse_probe_requires_the_exact_active_public_domestic_warehouse(self):
import io
from unittest.mock import patch
f,r,s,saved,controls=self.fixture()
state={'internalToken':'never-public','upstreams':{'IAM':'http://iam.invalid','FINANCE':'http://finance.invalid','WAREHOUSE':'http://warehouse.invalid'}}
warehouse={'id':30,'tenantId':0,'status':'ACTIVE','warehouseType':'DOMESTIC','warehouseName':'Scoped receiving warehouse'}
class Response(io.BytesIO):status=200
def response(request,timeout):
url=request.full_url
if '/internal/iam/' in url:data={'eligible':True}
elif '/internal/finance/' in url:data={'actualBalance':0}
else:
self.assertTrue(url.endswith('/internal/warehouses/0/30/active'))
data={'warehouse':warehouse}
return Response(json.dumps({'code':'200','data':data}).encode())
with patch.object(m,'http_open',side_effect=response):
self.assertEqual(m.Runtime.probes(r,state),{'IAM':True,'FINANCE':True,'WAREHOUSE':True})
for field,value in [('id',31),('tenantId',20),('status','DISABLED'),('warehouseType','FBA'),('warehouseName','')]:
original=warehouse[field];warehouse[field]=value
with self.assertRaisesRegex(RuntimeError,'RECEIVING_WAREHOUSE_NOT_READY'):m.Runtime.probes(r,state)
warehouse[field]=original
self.assertEqual(f.calls,[])
def test_original_fingerprint_config_and_image_restored(self):
f,r,s,saved,controls=self.fixture();r.activate(s)
self.assertTrue(r.matches_injected(f.current,s));self.assertEqual(s['phase'],'INJECTED')
@@ -227,5 +319,5 @@ sys.exit(0 if result.wasSuccessful() else 1)
fileURLToPath(new URL('./order-submit-dependency-fault-runtime.py',import.meta.url)),
fileURLToPath(new URL('./test-event-fault-runtime.py',import.meta.url))],{encoding:'utf8',env:{...process.env,PYTHONDONTWRITEBYTECODE:'1'}})
assert.equal(result.status,0,result.stderr || result.error?.message)
assert.match(result.stderr,/Ran 10 tests/)
assert.match(result.stderr,/Ran 11 tests/)
})