test(fleet): cover separate customers sharing one physical trip
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
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'
|
||||
|
||||
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',
|
||||
'unready customer gate rejection', 'shared departure and replay', 'independent receipts'],
|
||||
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,
|
||||
contextFactory = options => new AcceptanceRun(options), pickerAccess = resolvePickerAccess, pickerCreate = createPicker, pickerDisable = disablePicker } = {}) {
|
||||
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)
|
||||
await verify(context, fixture, { execute: true, readInventory })
|
||||
report = { success: context.createdResources.fleetCrossCustomer?.status === 'VERIFIED' }
|
||||
} 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 }
|
||||
}
|
||||
Reference in New Issue
Block a user