Files
baiyue-firstleg/scripts/fleet-capacity-pool-ui-fixture-test-env.mjs
T

384 lines
29 KiB
JavaScript
Raw Normal View History

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'
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'
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.' }
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 }
}
/** 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) {
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,
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,
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) {
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)
&& 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`)
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(),
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
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]
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' })
}
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.
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)
}
}
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)
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(),
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])
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)
}
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
&& 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`
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 })) }
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)) {
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
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 }
}