test(fba): verify signed delivery ACK loss and original event recovery
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user