import { mkdirSync, writeFileSync, openSync, closeSync, unlinkSync } from 'node:fs' import { resolve, dirname, basename, relative, isAbsolute, join } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' import { createHash } from 'node:crypto' import { AcceptanceRun, parseArguments, validateExecutionOptions } from './admin-test-env-acceptance.mjs' import { createTestEvidenceReaders, validateEvidenceConfig, EVIDENCE_ENV_KEYS } from './fba-core-regression-evidence.mjs' const repositoryRoot=resolve(dirname(fileURLToPath(import.meta.url)),'..') export const F40_MODES=Object.freeze(['ROOT_FIRST','CHILD_FIRST','ROOT_SIGNED_REMAINDER_CANCELLED']) export const F40_TAX_MODES=Object.freeze(['BUY_DECLARATION','TAX_REFUND']) export const REQUIRED_ENV_KEYS=Object.freeze(['CHONGCHONG_ADMIN_API_BASE_URL','CHONGCHONG_ADMIN_USERNAME', 'CHONGCHONG_ADMIN_PASSWORD','ADMIN_PDA_USERNAME','ADMIN_PDA_PASSWORD','FBA_CORE_TEST_GATEWAY_ORIGIN',...EVIDENCE_ENV_KEYS]) export function parseCoreArguments(argv) { const values={},flags=new Set() for(let i=0;i`${mode.toLowerCase()}.json`),'evidence/*.json'], retention:'Exclusive run directory and evidence files. Interrupted lock requires operator review; no automatic cleanup or resume.', 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') if(REQUIRED_ENV_KEYS.some(key=>typeof env[key]!=='string'||!env[key].trim()))throw Error('Missing required TEST configuration') let base,expected try {base=new URL(env.CHONGCHONG_ADMIN_API_BASE_URL);expected=new URL(env.FBA_CORE_TEST_GATEWAY_ORIGIN)}catch{throw Error('Invalid TEST gateway configuration')} if(!['http:','https:'].includes(base.protocol)||base.username||base.password||base.search||base.hash ||expected.href!==expected.origin+'/'||base.origin!==expected.origin ||/(?:^|[.-])(?:dev|prod|production)(?:[.-]|$)/i.test(base.hostname))throw Error('TEST gateway origin mismatch') if(Boolean(env.CHONGCHONG_MERCHANT_USERNAME)!==Boolean(env.CHONGCHONG_MERCHANT_PASSWORD))throw Error('Incomplete optional merchant credentials') validateEvidenceConfig(env) return base.origin } export function redactEvidence(value,secrets=[]) { if(Array.isArray(value))return value.map(item=>redactEvidence(item,secrets)) if(value&&typeof value==='object')return Object.fromEntries(Object.entries(value).map(([key,item])=>[key, /^(?:.*password|.*token|authorization|cookie|gateway|jenkinsUrl|privateKey)$/i.test(key)?'[REDACTED]':redactEvidence(item,secrets)])) if(typeof value==='string')return secrets.filter(secret=>typeof secret==='string'&&secret.length>0) .reduce((text,secret)=>text.split(secret).join('[REDACTED]'),value) return value } export function modePassed(report) { const proof=report?.createdResources?.phase5RootFulfillment return report?.success===true&&report?.summary?.failed===0&&proof?.finalStatus==='VERIFIED' && !(report.deploymentBlockers||[]).length && !(report.steps||[]).some(step=>step.passed===false) } function safeFailure(error) { const message=typeof error?.message==='string'?error.message:'' const mysql=/^MYSQL_([0-9]{3,5})$/.exec(message) const http=/^TEST evidence HTTP ([1-5][0-9]{2})$/.exec(message) return mysql?{code:'READ_ONLY_MYSQL_FAILED',mysqlError:Number(mysql[1])} :http?{code:'READ_ONLY_HTTP_FAILED',http:Number(http[1])}:{code:'EXECUTION_FAILED'} } function createAcceptanceContext(mode,config,env,gateway) { const options=validateExecutionOptions({...parseArguments(['--modules','phase5-root-fulfillment','--quiet','true']), environment:'test',gateway,username:env.CHONGCHONG_ADMIN_USERNAME,password:env.CHONGCHONG_ADMIN_PASSWORD, pdaUsername:env.ADMIN_PDA_USERNAME,pdaPassword:env.ADMIN_PDA_PASSWORD, merchantUsername:env.CHONGCHONG_MERCHANT_USERNAME||'',merchantPassword:env.CHONGCHONG_MERCHANT_PASSWORD||'', 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,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}) const lock=join(reports,'.fba-core-regression.lock') 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,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 { writeFileSync(lockFd,JSON.stringify({runId:config.runId,pid:process.pid,startedAt:summary.startedAt})) mkdirSync(output,{recursive:false});initialized=true mkdirSync(join(output,'evidence')) const save=(name,value)=>writeFileSync(join(output,name),JSON.stringify(redactEvidence(value,secrets),null,2)+'\n',{flag:'wx'}) save('manifest.json',executionPlan(config)) readers ||= createTestEvidenceReaders(env) await readers.preflight() for(const mode of modes) { const context=createContext(mode,config,env,gateway) let sequence=0 const evidence=async(kind,scope,read)=>{ const record={mode,taxMode:config.taxMode,kind,scope,capturedAt:new Date().toISOString(),captured:false} try {record.evidence=await read(scope);record.captured=true;return record.evidence} catch (error) {record.error='READ_ONLY_EVIDENCE_FAILED';record.diagnostic=safeFailure(error);throw Error('Read-only evidence failed')} finally {save(`evidence/${mode.toLowerCase()}-${String(++sequence).padStart(4,'0')}-${kind}.json`,record)} } context.context.f40SignatureOrder=mode 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) 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===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() try { if(initialized)writeFileSync(join(output,'summary.json'),JSON.stringify(redactEvidence(summary,secrets),null,2)+'\n',{flag:'wx'}) } finally { closeSync(lockFd);unlinkSync(lock) for(const [key,value] of [['MERCHANT_TEST_USERNAME',merchantBefore.username],['MERCHANT_TEST_PASSWORD',merchantBefore.password]]) { if(value===undefined)delete process.env[key];else process.env[key]=value } } } return summary } if(process.argv[1]&&import.meta.url===pathToFileURL(resolve(process.argv[1])).href) { try { const config=parseCoreArguments(process.argv.slice(2)) const result=config.execute?await runFbaCore(config):executionPlan(config) console.log(JSON.stringify(result,null,2));if(config.execute&&!result.success)process.exitCode=1 }catch {console.error('FBA core rejected configuration or existing run/lock; no automatic overwrite.');process.exitCode=1} }