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
+52 -14
View File
@@ -15,18 +15,26 @@ export function parseCoreArguments(argv) {
const values={},flags=new Set()
for(let i=0;i<argv.length;i++) {
const arg=argv[i]
if(['--plan','--execute'].includes(arg)) {if(flags.has(arg))throw Error('Duplicate execution flag');flags.add(arg);continue}
if(!['--environment','--run-id','--output-dir','--tax-mode'].includes(arg)||values[arg]!==undefined||!argv[i+1]||argv[i+1].startsWith('--'))throw Error('Invalid FBA core argument')
if(['--plan','--execute','--verify-delivery-event-recovery'].includes(arg)) {if(flags.has(arg))throw Error('Duplicate execution flag');flags.add(arg);continue}
if(!['--environment','--run-id','--output-dir','--tax-mode','--signature-mode'].includes(arg)||values[arg]!==undefined||!argv[i+1]||argv[i+1].startsWith('--'))throw Error('Invalid FBA core argument')
values[arg]=argv[++i]
}
if(flags.size>1)throw Error('Choose plan or execute')
if(flags.has('--plan')&&flags.has('--execute'))throw Error('Choose plan or execute')
if(values['--environment'] && values['--environment']!=='TEST')throw Error('Only explicit TEST is supported')
const execute=flags.has('--execute'),runId=values['--run-id']||(!execute?'plan':null)
if(!runId||!/^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/.test(runId))throw Error('Fresh run-id is required')
if(execute&&values['--environment']!=='TEST')throw Error('Execution requires --environment TEST')
const taxMode=values['--tax-mode']||'BUY_DECLARATION'
if(!F40_TAX_MODES.includes(taxMode))throw Error('Unsupported F40 tax mode')
return {execute,environment:'TEST',runId,taxMode,outputDir:values['--output-dir']||`reports/fba-core-${runId}`}
const signatureMode=values['--signature-mode']
if(signatureMode&&!F40_MODES.includes(signatureMode))throw Error('Unsupported F40 signature mode')
const verifyDeliveryEventRecovery=flags.has('--verify-delivery-event-recovery')
if(verifyDeliveryEventRecovery&&!signatureMode)throw Error('Delivery fault requires one explicit signature mode')
return {execute,environment:'TEST',runId,taxMode,signatureMode,verifyDeliveryEventRecovery,outputDir:values['--output-dir']||`reports/fba-core-${runId}`}
}
function selectedModes(config) {
if(config.signatureMode&&!F40_MODES.includes(config.signatureMode))throw Error('Unsupported F40 signature mode')
return config.signatureMode?[config.signatureMode]:F40_MODES
}
function taxScope(taxMode) {
if(!F40_TAX_MODES.includes(taxMode))throw Error('Unsupported F40 tax mode')
@@ -36,13 +44,16 @@ function taxScope(taxMode) {
}
export function executionPlan(config) {
return {environment:'TEST',execution:config.execute?'EXECUTE':'PLAN',runId:config.runId,outputDir:config.outputDir,
taxMode:config.taxMode,scope:taxScope(config.taxMode),
modes:F40_MODES,module:'phase5-root-fulfillment',requiredEnvironmentKeys:REQUIRED_ENV_KEYS,
taxMode:config.taxMode,scope:taxScope(config.taxMode),verifyDeliveryEventRecovery:config.verifyDeliveryEventRecovery===true,
modes:selectedModes(config),module:'phase5-root-fulfillment',requiredEnvironmentKeys:REQUIRED_ENV_KEYS,
optionalEnvironmentKeys:['CHONGCHONG_MERCHANT_USERNAME','CHONGCHONG_MERCHANT_PASSWORD'],
behavior:'Serial fresh fixtures; stop on first failed mode. HTTP creates customers/orders/files and uses shared PDA/pallet resources. SQL is read-only; no deploy or database mutation.',
reports:['manifest.json','summary.json',...F40_MODES.map(mode=>`${mode.toLowerCase()}.json`),'evidence/*.json'],
behavior:config.verifyDeliveryEventRecovery
?'One explicit mode, first FBA receipt only: scoped ACK loss, original DEAD retry and verified Lastleg image/configuration restoration. HTTP creates fresh fixtures; SQL is read-only. Do not overlap deployments or other shared-environment writers.'
:'Serial fresh fixtures; stop on first failed mode. HTTP creates customers/orders/files and uses shared PDA/pallet resources. SQL is read-only; no deploy or database mutation.',
reports:['manifest.json','summary.json',...selectedModes(config).map(mode=>`${mode.toLowerCase()}.json`),'evidence/*.json'],
retention:'Exclusive run directory and evidence files. Interrupted lock requires operator review; no automatic cleanup or resume.',
scopeLimit:'F40 three modes and receiving/completion evidence; not all F01-F40, UI acceptance or reverse event replay.'}
scopeLimit:config.verifyDeliveryEventRecovery?'Only the selected first delivery event and listed F40 mode; not all reverse events, fee-void recovery, or UI acceptance.'
:'Only the listed F40 modes and receiving/completion evidence; not all F01-F40, UI acceptance or reverse event replay.'}
}
export function validateCoreEnvironment(env,root=repositoryRoot) {
if(basename(root)!=='baiyue-firstleg'||basename(dirname(root))!=='chongchong-global')throw Error('TEST workspace required')
@@ -84,9 +95,11 @@ function createAcceptanceContext(mode,config,env,gateway) {
markerNamespace:createHash('sha256').update(config.runId+':'+mode).digest('hex').slice(0,12)})
return new AcceptanceRun(options)
}
export async function runFbaCore(config,{env=process.env,readers,createContext=createAcceptanceContext,outputRoot=repositoryRoot}={}) {
export async function runFbaCore(config,{env=process.env,readers,createContext=createAcceptanceContext,outputRoot=repositoryRoot,faultFactory}={}) {
const scopeDescription=taxScope(config.taxMode)
const modes=selectedModes(config)
const gateway=validateCoreEnvironment(env)
if(config.verifyDeliveryEventRecovery&&(env.CHONGCHONG_ENV!=='test'||!config.signatureMode))throw Error('Delivery fault requires explicit TEST and one signature mode')
const output=resolve(outputRoot,config.outputDir),reports=resolve(outputRoot,'reports'),rel=relative(reports,output)
if(!config.execute||config.environment!=='TEST'||!rel||rel.startsWith('..')||isAbsolute(rel))throw Error('Fresh report directory inside reports required')
mkdirSync(reports,{recursive:true})
@@ -94,7 +107,7 @@ export async function runFbaCore(config,{env=process.env,readers,createContext=c
const lockFd=openSync(lock,'wx')
const secrets=[...REQUIRED_ENV_KEYS,'CHONGCHONG_MERCHANT_USERNAME','CHONGCHONG_MERCHANT_PASSWORD']
.filter(key=>/PASSWORD|TOKEN|USERNAME|JENKINS_USER/.test(key)).map(key=>env[key])
const summary={runId:config.runId,environment:'TEST',taxMode:config.taxMode,startedAt:new Date().toISOString(),success:false,modes:[],scope:scopeDescription}
const summary={runId:config.runId,environment:'TEST',taxMode:config.taxMode,verifyDeliveryEventRecovery:config.verifyDeliveryEventRecovery===true,startedAt:new Date().toISOString(),success:false,modes:[],scope:scopeDescription}
let initialized=false
const merchantBefore={username:process.env.MERCHANT_TEST_USERNAME,password:process.env.MERCHANT_TEST_PASSWORD}
try {
@@ -105,7 +118,7 @@ export async function runFbaCore(config,{env=process.env,readers,createContext=c
save('manifest.json',executionPlan(config))
readers ||= createTestEvidenceReaders(env)
await readers.preflight()
for(const mode of F40_MODES) {
for(const mode of modes) {
const context=createContext(mode,config,env,gateway)
let sequence=0
const evidence=async(kind,scope,read)=>{
@@ -118,17 +131,42 @@ export async function runFbaCore(config,{env=process.env,readers,createContext=c
context.context.f40TaxMode=config.taxMode
context.context.f40ReadOnlyOutboxEvidence=scope=>evidence('completion',scope,readers.readCompletion)
context.context.phase4ReceivingEvidenceReader=scope=>evidence('receiving',scope,readers.readReceiving)
let fault, faultPreparationAttempted=false
if(config.verifyDeliveryEventRecovery) {
if(typeof readers.readFleetInventory!=='function')throw Error('Complete raw inventory reader required for delivery recovery')
const factory=faultFactory||(await import('./test-event-fault-executor.mjs')).createTestDeliveryFaultExecutor
const executor=factory(env)
fault={...executor,prepare:async scope=>{
save(`evidence/${mode.toLowerCase()}-delivery-scope.json`,{scope,stage:'PREPARE_INTENT'})
faultPreparationAttempted=true;return executor.prepare(scope)
}}
context.context.f40DeliveryEventFaultExecutor=fault
context.context.f40ReadOnlyInventoryEvidence=scope=>evidence('inventory',scope,readers.readFleetInventory)
}
let report
try {report=await context.run()}
catch (error) {report={success:false,fatal:'FBA_CORE_MODE_EXCEPTION',diagnostic:safeFailure(error),summary:{failed:1},createdResources:context.createdResources}}
finally {secrets.push(context.token,context.pdaToken,context.refreshToken,context.context?.merchantQualificationFixture?.password)}
finally {
secrets.push(context.token,context.pdaToken,context.refreshToken,context.context?.merchantQualificationFixture?.password)
if(fault) {
try {
const restoration=await fault.cleanup()
save(`evidence/${mode.toLowerCase()}-delivery-restoration.json`,{attempted:faultPreparationAttempted,restoration:restoration??null})
if(faultPreparationAttempted&&(restoration?.restored!==true||restoration.healthy!==true||restoration.proxyStopped!==true))throw Error('Delivery restoration unproven')
} catch(error) {report={...report,success:false,deliveryRestorationFailed:true,restorationDiagnostic:safeFailure(error)}}
}
}
if(config.verifyDeliveryEventRecovery) {
const proof=report?.createdResources?.phase5RootFulfillment?.deliveryEventRecovery
if(proof?.status!=='VERIFIED')report={...report,success:false,deliveryRecoveryUnproven:true}
}
report={...report,regressionScope:{taxMode:config.taxMode,signatureOrder:mode,description:scopeDescription}}
const name=`${mode.toLowerCase()}.json`;save(name,report)
const passed=modePassed(report)
summary.modes.push({mode,taxMode:config.taxMode,report:name,passed,summary:report.summary,evidenceFiles:sequence})
if(!passed)break
}
summary.success=summary.modes.length===F40_MODES.length&&summary.modes.every(mode=>mode.passed)
summary.success=summary.modes.length===modes.length&&summary.modes.every(mode=>mode.passed)
} catch (error) {summary.failure='FBA_CORE_PREFLIGHT_OR_RUN_FAILED';summary.diagnostic=safeFailure(error)}
finally {
summary.finishedAt=new Date().toISOString()