test: verify fee void ACK loss with original event recovery

This commit is contained in:
2026-10-03 09:02:25 +08:00
parent 912e69aeb2
commit 4968745764
7 changed files with 688 additions and 16 deletions
+21 -5
View File
@@ -4,6 +4,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'
import { assertTestFeeVoidFaultProof, feeVoidScopeKeys, feeSourceTypes } from './test-fee-void-fault-proof.mjs'
export { assertTestFeeVoidFaultProof } from './test-fee-void-fault-proof.mjs'
export { assertTestDeliveryFaultProof } from './test-delivery-fault-proof.mjs'
import { validateEvidenceConfig } from './fba-core-regression-evidence.mjs'
@@ -19,10 +21,12 @@ export function renderTestUnloadFaultAction(action, scope, agent) {
return renderAction(action,scope,agent,'UNLOAD')
}
function renderAction(action,scope,agent,direction) {
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)
const keys=direction==='FEE_VOID'?feeVoidScopeKeys:direction==='DELIVERY'?deliveryFaultScopeKeys:direction==='UNLOAD'?['marker','customerId','containerBookingId']:['marker','customerId','loadingPlanId','containerBookingId']
if (!(action==='preflight'&&direction==='FEE_VOID')&&!['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] || ''))
|| keys.filter(key=>!['marker','sourceType'].includes(key)).some(key => !/^[1-9][0-9]{0,19}$/.test(scope?.[key] || ''))
|| (direction==='FEE_VOID'&&(!feeSourceTypes.includes(scope?.sourceType)||Number(scope?.sourceVersion)>2147483647))
|| (direction==='FEE_VOID'&&keys.some(key=>typeof scope?.[key]!=='string'))
|| (direction!=='LOADING'&&Object.keys(scope).some(key=>!keys.includes(key)))) {
throw Error('Invalid TEST fault action or scope')
}
@@ -37,6 +41,9 @@ export function createTestFaultExecutor(env, fetcher = globalThis.fetch) {
export function createTestDeliveryFaultExecutor(env, fetcher = globalThis.fetch) {
return createExecutor(env,fetcher,'DELIVERY')
}
export function createTestFeeVoidFaultExecutor(env, fetcher = globalThis.fetch) {
return createExecutor(env,fetcher,'FEE_VOID')
}
export function createTestUnloadFaultExecutor(env, fetcher = globalThis.fetch) {
return createExecutor(env,fetcher,'UNLOAD')
}
@@ -93,7 +100,7 @@ export function assertTestUnloadFaultProof(proof,scope) {
function createExecutor(env,fetcher,direction) {
validateEvidenceConfig(env)
if (env.CHONGCHONG_ENV !== 'test') throw Error('TEST fault requires CHONGCHONG_ENV=test')
let scope = null, finished = false, preparedEvidence=null, restoration=null
let scope = null, finished = false, preparedEvidence=null, restoration=null, preparationAttempted=false, ready=false
async function action(name) {
const script = renderAction(name, scope, env.FBA_CORE_JENKINS_AGENT,direction)
const response = await fetcher(new URL('scriptText', env.JENKINS_URL.replace(/\/?$/, '/')), {
@@ -118,6 +125,7 @@ function createExecutor(env,fetcher,direction) {
throw Error('TEST fault observation timed out: ' + name)
}
async function cleanup() {
if(direction==='FEE_VOID'&&!preparationAttempted)return {attempted:false}
if (!scope || finished) return restoration
await action('restore')
await wait('health', row => row.healthy)
@@ -127,7 +135,7 @@ function createExecutor(env,fetcher,direction) {
&&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((direction==='DELIVERY'?'Delivery':'Unload')+' restoration evidence mismatch')
&&restoration.currentFingerprint===preparedEvidence?.originalFingerprint))throw Error((direction==='FEE_VOID'?'Fee void':direction==='DELIVERY'?'Delivery':'Unload')+' restoration evidence mismatch')
finished = true
return restoration
}
@@ -135,15 +143,19 @@ function createExecutor(env,fetcher,direction) {
async prepare(next) {
if (scope) {
if(direction!=='LOADING'&&!isDeepStrictEqual(scope,next))throw Error('TEST unload fault scope drift')
if(direction==='FEE_VOID'&&!ready)throw Error('Fee void preparation did not complete')
return // Pin only the first actual current-run plan; other events pass through.
}
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 }
if(direction==='FEE_VOID')await wait('preflight', row=>row.ready===true,60)
preparationAttempted=true
preparedEvidence=await action('prepare')
await wait('proxy-ready', row => row.ready, 10, true)
await action('activate')
await wait('health', row => row.healthy)
ready=true
},
async readProof(expected) {
if (!scope || (direction!=='LOADING'?!isDeepStrictEqual(scope,expected):Object.keys(scope).some(key => expected?.[key] !== scope[key]))) throw Error('TEST fault scope drift')
@@ -154,6 +166,10 @@ function createExecutor(env,fetcher,direction) {
const proof = await action('proof')
if(direction==='UNLOAD')assertTestUnloadFaultProof(proof,scope)
if(direction==='DELIVERY')assertTestDeliveryFaultProof(proof,scope)
if(direction==='FEE_VOID') {
if(!isDeepStrictEqual(proof.prerequisite,preparedEvidence?.prerequisite))throw Error('Fee prerequisite evidence drift')
assertTestFeeVoidFaultProof(proof,scope)
}
await cleanup()
return { ...proof, restored: true, ...(direction!=='LOADING'?{restoration}:{}) }
} finally { if(direction!=='LOADING')await cleanup() }