2026-10-03 07:42:18 +08:00
import { mkdirSync , writeFileSync , openSync , closeSync , unlinkSync , readFileSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { dirname , resolve , join } from 'node:path'
import { fileURLToPath , pathToFileURL } from 'node:url'
import { createHash } from 'node:crypto'
import { AcceptanceRun , parseArguments , validateExecutionOptions , buildAcceptanceSummary } from './admin-test-env-acceptance.mjs'
import { createTestEvidenceReaders } from './fba-core-regression-evidence.mjs'
import { redactEvidence } from './fba-core-regression-test-env.mjs'
import { resolvePickerAccess , createPicker , disablePicker } from './admin-test-env-modules/phase4-overseas-pda.mjs'
import { verifyFleetCrossCustomer } from './admin-test-env-modules/fleet-cross-customer-acceptance.mjs'
2026-10-03 07:51:17 +08:00
import { finishFleetTrip } from './admin-test-env-modules/fleet-trip-fulfillment.mjs'
2026-10-03 07:42:18 +08:00
const ROOT = resolve ( dirname ( fileURLToPath ( import . meta . url )), '..' )
const ok = response => response ? . http === 200 && String ( response . body ? . code ) === '200'
export function parseCrossCustomerArguments ( args ) {
const config = { execute : false , environment : 'TEST' , runId : 'plan' , warehouseId : '' , destinationWarehouseId : '' }
const seen = new Set ()
for ( let i = 0 ; i < args . length ; i ++ ) {
const arg = args [ i ]
if ([ '--execute' , '--plan' ]. includes ( arg )) {
if ( seen . has ( 'action' )) throw Error ( 'Duplicate execution mode' )
seen . add ( 'action' ); config . execute = arg === '--execute' ; continue
}
const field = { '--environment' : 'environment' , '--run-id' : 'runId' , '--warehouse-id' : 'warehouseId' , '--destination-warehouse-id' : 'destinationWarehouseId' }[ arg ]
if ( ! field || seen . has ( arg ) || ! args [ i + 1 ] || args [ i + 1 ]. startsWith ( '--' )) throw Error ( 'Unsupported or duplicate argument' )
seen . add ( arg ); config [ field ] = args [ ++ i ]
}
if ( config . environment !== 'TEST' || ! /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/ . test ( config . runId )
|| ( config . warehouseId && ! /^[1-9][0-9]{0,19}$/ . test ( config . warehouseId ))
|| ( config . destinationWarehouseId && ! /^[1-9][0-9]{0,19}$/ . test ( config . destinationWarehouseId ))
|| ( config . execute && ( ! seen . has ( '--environment' ) || config . runId === 'plan' || ! config . warehouseId || ! config . destinationWarehouseId ))) throw Error ( 'Explicit TEST, fresh run-id and warehouse/destination IDs required' )
return config
}
export function crossCustomerPlan ( config ) {
return { environment : 'TEST' , execute : config . execute , runId : config . runId , warehouseId : config . warehouseId || null , destinationWarehouseId : config . destinationWarehouseId || null ,
output : `reports/fleet-cross- ${ config . runId } ` , stages : [ 'two new customers' , 'two ordinary transfer receipts' , 'two dispatches on one trip' ,
2026-10-03 07:51:17 +08:00
'unready customer gate rejection' , 'shared departure and replay' , 'independent receipts' , 'trip completion and separate resource release' ],
2026-10-03 07:42:18 +08:00
limits : 'Prerequisite rejection is not mid-transaction rollback. No deployment, configuration change, SQL writes or automatic rerun. Retain business samples; disable temporary PDA in finally.' }
}
export async function runCrossCustomer ( config , { env = process . env , outputRoot = ROOT , readers , prepare , verify = verifyFleetCrossCustomer ,
2026-10-03 07:51:17 +08:00
finish = finishFleetTrip , contextFactory = options => new AcceptanceRun ( options ), pickerAccess = resolvePickerAccess , pickerCreate = createPicker , pickerDisable = disablePicker } = {}) {
2026-10-03 07:42:18 +08:00
const plan = crossCustomerPlan ( config )
if ( ! config . execute ) return plan
if ( config . environment !== 'TEST' || env . CHONGCHONG_ENV !== 'test' || ! /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/ . test ( config . runId )
|| config . runId === 'plan' || ! /^[1-9][0-9]{0,19}$/ . test ( config . warehouseId ) || ! /^[1-9][0-9]{0,19}$/ . test ( config . destinationWarehouseId )
|| ! env . CHONGCHONG_ADMIN_USERNAME || ! env . CHONGCHONG_ADMIN_PASSWORD ) throw Error ( 'Explicit TEST configuration required' )
const base = new URL ( env . CHONGCHONG_ADMIN_API_BASE_URL )
if ( ! [ 'http:' , 'https:' ]. includes ( base . protocol ) || base . username || base . password || base . search || base . hash || ! [ '/api' , '/api/' ]. includes ( base . pathname )) throw Error ( 'Invalid TEST gateway' )
readers ||= createTestEvidenceReaders ( env )
// Load the reviewed producer before creating any resources.
prepare ||= ( await import ( './admin-test-env-modules/fleet-cross-customer-fixture.mjs' )). createFleetCrossCustomerFixture
if ( typeof prepare !== 'function' || typeof readers . readFleetInventory !== 'function' ) throw Error ( 'Fixture producer and inventory reader required' )
const reports = resolve ( outputRoot , 'reports' ), output = join ( reports , `fleet-cross- ${ config . runId } ` )
mkdirSync ( reports , { recursive : true })
const lockPath = join ( reports , '.fleet-acceptance.lock' ), lock = openSync ( lockPath , 'wx' )
let context , picker , createdPicker , initialized = false , report , evidenceIndex = 0 , requestIndex = 0
const sessions = new Map ()
const secrets = Object . entries ( env ). filter (([ name ]) => / PASSWORD | TOKEN / . test ( name )). map (([, value ]) => value )
const save = ( name , value ) => writeFileSync ( join ( output , name ), JSON . stringify ( redactEvidence ( value , secrets ), null , 2 ) + '\n' )
try {
writeFileSync ( lock , JSON . stringify ({ pid : process . pid , runId : config . runId }))
mkdirSync ( output ); initialized = true ; mkdirSync ( join ( output , 'evidence' )); mkdirSync ( join ( output , 'requests' ))
const sources = [ 'scripts/fleet-cross-customer-test-env.mjs' , 'scripts/fleet-inventory-evidence.mjs' , 'scripts/fba-core-regression-evidence.mjs' ,
'scripts/admin-test-env-modules/fleet-cross-customer-fixture.mjs' , 'scripts/admin-test-env-modules/fleet-cross-customer-acceptance.mjs' ,
'scripts/admin-test-env-modules/fleet-trip-fulfillment.mjs' ]. map ( path => ({ path , sha256 : createHash ( 'sha256' ). update ( readFileSync ( join ( ROOT , path ))). digest ( 'hex' ) }))
save ( 'manifest.json' , { ... plan , sourceCommit : execFileSync ( 'git' , [ 'rev-parse' , 'HEAD' ], { cwd : ROOT , encoding : 'utf8' }). trim (), sources })
await readers . preflight ()
const options = validateExecutionOptions ({ ... parseArguments ([ '--modules' , 'merchant-qualification-fixture' , '--quiet' , 'true' ]),
environment : 'test' , gateway : base . origin , username : env . CHONGCHONG_ADMIN_USERNAME , password : env . CHONGCHONG_ADMIN_PASSWORD ,
markerNamespace : createHash ( 'sha256' ). update ( config . runId ). digest ( 'hex' ). slice ( 0 , 12 ) })
context = contextFactory ( options )
for ( const method of [ 'request' , 'requestWithBody' ]) {
const original = context [ method ]
context [ method ] = async function (... args ) {
const number = ++ requestIndex , [ verb , path , body ] = args
const record = { number , method : verb , path , stage : 'INTENT' , body : body instanceof FormData ? '[multipart file]' : body }
save ( `requests/ ${ number } .json` , record )
const response = await original . apply ( this , args )
if ( verb === 'POST' && path === '/api/platform/v1/iam/users' && ok ( response )
&& String ( body ? . overseasWarehouseId ) === config . warehouseId && body ? . username ? . startsWith ( 'pda_overseas_' )) {
createdPicker = { membershipId : response . body ? . data ? . membershipId }
}
if ( path === '/api/auth/login' && ok ( response ) && response . body ? . data ? . accessToken ) {
const tokens = response . body . data
sessions . set ( tokens . accessToken , tokens . refreshToken )
secrets . push ( tokens . accessToken , tokens . refreshToken )
}
save ( `requests/ ${ number } .json` , { ... record , stage : 'RESPONSE' , response })
save ( 'progress.json' , { marker : context . marker , steps : context . steps , createdResources : context . createdResources })
return response
}
}
const login = await context . request ( 'POST' , '/api/auth/login' , { loginId : options . username , password : options . password }, '' )
if ( ! ok ( login ) || ! login . body . data ? . accessToken ) throw Error ( 'TEST administrator login failed' )
context . token = login . body . data . accessToken ; context . refreshToken = login . body . data . refreshToken
const warehouses = await context . request ( 'GET' , '/api/platform/v1/last-leg/fleet/drivers/warehouse-options?region=OVERSEAS' )
if ( ! ok ( warehouses ) || ! warehouses . body . data ? . some ( row => String ( row . id ) === config . warehouseId )) throw Error ( 'Requested warehouse not available' )
const access = await pickerAccess ( context )
if ( ! access ) throw Error ( 'PDA role unavailable' )
picker = await pickerCreate ( context , 81 , config . warehouseId , access )
if ( ! picker ? . token ) throw Error ( 'Temporary PDA login failed' )
secrets . push ( picker . password , picker . token , picker . refreshToken )
const readInventory = async scope => {
const result = await readers . readFleetInventory ( scope )
save ( `evidence/inventory- ${ ++ evidenceIndex } .json` , result ); return result
}
const fixture = await prepare ( context , { execute : true , warehouseId : config . warehouseId , destinationWarehouseId : config . destinationWarehouseId , pdaToken : picker . token , readInventory })
save ( 'fixture.json' , fixture )
2026-10-04 12:07:37 +08:00
const verification = await verify ( context , fixture , { execute : true , readInventory })
if ( verification ? . status !== 'VERIFIED' || verification . tripNo !== fixture . tripNo ) throw Error ( 'Current fixture verification incomplete' )
2026-10-03 07:51:17 +08:00
const trip = context . createdResources . fleetTrips ? . find ( row => row . tripNo === fixture . tripNo )
if ( ! trip ) throw Error ( 'Original fixture trip missing for resource completion' )
const beforeClosure = []
for ( const member of fixture . members ) beforeClosure . push ( await readInventory ({ marker : context . marker , customerId : member . customerId , warehouseId : config . warehouseId }))
await finish ( context , 'fleet-cross-customer-closure' , trip )
const afterClosure = []
for ( const member of fixture . members ) afterClosure . push ( await readInventory ({ marker : context . marker , customerId : member . customerId , warehouseId : config . warehouseId }))
if ( JSON . stringify ( beforeClosure ) !== JSON . stringify ( afterClosure )) throw Error ( 'Trip closure changed customer inventory' )
2026-10-04 12:07:37 +08:00
report = { success : verification . status === 'VERIFIED'
2026-10-03 07:51:17 +08:00
&& trip . closure ? . status === 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' , closureInventoryUnchanged : true }
2026-10-03 07:42:18 +08:00
} catch ( error ) {
report = { success : false , error : error instanceof Error ? error . message : 'CROSS_CUSTOMER_FAILED' }
} finally {
if ( context && ( picker ? . membershipId || createdPicker ? . membershipId )) {
try { await pickerDisable ( context , picker ? . membershipId ? picker : createdPicker , 81 ) } catch { report . success = false ; report . cleanupError = 'PDA disable failed' }
}
if ( context ) {
let sessionsClosed = true
for ( const [ accessToken , refreshToken ] of sessions ) {
const response = await context . request ( 'POST' , '/api/auth/logout' , { refreshToken }, accessToken )
if ( ! ok ( response ) && response . http !== 401 && String ( response . body ? . code ) !== 'A0401' ) sessionsClosed = false
}
report . sessionsClosed = sessionsClosed
report . summary = buildAcceptanceSummary ( context . steps , context . modules , context . coverageGaps )
report . success &&= sessionsClosed && report . summary . failed === 0
report . marker = context . marker ; report . createdResources = context . createdResources ; report . steps = context . steps
}
report ||= { success : false }
report . requestCount = requestIndex ; report . inventorySnapshots = evidenceIndex ; report . goalComplete = false
try { if ( initialized ) save ( 'report.json' , report ) } finally { closeSync ( lock ); unlinkSync ( lockPath ) }
}
return report
}
if ( process . argv [ 1 ] && import . meta . url === pathToFileURL ( resolve ( process . argv [ 1 ])). href ) {
try {
const config = parseCrossCustomerArguments ( process . argv . slice ( 2 ))
const result = await runCrossCustomer ( config )
console . log ( JSON . stringify ( config . execute ? { success : result . success , error : result . error , summary : result . summary ,
requestCount : result . requestCount , inventorySnapshots : result . inventorySnapshots , output : crossCustomerPlan ( config ). output } : result , null , 2 ))
if ( config . execute && ! result . success ) process . exitCode = 1
} catch ( error ) { console . error ( error . message ); process . exitCode = 1 }
}