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;i1)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}`} } function taxScope(taxMode) { if(!F40_TAX_MODES.includes(taxMode))throw Error('Unsupported F40 tax mode') return taxMode==='TAX_REFUND' ? 'F40 TAX_REFUND: real customs upload, receipt confirmation, declaration/release, final documents, signatures and post-COMPLETED customer document reads/downloads.' : 'F40 BUY_DECLARATION: existing buy-declaration shipment, signatures and remainder cancellation.' } 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, 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'], 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.'} } 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}={}) { const scopeDescription=taxScope(config.taxMode) const gateway=validateCoreEnvironment(env) 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,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 F40_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 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)} 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) } 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} }