2026-10-07 18:11:55 +08:00
import { mkdirSync , openSync , closeSync , writeFileSync , writeSync , ftruncateSync , fsyncSync , unlinkSync ,
readFileSync , realpathSync } from 'node:fs'
import { resolve , dirname , join , isAbsolute , extname } from 'node:path'
import { homedir } from 'node:os'
import { fileURLToPath , pathToFileURL } from 'node:url'
import { createHash , randomUUID } 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-07 18:11:55 +08:00
import { parseCrossCustomerArguments } from './fleet-cross-customer-test-env.mjs'
import { createFleetCrossCustomerFixture } from './admin-test-env-modules/fleet-cross-customer-fixture.mjs'
import { controlledPalletMeasurements } from './admin-test-env-modules/fleet-trip-fulfillment.mjs'
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-07 18:11:55 +08:00
import { guardFleetWrites , redactFleetWrite , findFleetActorPhone } from './fleet-test-env-acceptance.mjs'
import { createPicker , disablePicker , resolvePickerAccess } from './admin-test-env-modules/phase4-overseas-pda.mjs'
const ROOT = resolve ( dirname ( fileURLToPath ( import . meta . url )), '..' )
export const PRIVATE_DIRECTORY = resolve ( homedir (), '.codex' , 'env' , 'chongchong-global' )
const API = '/api/platform/v1/last-leg' , POOL = ` ${ API } /fleet/capacity-offers` , IAM = '/api/platform/v1/iam'
const PREFIX = 'platform:fleet:capacity-offer:'
const id = value => String ( value ?? '' )
const demand = ( condition , message ) => { if ( ! condition ) throw Error ( message ) }
const ok = response => response ? . http === 200 && id ( response ? . body ? . code ) === '200'
export function privateCredentialsPath ( value , privateDirectory = PRIVATE_DIRECTORY ) {
demand ( typeof value === 'string' && isAbsolute ( value ), 'EXPLICIT_ABSOLUTE_PRIVATE_CREDENTIALS_FILE_REQUIRED' )
const file = resolve ( value ), directory = resolve ( privateDirectory )
const same = ( a , b ) => process . platform === 'win32' ? a . toLowerCase () === b . toLowerCase () : a === b
demand ( same ( dirname ( file ), directory ) && extname ( file ). toLowerCase () === '.json' , 'CREDENTIALS_FILE_MUST_BE_JSON_DIRECTLY_IN_PRIVATE_ENV_DIRECTORY' )
return file
}
export function parseUiFixtureArguments ( args ) {
const ordinary = []; let credentialsFile
for ( let index = 0 ; index < args . length ; index ++ ) {
if ( args [ index ] !== '--private-credentials-file' ) { ordinary . push ( args [ index ]); continue }
demand ( credentialsFile === undefined && args [ index + 1 ] && ! args [ index + 1 ]. startsWith ( '--' ), 'INVALID_DUPLICATE_PRIVATE_CREDENTIALS_FILE' )
credentialsFile = privateCredentialsPath ( args [ ++ index ])
}
const config = parseCrossCustomerArguments ( ordinary )
demand ( ! config . execute || credentialsFile , 'EXPLICIT_ABSOLUTE_PRIVATE_CREDENTIALS_FILE_REQUIRED' )
return { ... config , privateCredentialsFile : credentialsFile || null }
}
export function uiFixturePlan ( config ) {
2026-10-08 17:07:57 +08:00
if ( config . environment === 'GRAY' ) return { ... uiFixturePlan ({ ... config , environment : 'TEST' }), environment : 'GRAY' ,
output : `reports/fleet-capacity-pool-ui-gray- ${ config . runId } ` ,
purpose : 'GRAY UI fixture only: one OPEN 30/18-space trip, four complete non-reserving drafts, two customer logins, sales A with quote:list, sales B without quote permission, manage dispatcher and one warehouse-bound PDA for H5.' ,
permissions : { salesA : uiActorPermissions ( 'SALES_A' , true ), salesB : uiActorPermissions ( 'SALES_B' , true ), manager : uiActorPermissions ( 'MANAGER' , true ) },
stopsAt : 'OPEN_AND_4_DRAFTS_READY_FOR_UI' , retainedPda : true ,
schedule : 'From preparation: cutoff +8h, departure +9h, delivery ETA +10h, driver expected end +12h; no actual departure.' ,
boundary : 'No automatic rerun/resume, SQL mutations, deployment, pool confirmation, PDA picking, departure or receipt signature. Retain business samples and all six UI logins including the warehouse-bound PDA; logout script sessions. Actual profile remains prod/gray; production-identity read-only evidence required.' }
2026-10-07 18:11:55 +08:00
return { execute : Boolean ( config . execute ), environment : 'TEST' , runId : config . runId ,
output : `reports/fleet-capacity-pool-ui- ${ config . runId } ` , privateCredentialsFile : config . privateCredentialsFile ,
purpose : 'UI fixture only: one OPEN 30-space trip with valid 18-space owned plan and 24 non-reserving drafts, two minimal sales logins, one manage login, two qualified customer logins.' ,
permissions : { sales : [ 'list' , 'inquiry-create' , 'inquiry-edit' , 'inquiry-cancel' ]. map ( name => PREFIX + name ), manager : [ PREFIX + 'manage' ] },
stopsAt : 'OPEN_AND_24_DRAFTS_READY_FOR_UI' , fullAcceptance : false ,
credentials : 'Exclusive private JSON creation before any writes; every generated password is persisted and fsynced there only. Reports and console redact passwords and tokens.' ,
boundary : 'No automatic rerun/resume, SQL mutations, deployment, pool confirmation, PDA picking, departure, receipt signature or completed-order fabrication. Retain business samples and UI accounts; disable the fixture-only PDA and logout script sessions.' }
}
2026-10-08 17:07:57 +08:00
export function uiActorPermissions ( kind , gray = false ) {
demand ([ 'SALES_A' , 'SALES_B' , 'MANAGER' ]. includes ( kind ), 'UI_ACTOR_KIND_REQUIRED' )
if ( kind === 'MANAGER' ) return [ PREFIX + 'manage' ]
return [...[ 'list' , 'inquiry-create' , 'inquiry-edit' , 'inquiry-cancel' ]. map ( name => PREFIX + name ),
...( gray && kind === 'SALES_A' ? [ 'platform:fleet:quote:list' ] : [])]
}
export function grayUiSchedule ( now = Date . now ()) {
demand ( Number . isFinite ( now ), 'VALID_GRAY_UI_CLOCK_REQUIRED' )
// Persisted business DATETIME columns have second precision.
const base = Math . floor ( now / 1000 ) * 1000
const instant = hours => new Date ( base + hours * 3600000 ). toISOString ()
const delivery = new Date ( base + 10 * 3600000 )
return { cutoffAt : instant ( 8 ), plannedDepartureAt : instant ( 9 ), expectedDriverEndAt : instant ( 12 ),
plannedDeliveryAt : new Date ( delivery . getTime () - delivery . getTimezoneOffset () * 60000 ). toISOString (). slice ( 0 , 19 ) }
}
export function applyGrayUiTransportWindow ( method , path , body , schedule ) {
if ( method !== 'POST' || ! /^\/api\/platform\/v1\/last-leg\/dispatches\/[^/]+\/transfer-delivery$/ . test ( path )) return body
demand ( body && schedule ? . plannedDeliveryAt , 'GRAY_UI_DELIVERY_WINDOW_REQUIRED' )
return { ... body , plannedDeliveryAt : schedule . plannedDeliveryAt }
}
2026-10-07 18:11:55 +08:00
/** Private directory override is for isolated offline tests, never exposed as a CLI option. */
export function createPrivateCredentialStore ( file , initial , { privateDirectory = PRIVATE_DIRECTORY } = {}) {
const target = privateCredentialsPath ( file , privateDirectory )
demand ( realpathSync ( dirname ( target )) === realpathSync ( privateDirectory ), 'PRIVATE_DIRECTORY_RESOLUTION_MISMATCH' )
const descriptor = openSync ( target , 'wx' , 0o600 )
let closed = false
const save = value => {
demand ( ! closed , 'PRIVATE_CREDENTIAL_STORE_CLOSED' )
const text = JSON . stringify ( value , null , 2 ) + '\n'
writeSync ( descriptor , text , 0 , 'utf8' ); ftruncateSync ( descriptor , Buffer . byteLength ( text )); fsyncSync ( descriptor )
}
try { save ( initial ) } catch ( error ) { closeSync ( descriptor ); throw error }
return { save , close () { if ( ! closed ) { closeSync ( descriptor ); closed = true } } }
}
export function publicUiEvidence ( value , secrets = []) {
const stripped = item => {
if ( Array . isArray ( item )) return item . map ( stripped )
if ( item && typeof item === 'object' ) return Object . fromEntries ( Object . entries ( item ). map (([ key , child ]) =>
[ key , /^(?:base64|bytes|content)$/i . test ( key ) ? '[BINARY OMITTED]' : stripped ( child )]))
return item
}
return stripped ( redactFleetWrite ( value , secrets ))
}
2026-10-08 17:07:57 +08:00
export function uiInquiryBody ( offer , customerId , index , key , complete = false ) {
2026-10-07 18:11:55 +08:00
const destination = offer . destinations ? . find ( row => row . destinationKey ? . startsWith ( 'FBA:' ))
demand ( offer . status === 'OPEN' && offer . canCreateInquiry && destination , 'OPEN_FBA_OFFER_REQUIRED' )
return { tripRecordVersion : offer . tripRecordVersion , offerRecordVersion : offer . recordVersion ,
customerId , businessType : index % 2 === 0 ? 'LOCAL_LOOSEBOARD' : 'US_TRUCK' , deliveryMode : 'SELF_DELIVERY' ,
2026-10-08 17:07:57 +08:00
expectedPalletCount : index === 0 && ! complete ? null : 1 ,
2026-10-07 18:11:55 +08:00
cargo : { destinationKey : destination . destinationKey , cargoName : `UI验收意向- ${ index + 1 } ` ,
2026-10-08 17:07:57 +08:00
packageCount : index === 0 && ! complete ? null : 1 , palletSpaces : index === 0 && ! complete ? null : 1 , weightKg : 100 , volumeCbm : 1 ,
2026-10-07 18:11:55 +08:00
priceBasis : 'PALLET' , unitPrice : 30 , currency : 'CNY' , contactName : 'UI验收联系人' , contactPhone : '19900000000' , remark : '独立UI验收草稿,不占位' },
idempotencyKey : key }
}
2026-10-08 17:07:57 +08:00
export async function runUiFixture ( config , dependencies = {}) {
demand ( config . environment === 'TEST' , 'EXPLICIT_TEST_FIXTURE_CONFIGURATION_REQUIRED' )
return runUiFixtureCore ( config , dependencies , false )
}
export async function runGrayUiFixture ( config , dependencies = {}) {
demand ( config . environment === 'GRAY' , 'EXPLICIT_GRAY_UI_FIXTURE_CONFIGURATION_REQUIRED' )
demand ( Object . keys ( dependencies ). every ( key => [ 'env' , 'outputRoot' ]. includes ( key )), 'GRAY_UI_CANNOT_OVERRIDE_EVIDENCE_OR_LIFECYCLE' )
return runUiFixtureCore ( config , dependencies , true )
}
async function runUiFixtureCore ( config , { env = process . env , outputRoot = ROOT , readers ,
prepare = createFleetCrossCustomerFixture , contextFactory = options => new AcceptanceRun ( options ) } = {}, gray = false ) {
2026-10-07 18:11:55 +08:00
if ( ! config . execute ) return uiFixturePlan ( config )
2026-10-08 17:07:57 +08:00
demand (( gray ? [ 'prod' , 'gray' ]. includes ( env . CHONGCHONG_ENV ) : env . CHONGCHONG_ENV === 'test' ) && /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/ . test ( config . runId )
2026-10-07 18:11:55 +08:00
&& config . runId !== 'plan' && /^[1-9]\d*$/ . test ( config . warehouseId ) && /^[1-9]\d*$/ . test ( config . destinationWarehouseId )
2026-10-08 17:07:57 +08:00
&& env . CHONGCHONG_ADMIN_USERNAME && env . CHONGCHONG_ADMIN_PASSWORD , `EXPLICIT_ ${ gray ? 'GRAY' : 'TEST' } _FIXTURE_CONFIGURATION_REQUIRED` )
2026-10-07 18:11:55 +08:00
const privateFile = privateCredentialsPath ( config . privateCredentialsFile )
const endpoint = new URL ( env . CHONGCHONG_ADMIN_API_BASE_URL )
2026-10-08 17:07:57 +08:00
demand (( gray ? endpoint . protocol === 'https:' : [ 'http:' , 'https:' ]. includes ( endpoint . protocol )) && [ '/api' , '/api/' ]. includes ( endpoint . pathname )
&& ! endpoint . username && ! endpoint . password && ! endpoint . hash && ! endpoint . search , 'INVALID_UI_API_BASE' )
if ( gray ) readers = createGrayEvidenceReaders ( env )
else readers ||= createTestEvidenceReaders ( env )
const reports = join ( outputRoot , 'reports' ), output = resolve ( outputRoot , uiFixturePlan ( config ). output )
const transport = { gateway : endpoint . origin , username : env . CHONGCHONG_ADMIN_USERNAME , password : env . CHONGCHONG_ADMIN_PASSWORD ,
output , markerNamespace : createHash ( 'sha256' ). update ( ` ${ config . environment } : ${ config . runId } ` ). digest ( 'hex' ). slice ( 0 , 12 ) }
const options = gray ? createFleetPoolGrayOptions ( transport ) : validateExecutionOptions ({
... parseArguments ([ '--modules' , 'merchant-qualification-fixture' , '--quiet' , 'true' ]), ... transport , environment : 'test' })
const secretDocument = { schemaVersion : 1 , environment : config . environment , runtimeProfile : env . CHONGCHONG_ENV , runId : config . runId , status : 'PREPARING' , createdAt : new Date (). toISOString (),
2026-10-07 18:11:55 +08:00
gatewayOrigin : endpoint . origin , accounts : [], business : {} }
const vault = createPrivateCredentialStore ( privateFile , secretDocument )
2026-10-08 17:07:57 +08:00
const schedule = gray ? grayUiSchedule () : null , draftCount = gray ? 4 : 24 , perSales = draftCount / 2
2026-10-07 18:11:55 +08:00
const secrets = Object . entries ( env ). filter (([ key ]) => / PASSWORD | TOKEN | SECRET / . test ( key )). map (([, value ]) => value ). filter ( Boolean )
const sessions = new Map (), kinds = new Map ()
let context , picker , lock , restore , initialized = false , requestNumber = 0 , inventoryNumber = 0 , report
const save = ( name , value ) => writeFileSync ( join ( output , name ), JSON . stringify ( publicUiEvidence ( value , secrets ), null , 2 ) + '\n' )
const writePrivate = () => vault . save ( secretDocument )
const remember = account => {
if ( account . password ) secrets . push ( account . password )
const existing = secretDocument . accounts . find ( row => row . username === account . username )
if ( existing ) Object . assign ( existing , account ); else secretDocument . accounts . push ( account )
writePrivate ()
}
try {
mkdirSync ( reports , { recursive : true })
lock = openSync ( join ( reports , '.fleet-acceptance.lock' ), 'wx' )
writeFileSync ( lock , JSON . stringify ({ runId : config . runId , pid : process . pid , kind : 'UI_FIXTURE' }))
mkdirSync ( output ); initialized = true ; mkdirSync ( join ( output , 'requests' )); mkdirSync ( join ( output , 'write-journal' )); mkdirSync ( join ( output , 'evidence' ))
save ( 'manifest.json' , { ... uiFixturePlan ( config ), sourceSha256 : createHash ( 'sha256' ). update ( readFileSync ( fileURLToPath ( import . meta . url ))). digest ( 'hex' ) })
await readers . preflight ()
context = contextFactory ( options )
secretDocument . marker = context . marker ; writePrivate ()
const proof = context . createdResources . fleetCapacityPoolUi = { marker : context . marker , status : 'PREPARING' , actors : [], drafts : [], fullAcceptance : false }
for ( const method of [ 'request' , 'requestWithBody' ]) {
const original = context [ method ]
context [ method ] = async function (... args ) {
2026-10-08 17:07:57 +08:00
const sequence = ++ requestNumber , [ verb , path ] = args
if ( gray ) args [ 2 ] = applyGrayUiTransportWindow ( verb , path , args [ 2 ], schedule )
const body = args [ 2 ]
2026-10-07 18:11:55 +08:00
if ( verb === 'POST' && path === ` ${ IAM } /users` && body ? . username && body . password ) {
remember ({ kind : kinds . get ( body . username ) || ( body . username . startsWith ( 'pda_overseas_' ) ? 'FIXTURE_PDA' : 'FIXTURE_USER' ),
2026-10-08 17:07:57 +08:00
username : body . username , password : body . password , roleIds : body . roleIds , warehouseId : body . overseasWarehouseId || null , status : 'PREPARED' })
2026-10-07 18:11:55 +08:00
}
save ( `requests/ ${ sequence } .json` , { stage : 'INTENT' , sequence , method : verb , path , body : body instanceof FormData ? '[MULTIPART]' : body })
const response = await original . apply ( this , args )
if ( ok ( response ) && verb === 'POST' && path === ` ${ IAM } /tenants` ) {
const data = response . body . data , customer = data . tenant || data . customer || data
remember ({ kind : 'CUSTOMER' , customerId : id ( customer . id || data . tenantId ), customerName : customer . tenantName || customer . customerName ,
username : data . administratorUsername , password : data . temporaryPassword , status : 'CREATED' })
}
if ( ok ( response ) && verb === 'POST' && path === ` ${ IAM } /users` ) {
const data = response . body . data
remember ({ kind : kinds . get ( body . username ) || ( body . username . startsWith ( 'pda_overseas_' ) ? 'FIXTURE_PDA' : 'FIXTURE_USER' ),
username : body . username , password : body . password , membershipId : id ( data . membershipId ), roleIds : data . roleIds , status : 'ACTIVE' })
}
if ( ok ( response ) && path === '/api/auth/login' && response . body . data ? . accessToken ) {
const auth = response . body . data
sessions . set ( auth . accessToken , auth . refreshToken ); secrets . push ( auth . accessToken , auth . refreshToken )
}
save ( `requests/ ${ sequence } .json` , { stage : 'RESPONSE' , sequence , method : verb , path , body : body instanceof FormData ? '[MULTIPART]' : body , response })
save ( 'progress.json' , { marker : context . marker , createdResources : context . createdResources , steps : context . steps })
return response
}
}
restore = guardFleetWrites ( context , async entry => {
const file = openSync ( join ( output , 'write-journal' , ` ${ entry . sequence } .json` ), 'w' )
try { writeFileSync ( file , JSON . stringify ( publicUiEvidence ( entry , secrets ), null , 2 ) + '\n' ); fsyncSync ( file ) } finally { closeSync ( file ) }
}, { guardFetch : true })
const check = ( label , condition , response = null ) => {
context . assert ( 'fleet-capacity-pool-ui' , label , response , Boolean ( condition ), '' , { scenario : 'UI_FIXTURE' , deploymentBlocker : true })
demand ( condition , label )
}
const request = async ( method , path , body , token = context . token ) => {
const response = await context . request ( method , path , body , token )
check ( ` ${ method } ${ path } ` , ok ( response ) && response . requestId && response . traceId , response )
return response . body . data
}
const login = await request ( 'POST' , '/api/auth/login' , { loginId : options . username , password : options . password }, '' )
context . token = login . accessToken ; context . refreshToken = login . refreshToken
const access = await resolvePickerAccess ( context )
check ( 'Fixture PDA role exists' , Boolean ( access ))
picker = await createPicker ( context , 93 , config . warehouseId , access )
check ( 'Fixture PDA authenticated' , Boolean ( picker ? . token ))
const readInventory = async scope => {
const snapshot = await readers . readFleetInventory ( scope ); save ( `evidence/inventory- ${ ++ inventoryNumber } .json` , snapshot ); return snapshot
}
const fixture = await prepare ( context , { execute : true , warehouseId : config . warehouseId ,
destinationWarehouseId : config . destinationWarehouseId , pdaToken : picker . token , readInventory })
save ( 'fixture.json' , fixture )
const trip = context . createdResources . fleetTrips . find ( row => row . tripNo === fixture . tripNo ), tripPath = ` ${ API } /fleet/trips/ ${ encodeURIComponent ( fixture . tripNo ) } `
check ( 'Existing producer created exactly two untouched customer dispatches' , fixture . members . length === 2 && trip )
let detail = await request ( 'GET' , tripPath ), plan = await request ( 'GET' , ` ${ tripPath } /capacity-plan` )
check ( 'Owned fixture vehicle has actual 30-space capacity' , Number ( detail . trip . maxPalletSpaces ) === 30 && plan . cargo . length === 2 )
2026-10-08 17:07:57 +08:00
// GRAY delivery ETA was set on the producer's real transfer command, so all four times stay ordered.
2026-10-07 18:11:55 +08:00
detail = await request ( 'PUT' , ` ${ tripPath } /plan` , { recordVersion : detail . trip . recordVersion ,
2026-10-08 17:07:57 +08:00
plannedDepartureAt : schedule ? . plannedDepartureAt || new Date ( Date . now () + 4 * 3600000 ). toISOString (),
expectedDriverEndAt : schedule ? . expectedDriverEndAt || detail . trip . expectedDriverEndAt ,
stops : detail . trip . stops , reason : gray ? '灰度UI验收:统一截止、发车、到站与司机结束时间,不执行发车' : 'UI验收窗口:四小时后计划发车,不执行发车' })
if ( gray ) {
check ( 'GRAY trip departure and driver end follow the explicit UI window' , Date . parse ( detail . trip . plannedDepartureAt ) === Date . parse ( schedule . plannedDepartureAt )
&& Date . parse ( detail . trip . expectedDriverEndAt ) === Date . parse ( schedule . expectedDriverEndAt ))
for ( const member of fixture . members ) {
const delivery = await request ( 'GET' , ` ${ API } /delivery-orders/ ${ member . deliveryNo } ?customerId= ${ member . customerId } ` )
check ( 'Original customer delivery ETA follows the same valid UI window' , delivery . order . plannedDeliveryAt === schedule . plannedDeliveryAt )
}
}
2026-10-07 18:11:55 +08:00
plan = await request ( 'GET' , ` ${ tripPath } /capacity-plan` )
await request ( 'POST' , ` ${ tripPath } /capacity-plan` , { recordVersion : plan . recordVersion , manifestHash : plan . manifestHash ,
pallets : controlledPalletMeasurements ( plan . cargo , detail . trip . stops ). map ( row => ({ ... row , palletSpaces : 9 })), reason : '仅UI验收:自有计划18/总30,不作为实际发车计量' })
const permissions = await request ( 'GET' , ` ${ IAM } /permissions` ), actorTokens = []
const key = suffix => createHash ( 'sha256' ). update ( ` ${ context . marker } :UI: ${ suffix } ` ). digest ( 'hex' )
for ( const kind of [ 'SALES_A' , 'SALES_B' , 'MANAGER' ]) {
const suffix = key ( kind ). slice ( 0 , 16 ), username = `pool_ui_ ${ kind . toLowerCase () } _ ${ suffix } `
2026-10-08 17:07:57 +08:00
const expected = uiActorPermissions ( kind , gray )
2026-10-07 18:11:55 +08:00
const candidates = expected . map ( code => permissions . filter ( row => row . permissionCode === code && row . status === 'ACTIVE' ))
check ( 'Exact fixture permissions are installed' , candidates . every ( rows => rows . length === 1 && rows [ 0 ]. id ))
const role = await request ( 'POST' , ` ${ IAM } /roles` , { roleCode : `POOL_UI_ ${ kind } _ ${ suffix } ` . toUpperCase (), roleName : ` ${ context . marker } - ${ kind } ` , description : '仅本轮UI验收账号' })
proof . actors . push ({ kind , roleId : id ( role . id ) })
await request ( 'PUT' , ` ${ IAM } /roles/permissions` , { roleId : id ( role . id ), permissionIds : candidates . map ( rows => id ( rows [ 0 ]. id )) })
kinds . set ( username , kind )
const password = `Ui ${ randomUUID (). replaceAll ( '-' , '' ) } A9` ; remember ({ kind , username , password , roleIds : [ id ( role . id )], status : 'PREPARED' })
const user = await request ( 'POST' , ` ${ IAM } /users` , { username , password , displayName : ` ${ context . marker } -同名销售` ,
phone : await findFleetActorPhone ( context , suffix ), email : ` ${ username } @example.com` , tenantId : '0' , roleIds : [ id ( role . id )], status : 'ACTIVE' })
proof . actors . at ( - 1 ). membershipId = id ( user . membershipId )
const auth = await request ( 'POST' , '/api/auth/login' , { loginId : username , password }, '' )
const effective = await request ( 'GET' , ` ${ IAM } /authorization/permissions` , undefined , auth . accessToken )
check ( 'UI actor has only its exact minimal permissions' , JSON . stringify ([... effective ]. sort ()) === JSON . stringify ([... expected ]. sort ()))
actorTokens . push ( auth . accessToken )
}
detail = await request ( 'GET' , tripPath )
const draft = await request ( 'POST' , POOL , { tripNo : fixture . tripNo , tripRecordVersion : detail . trip . recordVersion ,
2026-10-08 17:07:57 +08:00
cutoffAt : schedule ? . cutoffAt || new Date ( Math . min ( Date . now () + 3 * 3600000 , Date . parse ( detail . trip . plannedDepartureAt ) - 60000 )). toISOString (),
2026-10-07 18:11:55 +08:00
contactName : 'UI验收调度' , contactPhone : '19900000000' , remark : context . marker , idempotencyKey : key ( 'OFFER' ) }, actorTokens [ 2 ])
let offer = ( await request ( 'POST' , ` ${ POOL } / ${ draft . offer . offerNo } /publish` , { tripRecordVersion : draft . offer . tripRecordVersion ,
offerRecordVersion : draft . offer . recordVersion , idempotencyKey : key ( 'PUBLISH' ) }, actorTokens [ 2 ])). offer
check ( 'OPEN UI offer is available with exactly 12 planned spaces' , offer . status === 'OPEN' && offer . canCreateInquiry && offer . canConfirm
&& offer . destinations . length === 1 && Number ( offer . destinations [ 0 ]. remaining . palletSpaces ) === 12 )
proof . offerNo = offer . offerNo ; proof . tripNo = fixture . tripNo
2026-10-08 17:07:57 +08:00
for ( let index = 0 ; index < draftCount ; index ++ ) {
const owner = index < perSales ? 0 : 1 , member = fixture . members [ owner ]
const body = uiInquiryBody ( offer , member . customerId , index , key ( `DRAFT- ${ index } ` ), gray )
if ( gray ) body . estimatedArrivalAt = new Date ( Date . now () + 3600000 ). toISOString ()
const created = await request ( 'POST' , ` ${ POOL } / ${ offer . offerNo } /inquiries` , body , actorTokens [ owner ])
2026-10-07 18:11:55 +08:00
check ( 'UI inquiry is DRAFT with no commercial order or reservation' , created . inquiry . status === 'DRAFT' && ! created . inquiry . businessOrderNo
&& ! created . inquiry . incomingNo && ! created . inquiry . dispatchNo && Number ( created . offer . destinations [ 0 ]. remaining . palletSpaces ) === 12 )
proof . drafts . push ({ bookingNo : created . inquiry . bookingNo , customerId : member . customerId , owner : owner === 0 ? 'SALES_A' : 'SALES_B' })
offer = created . offer
}
const firstPage = await request ( 'GET' , ` ${ POOL } /inquiries?page=1&size=20&scope=ALL&offerNo= ${ offer . offerNo } ` , undefined , actorTokens [ 2 ])
2026-10-08 17:07:57 +08:00
if ( gray ) check ( 'Manager sees exactly four current GRAY UI drafts' , Number ( firstPage . total ) === 4 && firstPage . records . length === 4 )
else {
const secondPage = await request ( 'GET' , ` ${ POOL } /inquiries?page=2&size=20&scope=ALL&offerNo= ${ offer . offerNo } ` , undefined , actorTokens [ 2 ])
check ( 'Manager can exercise real page two of 24 drafts' , Number ( firstPage . total ) === 24 && firstPage . records . length === 20
&& secondPage . records . length === 4 && new Set ([... firstPage . records , ... secondPage . records ]. map ( row => row . bookingNo )). size === 24 )
}
2026-10-07 18:11:55 +08:00
for ( let owner = 0 ; owner < 2 ; owner ++ ) {
const mine = await request ( 'GET' , ` ${ POOL } /inquiries?page=1&size=20&scope=MINE&offerNo= ${ offer . offerNo } ` , undefined , actorTokens [ owner ])
2026-10-08 17:07:57 +08:00
check ( `Each sales login sees exactly its own ${ perSales } drafts` , Number ( mine . total ) === perSales && mine . records . length === perSales
2026-10-07 18:11:55 +08:00
&& mine . records . every ( row => proof . drafts . some ( draft => draft . bookingNo === row . bookingNo && draft . owner === ( owner === 0 ? 'SALES_A' : 'SALES_B' ))))
}
for ( const member of fixture . members ) {
const account = secretDocument . accounts . find ( row => row . kind === 'CUSTOMER' && row . customerId === member . customerId )
check ( 'Customer UI credentials safely preserved' , account ? . username && account . password )
const auth = await request ( 'POST' , '/api/auth/login' , { loginId : account . username , password : account . password }, '' )
const profile = await request ( 'GET' , '/api/merchant/v1/iam/tenant/profile' , undefined , auth . accessToken )
check ( 'Customer login belongs to this fixture customer' , id ( profile . id ) === member . customerId )
for ( const type of [ 'local-looseboard-orders' , 'us-truck-orders' ]) {
const page = await request ( 'GET' , `/api/merchant/v1/last-leg/ ${ type } ?page=1&size=20` , undefined , auth . accessToken )
check ( 'Fresh customer commercial order page provides real empty state before UI confirmation' , Number ( page . total ) === 0 && page . records . length === 0 )
}
account . status = 'ACTIVE' ; writePrivate ()
}
detail = await request ( 'GET' , tripPath )
check ( 'UI fixture has not picked departed or signed cargo' , detail . trip . status === 'PLANNED' )
for ( const member of fixture . members ) {
const dispatch = await request ( 'GET' , ` ${ API } /dispatches/ ${ member . dispatchNo } ` )
check ( 'Original real cargo remains unpicked' , Number ( dispatch . pickedPackageCount ) === 0 && Number ( dispatch . shippedPackageCount ) === 0 )
}
const final = await request ( 'GET' , ` ${ POOL } / ${ offer . offerNo } ` , undefined , actorTokens [ 2 ])
2026-10-08 17:07:57 +08:00
check ( `UI handoff keeps publication OPEN and ${ draftCount } drafts untouched` , final . offer . status === 'OPEN' && final . offer . canCreateInquiry && Number ( final . inquiries . total ) === draftCount )
proof . status = `OPEN_AND_ ${ draftCount } _DRAFTS_READY_FOR_UI`
2026-10-07 18:11:55 +08:00
secretDocument . business = { offerNo : proof . offerNo , tripNo : fixture . tripNo , warehouseId : fixture . warehouseId ,
destinationWarehouseId : config . destinationWarehouseId , cutoffAt : final . offer . cutoffAt ,
2026-10-08 17:07:57 +08:00
plannedDepartureAt : final . offer . plannedDepartureAt , ...( gray ? { uiSchedule : schedule } : {}),
customers : fixture . members . map ( row => ({ customerId : row . customerId , incomingNo : row . incomingNo , dispatchNo : row . dispatchNo })) }
2026-10-07 18:11:55 +08:00
secretDocument . status = 'READY_FOR_UI' ; writePrivate ()
report = { success : true , fixtureReady : true , fullAcceptance : false , proof }
} catch ( error ) {
report = { success : false , fixtureReady : false , fullAcceptance : false , error : publicUiEvidence ( id ( error . message ), secrets ) }
secretDocument . status = 'FAILED_RETAINED' ; writePrivate ()
} finally {
try {
if ( context ) {
const pdaAccount = secretDocument . accounts . find ( row => row . kind === 'FIXTURE_PDA' && row . membershipId )
2026-10-08 17:07:57 +08:00
if ( gray && pdaAccount ) {
pdaAccount . status = 'RETAINED_FOR_UI' ; pdaAccount . purpose = 'GRAY_WAREHOUSE_BOUND_PDA_H5_ACCEPTANCE'
pdaAccount . warehouseId = config . warehouseId
report . retainedPda = { membershipId : pdaAccount . membershipId , warehouseId : config . warehouseId , purpose : pdaAccount . purpose }
} else if ( ! gray && ( picker ? . membershipId || pdaAccount ? . membershipId )) {
2026-10-07 18:11:55 +08:00
try {
const previousSteps = context . steps . length
await disablePicker ( context , picker ? . membershipId ? picker : pdaAccount , 93 )
demand ( context . steps . length > previousSteps && context . steps . at ( - 1 ) ? . passed === true , 'PDA_DISABLE_NOT_CONFIRMED' )
if ( pdaAccount ) { pdaAccount . status = 'DISABLED_AFTER_FIXTURE' ; writePrivate () }
} catch {
if ( pdaAccount ) pdaAccount . status = 'DISABLE_REQUIRES_REVIEW'
report . success = false ; report . cleanupError = 'PDA_DISABLE_REQUIRES_REVIEW'
}
}
let sessionsClosed = true
for ( const [ token , refreshToken ] of sessions ) {
try {
const response = await context . request ( 'POST' , '/api/auth/logout' , { refreshToken }, token )
if ( ! ok ( response ) && response . http !== 401 && id ( response . body ? . code ) !== 'A0401' ) sessionsClosed = false
} catch { sessionsClosed = false }
}
report . sessionsClosed = sessionsClosed ; report . summary = buildAcceptanceSummary ( context . steps , context . modules , context . coverageGaps )
report . success &&= sessionsClosed && report . summary . failed === 0
report . fixtureReady &&= report . success
report . marker = context . marker ; report . createdResources = context . createdResources ; report . steps = context . steps
}
if ( ! report . success ) secretDocument . status = 'FAILED_RETAINED'
writePrivate ()
report . privateCredentialsFile = privateFile ; report . requestCount = requestNumber ; report . inventorySnapshots = inventoryNumber
2026-10-08 17:07:57 +08:00
report . environment = config . environment ; report . runtimeProfile = env . CHONGCHONG_ENV
2026-10-07 18:11:55 +08:00
report . output = output
if ( initialized ) save ( 'report.json' , report )
} finally {
restore ? .(); vault . close ()
if ( lock !== undefined ) { closeSync ( lock ); unlinkSync ( join ( reports , '.fleet-acceptance.lock' )) }
}
}
return report
}
if ( process . argv [ 1 ] && import . meta . url === pathToFileURL ( resolve ( process . argv [ 1 ])). href ) {
try {
const config = parseUiFixtureArguments ( process . argv . slice ( 2 )), result = await runUiFixture ( config )
console . log ( JSON . stringify ( config . execute ? { success : result . success , fixtureReady : result . fixtureReady , fullAcceptance : false ,
error : result . error , summary : result . summary , output : result . output , privateCredentialsFile : result . privateCredentialsFile } : result , null , 2 ))
if ( config . execute && ! result . success ) process . exitCode = 1
} catch { console . error ( 'UI fixture preparation failed; preserve existing private credentials/report and inspect locally.' ); process . exitCode = 1 }
}