317 lines
24 KiB
JavaScript
317 lines
24 KiB
JavaScript
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 } 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'
|
|
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) {
|
|
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.' }
|
|
}
|
|
|
|
/** 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))
|
|
}
|
|
|
|
export function uiInquiryBody(offer, customerId, index, key) {
|
|
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',
|
|
expectedPalletCount: index === 0 ? null : 1,
|
|
cargo: { destinationKey: destination.destinationKey, cargoName: `UI验收意向-${index + 1}`,
|
|
packageCount: index === 0 ? null : 1, palletSpaces: index === 0 ? null : 1, weightKg: 100, volumeCbm: 1,
|
|
priceBasis: 'PALLET', unitPrice: 30, currency: 'CNY', contactName: 'UI验收联系人', contactPhone: '19900000000', remark: '独立UI验收草稿,不占位' },
|
|
idempotencyKey: key }
|
|
}
|
|
|
|
export async function runUiFixture(config, { env = process.env, outputRoot = ROOT, readers,
|
|
prepare = createFleetCrossCustomerFixture, contextFactory = options => new AcceptanceRun(options) } = {}) {
|
|
if (!config.execute) return uiFixturePlan(config)
|
|
demand(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]\d*$/.test(config.warehouseId) && /^[1-9]\d*$/.test(config.destinationWarehouseId)
|
|
&& env.CHONGCHONG_ADMIN_USERNAME && env.CHONGCHONG_ADMIN_PASSWORD, 'EXPLICIT_TEST_FIXTURE_CONFIGURATION_REQUIRED')
|
|
const privateFile = privateCredentialsPath(config.privateCredentialsFile)
|
|
const endpoint = new URL(env.CHONGCHONG_ADMIN_API_BASE_URL)
|
|
demand(['http:', 'https:'].includes(endpoint.protocol) && ['/api', '/api/'].includes(endpoint.pathname)
|
|
&& !endpoint.username && !endpoint.password && !endpoint.hash && !endpoint.search, 'INVALID_TEST_API_BASE')
|
|
readers ||= createTestEvidenceReaders(env)
|
|
const options = validateExecutionOptions({ ...parseArguments(['--modules', 'merchant-qualification-fixture', '--quiet', 'true']),
|
|
environment: 'test', gateway: endpoint.origin, username: env.CHONGCHONG_ADMIN_USERNAME, password: env.CHONGCHONG_ADMIN_PASSWORD,
|
|
markerNamespace: createHash('sha256').update(config.runId).digest('hex').slice(0, 12) })
|
|
const secretDocument = { schemaVersion: 1, environment: 'TEST', runId: config.runId, status: 'PREPARING', createdAt: new Date().toISOString(),
|
|
gatewayOrigin: endpoint.origin, accounts: [], business: {} }
|
|
const vault = createPrivateCredentialStore(privateFile, secretDocument)
|
|
const reports = join(outputRoot, 'reports'), output = join(reports, `fleet-capacity-pool-ui-${config.runId}`)
|
|
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) {
|
|
const sequence = ++requestNumber, [verb, path, body] = args
|
|
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, 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)
|
|
// Keep UI logins useful beyond the producer's short smoke-test window, before its existing six-hour delivery ETA.
|
|
detail = await request('PUT', `${tripPath}/plan`, { recordVersion: detail.trip.recordVersion,
|
|
plannedDepartureAt: new Date(Date.now() + 4 * 3600000).toISOString(), expectedDriverEndAt: detail.trip.expectedDriverEndAt,
|
|
stops: detail.trip.stops, reason: 'UI验收窗口:四小时后计划发车,不执行发车' })
|
|
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}`
|
|
const expected = kind === 'MANAGER' ? [PREFIX + 'manage'] : ['list', 'inquiry-create', 'inquiry-edit', 'inquiry-cancel'].map(name => PREFIX + name)
|
|
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,
|
|
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
|
|
for (let index = 0; index < 24; index++) {
|
|
const owner = index < 12 ? 0 : 1, member = fixture.members[owner]
|
|
const created = await request('POST', `${POOL}/${offer.offerNo}/inquiries`, uiInquiryBody(offer, member.customerId, index, key(`DRAFT-${index}`)), 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])
|
|
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])
|
|
check('Each sales login sees exactly its own twelve drafts', Number(mine.total) === 12 && mine.records.length === 12
|
|
&& 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])
|
|
check('UI handoff keeps publication OPEN and 24 drafts untouched', final.offer.status === 'OPEN' && final.offer.canCreateInquiry && Number(final.inquiries.total) === 24)
|
|
proof.status = 'OPEN_AND_24_DRAFTS_READY_FOR_UI'
|
|
secretDocument.business = { offerNo: proof.offerNo, tripNo: fixture.tripNo, warehouseId: fixture.warehouseId,
|
|
destinationWarehouseId: config.destinationWarehouseId, cutoffAt: final.offer.cutoffAt,
|
|
plannedDepartureAt: final.offer.plannedDepartureAt, 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)
|
|
if (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
|
|
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 }
|
|
}
|