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'
2026-10-08 17:07:57 +08:00
import { AcceptanceRun , parseArguments , validateExecutionOptions , buildAcceptanceSummary , createFleetPoolGrayOptions } from './admin-test-env-acceptance.mjs'
2026-10-03 07:42:18 +08:00
import { createTestEvidenceReaders } from './fba-core-regression-evidence.mjs'
2026-10-08 17:07:57 +08:00
import { createGrayEvidenceReaders } from './gray-regression-evidence.mjs'
2026-10-03 07:42:18 +08:00
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.' }
}
2026-10-08 17:07:57 +08:00
export async function runCrossCustomer ( config , dependencies = {}) {
return runCrossCustomerCore ( config , dependencies , false )
}
/** Not a CLI mode: called only by the explicit GRAY pool entry with its verified pool scenario. */
export async function runFleetPoolGrayCrossCustomer ( config , dependencies = {}) {
if ( Object . keys ( dependencies ). some ( key => ! [ 'env' , 'outputRoot' , 'verify' , 'contextFactory' ]. includes ( key )))
throw Error ( 'Gray pool cannot override evidence readers or fixture lifecycle' )
if ( config . execute && ( typeof dependencies . verify !== 'function' || typeof dependencies . contextFactory !== 'function' ))
throw Error ( 'Gray pool requires its dedicated scenario and journaled context' )
return runCrossCustomerCore ( config , dependencies , true )
}
async function runCrossCustomerCore ( config , { env = process . env , outputRoot = ROOT , readers , prepare , verify = verifyFleetCrossCustomer ,
finish = finishFleetTrip , contextFactory = options => new AcceptanceRun ( options ), pickerAccess = resolvePickerAccess , pickerCreate = createPicker , pickerDisable = disablePicker } = {}, gray = false ) {
const environment = gray ? 'GRAY' : 'TEST' , reportName = ` ${ gray ? 'fleet-pool-gray' : 'fleet-cross' } - ${ config . runId } `
const plan = { ... crossCustomerPlan ( config ), environment , output : `reports/ ${ reportName } ` ,
...( gray ? { scope : 'FLEET_CAPACITY_POOL_GRAY' , stages : [ 'current pool fixture and full pool HTTP matrix' , 'trip completion and separate resource release' ] } : {}) }
2026-10-03 07:42:18 +08:00
if ( ! config . execute ) return plan
2026-10-08 17:07:57 +08:00
if ( config . environment !== environment || ! ( gray ? [ 'gray' , 'prod' ]. includes ( env . CHONGCHONG_ENV ) : env . CHONGCHONG_ENV === 'test' ) || ! /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/ . test ( config . runId )
2026-10-03 07:42:18 +08:00
|| config . runId === 'plan' || ! /^[1-9][0-9]{0,19}$/ . test ( config . warehouseId ) || ! /^[1-9][0-9]{0,19}$/ . test ( config . destinationWarehouseId )
2026-10-08 17:07:57 +08:00
|| ! env . CHONGCHONG_ADMIN_USERNAME || ! env . CHONGCHONG_ADMIN_PASSWORD ) throw Error ( `Explicit ${ environment } configuration required` )
2026-10-03 07:42:18 +08:00
const base = new URL ( env . CHONGCHONG_ADMIN_API_BASE_URL )
2026-10-08 17:07:57 +08:00
if ( ! ( gray ? base . protocol === 'https:' : [ 'http:' , 'https:' ]. includes ( base . protocol )) || base . username || base . password || base . search || base . hash || ! [ '/api' , '/api/' ]. includes ( base . pathname )) throw Error ( `Invalid ${ environment } gateway` )
if ( gray ) readers = createGrayEvidenceReaders ( env )
else readers ||= createTestEvidenceReaders ( env )
2026-10-03 07:42:18 +08:00
// 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' )
2026-10-08 17:07:57 +08:00
const reports = resolve ( outputRoot , 'reports' ), output = join ( reports , reportName )
2026-10-03 07:42:18 +08:00
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' ))
2026-10-08 17:07:57 +08:00
const sources = [ 'scripts/fleet-cross-customer-test-env.mjs' , 'scripts/fleet-inventory-evidence.mjs' , gray ? 'scripts/gray-regression-evidence.mjs' : 'scripts/fba-core-regression-evidence.mjs' ,
2026-10-03 07:42:18 +08:00
'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' ) }))
2026-10-08 17:07:57 +08:00
save ( 'manifest.json' , { ... plan , runtimeProfile : env . CHONGCHONG_ENV , sourceCommit : execFileSync ( 'git' , [ 'rev-parse' , 'HEAD' ], { cwd : ROOT , encoding : 'utf8' }). trim (), sources })
2026-10-03 07:42:18 +08:00
await readers . preflight ()
2026-10-08 17:07:57 +08:00
const transport = { gateway : base . origin , username : env . CHONGCHONG_ADMIN_USERNAME , password : env . CHONGCHONG_ADMIN_PASSWORD ,
output , markerNamespace : createHash ( 'sha256' ). update ( ` ${ environment } : ${ config . runId } ` ). digest ( 'hex' ). slice ( 0 , 12 ) }
const options = gray ? createFleetPoolGrayOptions ( transport ) : validateExecutionOptions ({
... parseArguments ([ '--modules' , 'merchant-qualification-fixture' , '--quiet' , 'true' ]), ... transport , environment : 'test' })
2026-10-03 07:42:18 +08:00
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 }, '' )
2026-10-08 17:07:57 +08:00
if ( ! ok ( login ) || ! login . body . data ? . accessToken ) throw Error ( ` ${ environment } administrator login failed` )
2026-10-03 07:42:18 +08:00
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 }
2026-10-08 17:07:57 +08:00
report . environment = environment ; report . runtimeProfile = env . CHONGCHONG_ENV
if ( gray ) report . scope = 'FLEET_CAPACITY_POOL_GRAY'
2026-10-03 07:42:18 +08:00
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 }
}