This commit is contained in:
2026-10-27 17:44:41 +08:00
parent 57cd211973
commit db0e72daeb
37 changed files with 701 additions and 671 deletions
@@ -1,16 +1,17 @@
import { openFixedActor, requireFixedActors, UI_ACTOR_SLOTS } from './admin-test-env-modules/fixed-test-actors.mjs'
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'
import { AcceptanceRun, parseArguments, validateExecutionOptions, buildAcceptanceSummary, createFleetPoolGrayOptions } from './admin-test-env-acceptance.mjs'
import { createHash } from 'node:crypto'
import { AcceptanceRun, parseArguments, validateExecutionOptions, buildAcceptanceSummary, createFleetPoolGrayOptions, grayFixedCustomerOptions } 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 { testPlannedCapacity } from './admin-test-env-modules/fleet-booking-reservation-acceptance.mjs'
import { createTestEvidenceReaders } from './fba-core-regression-evidence.mjs'
import { createGrayEvidenceReaders } from './gray-regression-evidence.mjs'
import { guardFleetWrites, redactFleetWrite, findFleetActorPhone } from './fleet-test-env-acceptance.mjs'
import { guardFleetWrites, redactFleetWrite } 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)), '..')
@@ -139,19 +140,20 @@ async function runUiFixtureCore(config, { env = process.env, outputRoot = ROOT,
const endpoint = new URL(env.CHONGCHONG_ADMIN_API_BASE_URL)
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')
const fixedCustomers = gray ? grayFixedCustomerOptions(env) : {}
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({
const options = gray ? createFleetPoolGrayOptions({ ...transport, ...fixedCustomers }) : 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)
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()
const sessions = 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)
@@ -169,6 +171,8 @@ async function runUiFixtureCore(config, { env = process.env, outputRoot = ROOT,
save('manifest.json', { ...uiFixturePlan(config), sourceSha256: createHash('sha256').update(readFileSync(fileURLToPath(import.meta.url))).digest('hex') })
await readers.preflight()
context = contextFactory(options)
requireFixedActors(context.options, UI_ACTOR_SLOTS)
secrets.push(...Object.values(context.options.fixedActors).map(actor => actor.password), ...(context.options.fixedPdaActors || []).map(actor => actor.password))
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']) {
@@ -177,25 +181,16 @@ async function runUiFixtureCore(config, { env = process.env, outputRoot = ROOT,
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'),
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)
// 固定客户不经过建客回执;只将显式配置且成功登录的既有凭据交接到私有文件。
const customer = [{ customerId: context.options.fixedCustomerId, username: context.options.merchantUsername, password: context.options.merchantPassword }, context.options.fixedSecondaryCustomer]
.find(row => row?.customerId && row.username === body?.loginId && row.password === body?.password)
if (customer) remember({ kind: 'CUSTOMER', customerId: String(customer.customerId).replace(/^TEN/, ''), username: customer.username,
password: customer.password, status: 'REUSED' })
}
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 })
@@ -221,6 +216,10 @@ async function runUiFixtureCore(config, { env = process.env, outputRoot = ROOT,
check('Fixture PDA role exists', Boolean(access))
picker = await createPicker(context, 93, config.warehouseId, access)
check('Fixture PDA authenticated', Boolean(picker?.token))
const configuredPda = context.options.fixedPdaActors?.find(actor => String(actor.membershipId) === String(picker.membershipId))
check('PDA handoff uses the configured existing account', picker.reused && configuredPda?.username === picker.username && configuredPda.password)
remember({ kind: 'FIXTURE_PDA', membershipId: picker.membershipId, username: picker.username, password: configuredPda.password,
warehouseId: config.warehouseId, status: 'REUSED' })
const readInventory = async scope => {
const snapshot = await readers.readFleetInventory(scope); save(`evidence/inventory-${++inventoryNumber}.json`, snapshot); return snapshot
}
@@ -257,25 +256,16 @@ async function runUiFixtureCore(config, { env = process.env, outputRoot = ROOT,
await request('POST', `${tripPath}/capacity-plan`, { recordVersion: plan.recordVersion, manifestHash: plan.manifestHash,
pallets: plannedPallets, reason: '仅UI验收:自有计划18/总30,不作为实际发车计量' })
proof.planEvidence = { actual: false, pallets: plannedPallets, reason: 'TEST UI销售计划18/30,不是实载测量' }
const permissions = await request('GET', `${IAM}/permissions`), actorTokens = []
requireFixedActors(context.options, UI_ACTOR_SLOTS)
const 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}`
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)
const configured = context.options.fixedActors[`pool-ui.${kind}`]
secrets.push(configured.password)
const actor = await openFixedActor(context, `pool-ui.${kind}`, uiActorPermissions(kind, gray))
proof.actors.push({ kind, roleId: actor.roleId, membershipId: actor.membershipId, username: actor.username, reused: true })
remember({ kind, username: actor.username, password: configured.password, membershipId: actor.membershipId, roleIds: [actor.roleId], status: 'REUSED' })
actorTokens.push(actor.token)
}
detail = await request('GET', tripPath)
const draft = await request('POST', POOL, { tripNo: fixture.tripNo, tripRecordVersion: detail.trip.recordVersion,
@@ -316,7 +306,8 @@ async function runUiFixtureCore(config, { env = process.env, outputRoot = ROOT,
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)
check('Existing customer commercial order page is valid without requiring an empty account', Array.isArray(page.records)
&& Number.isSafeInteger(Number(page.total)) && Number(page.total) >= page.records.length)
}
account.status = 'ACTIVE'; writePrivate()
}
@@ -344,8 +335,8 @@ async function runUiFixtureCore(config, { env = process.env, outputRoot = ROOT,
try {
if (context) {
const pdaAccount = secretDocument.accounts.find(row => row.kind === 'FIXTURE_PDA' && row.membershipId)
if (gray && pdaAccount) {
pdaAccount.status = 'RETAINED_FOR_UI'; pdaAccount.purpose = 'GRAY_WAREHOUSE_BOUND_PDA_H5_ACCEPTANCE'
if (picker?.reused && pdaAccount) {
pdaAccount.status = 'RETAINED_FOR_UI'; pdaAccount.purpose = `${config.environment}_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)) {
@@ -353,7 +344,7 @@ async function runUiFixtureCore(config, { env = process.env, outputRoot = ROOT,
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() }
if (pdaAccount) { pdaAccount.status = picker?.reused ? 'REUSED' : 'DISABLED_AFTER_FIXTURE'; writePrivate() }
} catch {
if (pdaAccount) pdaAccount.status = 'DISABLE_REQUIRES_REVIEW'
report.success = false; report.cleanupError = 'PDA_DISABLE_REQUIRES_REVIEW'