test(fba): verify signed delivery ACK loss and original event recovery

This commit is contained in:
2026-10-03 08:13:04 +08:00
parent ef1eef3278
commit 51bf4e5558
11 changed files with 499 additions and 57 deletions
+16 -10
View File
@@ -3,6 +3,8 @@
import { readFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { isDeepStrictEqual } from 'node:util'
import { assertTestDeliveryFaultProof, deliveryFaultScopeKeys } from './test-delivery-fault-proof.mjs'
export { assertTestDeliveryFaultProof } from './test-delivery-fault-proof.mjs'
import { validateEvidenceConfig } from './fba-core-regression-evidence.mjs'
const runtime = readFileSync(new URL('./test-event-fault-runtime.py', import.meta.url), 'utf8')
@@ -17,11 +19,11 @@ export function renderTestUnloadFaultAction(action, scope, agent) {
return renderAction(action,scope,agent,'UNLOAD')
}
function renderAction(action,scope,agent,direction) {
const keys=direction==='UNLOAD'?['marker','customerId','containerBookingId']:['marker','customerId','loadingPlanId','containerBookingId']
const keys=direction==='DELIVERY'?deliveryFaultScopeKeys:direction==='UNLOAD'?['marker','customerId','containerBookingId']:['marker','customerId','loadingPlanId','containerBookingId']
if (!['prepare', 'activate', 'proxy-ready', 'health', 'status', 'recover', 'proof', 'restore', 'finish'].includes(action)
|| !/^[A-Za-z0-9_.-]{1,128}$/.test(agent) || !/^[A-Za-z0-9_-]{1,96}$/.test(scope?.marker || '')
|| keys.filter(key=>key!=='marker').some(key => !/^[1-9][0-9]{0,19}$/.test(scope?.[key] || ''))
|| (direction==='UNLOAD'&&Object.keys(scope).some(key=>!keys.includes(key)))) {
|| (direction!=='LOADING'&&Object.keys(scope).some(key=>!keys.includes(key)))) {
throw Error('Invalid TEST fault action or scope')
}
const config = encode(JSON.stringify({ action, scope, direction, ...(action === 'prepare' ? { proxySource } : {}) }))
@@ -32,6 +34,9 @@ function renderAction(action,scope,agent,direction) {
export function createTestFaultExecutor(env, fetcher = globalThis.fetch) {
return createExecutor(env,fetcher,'LOADING')
}
export function createTestDeliveryFaultExecutor(env, fetcher = globalThis.fetch) {
return createExecutor(env,fetcher,'DELIVERY')
}
export function createTestUnloadFaultExecutor(env, fetcher = globalThis.fetch) {
return createExecutor(env,fetcher,'UNLOAD')
}
@@ -117,23 +122,23 @@ function createExecutor(env,fetcher,direction) {
await action('restore')
await wait('health', row => row.healthy)
restoration=await action('finish')
if(direction==='UNLOAD'&&!(/^sha256:[0-9a-f]{64}$/.test(preparedEvidence?.originalImageId||'')
if(direction!=='LOADING'&&!(/^sha256:[0-9a-f]{64}$/.test(preparedEvidence?.originalImageId||'')
&&/^[0-9a-f]{64}$/.test(preparedEvidence?.originalFingerprint||'')
&&restoration?.restored===true&&restoration.healthy===true&&restoration.proxyStopped===true
&&restoration.producerName==='firstleg-test-lastleg'&&restoration.originalImageId===preparedEvidence?.originalImageId
&&restoration.currentImageId===preparedEvidence?.originalImageId&&restoration.originalFingerprint===preparedEvidence?.originalFingerprint
&&restoration.currentFingerprint===preparedEvidence?.originalFingerprint))throw Error('Unload restoration evidence mismatch')
&&restoration.currentFingerprint===preparedEvidence?.originalFingerprint))throw Error((direction==='DELIVERY'?'Delivery':'Unload')+' restoration evidence mismatch')
finished = true
return restoration
}
return {
async prepare(next) {
if (scope) {
if(direction==='UNLOAD'&&!isDeepStrictEqual(scope,next))throw Error('TEST unload fault scope drift')
if(direction!=='LOADING'&&!isDeepStrictEqual(scope,next))throw Error('TEST unload fault scope drift')
return // Pin only the first actual current-run plan; other events pass through.
}
if(direction==='UNLOAD')renderAction('prepare',next,env.FBA_CORE_JENKINS_AGENT,direction)
scope = direction==='UNLOAD'?{...next}:{ marker: next.marker, customerId: next.customerId,
if(direction!=='LOADING')renderAction('prepare',next,env.FBA_CORE_JENKINS_AGENT,direction)
scope = direction!=='LOADING'?{...next}:{ marker: next.marker, customerId: next.customerId,
loadingPlanId: next.loadingPlanId, containerBookingId: next.containerBookingId }
preparedEvidence=await action('prepare')
await wait('proxy-ready', row => row.ready, 10, true)
@@ -141,16 +146,17 @@ function createExecutor(env,fetcher,direction) {
await wait('health', row => row.healthy)
},
async readProof(expected) {
if (!scope || (direction==='UNLOAD'?!isDeepStrictEqual(scope,expected):Object.keys(scope).some(key => expected?.[key] !== scope[key]))) throw Error('TEST fault scope drift')
if (!scope || (direction!=='LOADING'?!isDeepStrictEqual(scope,expected):Object.keys(scope).some(key => expected?.[key] !== scope[key]))) throw Error('TEST fault scope drift')
try {
await wait('status', row => row.producer?.publishStatus === 'DEAD' && row.producer.failedAttempts === 12, 180)
await action('recover')
await wait('status', row => row.producer?.publishStatus === 'PUBLISHED', 90)
const proof = await action('proof')
if(direction==='UNLOAD')assertTestUnloadFaultProof(proof,scope)
if(direction==='DELIVERY')assertTestDeliveryFaultProof(proof,scope)
await cleanup()
return { ...proof, restored: true, ...(direction==='UNLOAD'?{restoration}:{}) }
} finally { if(direction==='UNLOAD')await cleanup() }
return { ...proof, restored: true, ...(direction!=='LOADING'?{restoration}:{}) }
} finally { if(direction!=='LOADING')await cleanup() }
},
cleanup,
}