Files
baiyue-firstleg/scripts/partial-loading-actual-test-env.mjs
T

976 lines
78 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict'
import { readFile, mkdir, writeFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { createHash } from 'node:crypto'
import { stableSnapshot } from './admin-test-env-modules/container-customs-acceptance-matrix.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const PDA = '/api/platform/v1/first-leg/pda-loading'
const LEG = '/api/platform/v1/first-leg'
const CUSTOMS = '/api/platform/v1/customs/loading-plans'
const TYPES = ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL']
const id = value => String(value ?? '')
const positive = value => typeof value === 'string' && /^[1-9]\d*$/.test(value)
const rows = value => Array.isArray(value) ? value : []
const sha = value => createHash('sha256').update(value).digest('hex')
class CheckFailure extends Error {}
function need(condition, message) { if (!condition) throw new CheckFailure(message) }
export function plan() {
return { plan: true, networkRequests: 0, businessWrites: false,
cases: ['A: refund PARTIAL 3→2 + same-owner SKIP + buy PICK + skipped refund customer',
'B: only actual buy row PARTIAL 2→1 + all refund rows SKIP; all actual rows PARTIAL'],
upstream: 'Use --fixture=NEW to prepare isolated customers/orders/measurements and two confirmed cabinets, or consume two passing manual-pda-batch-fixture reports',
operations: ['validate both unclaimed tasks before writes', 'claim', 'one atomic batch per task',
'upload five supplied image files', 'complete and poll', 'actual list/metrics/customs scope',
'same original completion replay and zero drift', 'all-buy start/complete and refund-send rejection', 'logout own sessions'],
excluded: ['F01–F40 full regression', 'SQL/deployment', 'historical events or data repair',
'browser/device proof', 'Outbox/ACK/downstream inventory evidence', 'refund document workflow and XLSX download'],
manifest: { schemaVersion: 1, runId: 'PARTIAL-ACTUAL-<new-run>', environment: 'TEST',
cases: ['A', 'B'].map(kind => ({ kind, sourceReport: '<new successful Admin fixture report>',
actions: [{ itemId: '<PDA item id from taskItems>', action: 'PARTIAL', finalPickedPackageCount: kind === 'A' ? 2 : 1 }],
evidence: Object.fromEntries(TYPES.map(type => [type, '<supplied PNG or JPEG path>'])), sealNo: '<nonempty seal>' })) },
execution: 'node --env-file=<authorized TEST test-acceptance.env> scripts/partial-loading-actual-test-env.mjs --execute --environment=TEST --run-id=<new-run> --fixture=<manifest.json>' }
}
export function executionOptions(args, execArgs, env, cwd = process.cwd()) {
if (!args.length || args.length === 1 && args[0] === '--plan') return { execute: false }
need([4,5].includes(args.length) && args[0] === '--execute' && args[1] === '--environment=TEST'
&& /^--run-id=PARTIAL-ACTUAL-[A-Za-z0-9_-]{1,40}$/.test(args[2])
&& args[3].startsWith('--fixture=') && args[3].slice(10), 'Explicit TEST, new PARTIAL-ACTUAL run-id and fixture required')
need(resolve(cwd).replaceAll('\\', '/').toLowerCase() === ROOT.replaceAll('\\', '/').toLowerCase(), 'Run in the TEST backend workspace')
need(execArgs.length === 1 && execArgs[0].startsWith('--env-file=')
&& resolve(execArgs[0].slice(11)).replaceAll('\\', '/').endsWith('/.codex/env/chongchong-global/test-acceptance.env'), 'Use the authorized TEST env-file')
need(String(env.CHONGCHONG_ENV).toUpperCase() === 'TEST', 'TEST profile required')
for (const key of ['CHONGCHONG_ADMIN_API_BASE_URL', 'CHONGCHONG_ADMIN_USERNAME', 'CHONGCHONG_ADMIN_PASSWORD', 'ADMIN_PDA_USERNAME', 'ADMIN_PDA_PASSWORD']) need(Boolean(env[key]), `Missing ${key}`)
const base = new URL(env.CHONGCHONG_ADMIN_API_BASE_URL)
need(['https:', 'http:'].includes(base.protocol) && !base.username && !base.password
&& base.pathname === '/api' && !base.search && !base.hash, 'Configured TEST admin API base must end in /api')
const input = args[3].slice(10)
const completion = input.startsWith('COMPLETION:') || input.startsWith('VERIFY_A:') || input.startsWith('B_ONLY:')
need(completion ? args[4] === '--ready-commit=ea9732749a9215e660530035c40afc69fe56a5ae' : args.length === 4,
'Original-command recovery requires explicit matching controller-ready commit')
return { execute: true, runId: args[2].slice(9), fixture: input === 'NEW' ? 'NEW' : input.startsWith('RESUME:')
? { resumeReport: resolve(input.slice(7)) } : input.startsWith('PREPARED:')
? { preparedReport: resolve(input.slice(9)) } : input.startsWith('B_ONLY:') ? { bOnlyReport: resolve(input.slice(7)) }
: input.startsWith('VERIFY_A:') ? { verifyAReport: resolve(input.slice(9)) }
: completion ? { completionReport: resolve(input.slice(11)) } : resolve(input),
gateway: base.origin, ...(completion ? { readyCommit: args[4].slice(15) } : {}) }
}
export function originalCompletionCommand(failedReport) {
need(failedReport.runId === 'PARTIAL-ACTUAL-20261004-recovery-r7' && failedReport.success === false, 'Bind the original A failed execution')
const attempts = rows(failedReport.requests).filter(item => item.method === 'POST'
&& item.path === `${PDA}/tasks/2106713892758491138/complete`)
need(attempts.length === 1 && attempts[0].state === 'RECEIVED' && attempts[0].http === 200 && attempts[0].code === '200', 'Exactly one accepted original completion command required')
const body = attempts[0].request
assert.deepEqual(body, { expectedTaskVersion: 2, operationId: 'PARTIAL-ACTUAL-20261004-recovery-r3-A-complete', remark: 'PARTIAL-ACTUAL-20261004-recovery-r3-A' }, 'Original immutable completion payload')
const command = attempts[0].response?.data?.completionCommand
need(id(command?.id) === '2106716718935052290' && command.operationId === body.operationId && Number(command.expectedTaskVersion) === 2,
'Original command identity/version mismatch')
return { commandId: id(command.id), body: structuredClone(body) }
}
const dataOf = response => {
need(response.http === 200 && String(response.body?.code) === '200', `API rejected ${response.path}: HTTP=${response.http}, code=${response.body?.code ?? 'missing'}`)
return response.body.data
}
const selected = (value, names) => Object.fromEntries(names.map(name => [name, value?.[name] ?? null]))
export function stableSource(source) {
return { summary: selected(source.summary, ['id','tenantId','orderId','orderNo','inboundCode','processStatus','version','packageCount','totalPackageCount','splitStatus','actualGrossWeightKg','actualVolumeCbm']),
statusHistory: source.statusHistory, palletLocations: source.palletLocations }
}
export async function sourceSnapshot(spec, request) {
const result = []
for (const owner of spec.owners) result.push({ owner, view: dataOf(await request('GET', `/api/platform/inbounds/${owner.inboundId}?tenantId=${owner.tenantId}`)) })
// Failed/completed tasks are not an editable inventory-snapshot surface. The
// source detail API exposes the persisted block/slot/reservation and allocation
// quantities needed to prove these already-completed owners do not settle twice.
return { sources: result, inventory: { allocations: result.map(item => ({ tenantId: item.owner.tenantId,
inboundId: item.owner.inboundId, palletLocations: item.view.palletLocations })) } }
}
export function verifyOldOwnersUnchanged(before, after, expected) {
for (const item of before.sources) {
const current = after.sources.find(row => row.owner.inboundId === item.owner.inboundId)
need(current, 'Original source disappeared from readback')
assert.equal(stableSnapshot(stableSource(current.view)), stableSnapshot(stableSource(item.view)), 'Original owner facts/version/history/resources must not repeat')
const actual = expected.some(row => row.inboundId === item.owner.inboundId && row.actual > 0)
need(actual ? current.view.summary.processStatus === 'LOADED' : current.view.summary.processStatus !== 'LOADED', 'Whole skipped source cannot become LOADED')
}
assert.equal(stableSnapshot(after.inventory), stableSnapshot(before.inventory), 'Original domestic inventory must not be deducted again')
}
export function verifyPhysicalFacts(expected, task) {
const actual = rows(task.items).filter(row => ['PICKED','PARTIAL'].includes(row.itemStatus) && Number(row.pickedPackageCount) > 0)
const expectedActual = expected.filter(row => row.actual > 0)
assert.deepEqual(actual.map(row => id(row.loadingPlanItemId)).sort(), expectedActual.map(row => row.itemId).sort(), 'Original physical item set')
for (const item of expectedActual) {
const row = actual.find(row => id(row.loadingPlanItemId) === item.itemId)
need(Number(row.pickedPackageCount) === item.actual && id(row.tenantId) === item.accountId && id(row.inboundId) === item.inboundId,
'Original physical count/customer/source drift')
}
}
async function readSmallPage(path, request) {
const page = dataOf(await request('GET', path))
need(Array.isArray(page.records) && Number(page.page) === 1 && Number(page.size) === 200
&& Number(page.total) === page.records.length && new Set(page.records.map(row => id(row.id))).size === page.records.length,
'Bounded new-sample page incomplete or duplicated')
return page.records
}
export async function downstreamSnapshot(spec, request) {
const result = []
for (const owner of spec.owners) {
const query = new URLSearchParams({ customerId: owner.tenantId, containerNo: spec.source.containerNo, page: '1', size: '200' })
const orders = await readSmallPage(`/api/platform/v1/last-leg/orders?${query}`, request)
const code = spec.expected.find(row => row.inboundId === owner.inboundId)?.frozen.inboundCode
need(code, 'Frozen original inbound code required for inventory scope')
const inventory = new URLSearchParams({ customerId: owner.tenantId, inboundCode: code, page: '1', size: '200' })
const groupedBalances = await readSmallPage(`/api/platform/v1/last-leg/inventory/balances?${inventory}`, request)
const balances = []
for (const row of groupedBalances) balances.push(dataOf(await request('GET', `/api/platform/v1/last-leg/inventory/balances/${row.id}?customerId=${owner.tenantId}`)))
const ledger = await readSmallPage(`/api/platform/v1/last-leg/inventory/ledger?${inventory}`, request)
const details = []
for (const order of orders) details.push(dataOf(await request('GET', `/api/platform/v1/last-leg/orders/${order.id}?customerId=${owner.tenantId}`)))
result.push({ owner, orders, balances, groupedBalances, ledger, details })
}
return result
}
export function verifyDownstream(spec, snapshot) {
let packages = 0
for (const item of snapshot) {
const actualRows = spec.expected.filter(row => row.inboundId === item.owner.inboundId && row.actual > 0)
const expected = actualRows.reduce((sum, row) => sum + row.actual, 0)
if (!expected) {
need(!item.orders.length && !item.balances.length && !item.ledger.length, 'Skipped owner must have no tail cargo or transit inventory')
continue
}
need(item.orders.length === 1 && item.details.length === 1, 'Exactly one consumer-created tail order per actual source')
const order = item.orders[0], detail = item.details[0]
need(id(order.customerId) === item.owner.tenantId && id(order.sourceInboundId) === item.owner.inboundId
&& id(order.sourceContainerBookingId) === spec.source.containerBookingId && order.containerNo === spec.source.containerNo
&& Number(order.packageCount) === expected, 'Tail materialization must use exact actual customer/source/container boxes')
need(rows(detail.statusHistory).filter(row => row.toStatus === 'PENDING_ASSIGNMENT' && row.remark === '头程装柜完成自动生成').length === 1,
'Exactly one actual loading-event creation history')
need(rows(detail.cargo).length === actualRows.length, 'Skipped SKU cannot enter frozen tail cargo')
for (const row of actualRows) {
const cargo = detail.cargo.find(cargo => cargo.skuCode === row.frozen.skuCode)
need(cargo && Number(cargo.packageCount) === row.actual, 'Tail SKU actual boxes mismatch')
}
const sum = (values, field) => values.reduce((total, row) => total + Number(row[field]), 0)
need(item.balances.every(row => row.customerId === item.owner.tenantId && row.inboundCode === rowCode(actualRows) && id(row.inboundOrderId) === id(order.id)
&& Number(row.availablePackages) === 0 && Number(row.holdPackages) === 0 && Number(row.allocatedPackages) === 0
&& Number(row.pickedPackages) === 0 && Number(row.receivingPackages) === 0 && Number(row.procurementPackages) === 0)
&& sum(item.balances, 'inTransitPackages') === expected && sum(item.balances, 'totalPackages') === 0,
'Initial transit inventory must equal actual boxes only')
need(item.ledger.length === actualRows.length && sum(item.ledger, 'deltaPackages') === expected
&& item.ledger.every(row => Number(row.beforePackages) === 0 && Number(row.afterPackages) === Number(row.deltaPackages)
&& row.customerId === item.owner.tenantId && row.inboundCode === rowCode(actualRows) && id(row.inboundOrderId) === id(order.id) && row.businessOrderNo === order.orderNo),
'One initial inventory movement per actual SKU; no duplicate or skipped movement')
packages += expected
}
return { actualPackages: packages, loadingEventMaterializationVerified: true, directOutboxAckAuditVerified: false }
}
const rowCode = actualRows => actualRows[0].frozen.inboundCode
async function waitDownstream(spec, request, sleep) {
for (let attempt = 0; attempt < 30; attempt++) {
const snapshot = await downstreamSnapshot(spec, request)
const expectedOrders = spec.expected.filter(row => row.actual > 0).map(row => row.inboundId)
const active = [...new Set(expectedOrders)]
if (snapshot.reduce((sum, item) => sum + item.orders.length, 0) === active.length) return { snapshot, facts: verifyDownstream(spec, snapshot) }
await sleep(1000)
}
throw new CheckFailure('Loading event materialization did not converge; no source/event replay')
}
export async function recoverOriginalA(spec, original, request, checkpoint, sleep = ms => new Promise(done => setTimeout(done, ms))) {
const path = `${PDA}/tasks/${spec.source.pdaTaskId}`
const task = dataOf(await request('GET', `${path}?warehouseId=${spec.source.warehouseId}`, undefined, 'pda'))
need(task.taskStatus === 'COMPLETION_FAILED' && task.completionCommand?.status === 'FAILED'
&& id(task.completionCommand.id) === original.commandId && task.completionCommand.operationId === original.body.operationId
&& Number(task.completionCommand.expectedTaskVersion) === original.body.expectedTaskVersion, 'Original failed command no longer matches; do not issue another command')
verifyPhysicalFacts(spec.expected, task)
const before = await sourceSnapshot(spec, request)
for (const item of before.sources) {
const actual = spec.expected.some(row => row.inboundId === item.owner.inboundId && row.actual > 0)
need(actual ? item.view.summary.processStatus === 'LOADED' : item.view.summary.processStatus !== 'LOADED', 'Original completed/whole-skipped owner baseline mismatch')
}
await checkpoint({ stage: 'A_BEFORE_ORIGINAL_RECOVERY', task, original, before })
try { dataOf(await request('POST', `${path}/complete`, original.body, 'pda')) }
catch (error) {
const readback = await request('GET', `${path}?warehouseId=${spec.source.warehouseId}`, undefined, 'pda')
await checkpoint({ stage: 'A_RECOVERY_REJECTED_OR_UNKNOWN_READBACK', readback })
throw error // no blind repetition of a write with an uncertain result
}
let completed
for (let attempt = 0; attempt < 60; attempt++) {
completed = dataOf(await request('GET', `${path}?warehouseId=${spec.source.warehouseId}`, undefined, 'pda'))
if (completed.taskStatus === 'COMPLETED' && completed.workflowStatus === 'COMPLETED') break
need(completed.taskStatus === 'COMPLETING', `Original recovery failed: ${completed.completionCommand?.failureStage}/${completed.completionCommand?.failureCode}`)
await sleep(1000)
}
need(completed.taskStatus === 'COMPLETED' && id(completed.completionCommand.id) === original.commandId, 'Same original command must finish')
verifyPhysicalFacts(spec.expected, completed)
const after = await sourceSnapshot(spec, request); verifyOldOwnersUnchanged(before, after, spec.expected)
const detail = dataOf(await request('GET', `${LEG}/loading-lists/${spec.source.loadingPlanId}?warehouseId=${spec.source.warehouseId}`))
const customs = dataOf(await request('GET', `${CUSTOMS}/${spec.source.loadingPlanId}`))
const planState = dataOf(await request('GET', `${LEG}/loading-plans/${spec.source.loadingPlanId}/lifecycle?warehouseId=${spec.source.warehouseId}`))
const facts = verifyActual(spec.expected, detail, customs)
need(planState.detailResource === 'LOADING_LIST' && planState.planStatus === 'COMPLETED', 'Recovered plan must expose completed loading-list lifecycle')
const downstream = await waitDownstream(spec, request, sleep)
await checkpoint({ stage: 'A_AFTER_ORIGINAL_RECOVERY', completed, after, detail, customs, planState, downstream })
const operations = dataOf(await request('GET', `${path}/operations`, undefined, 'pda'))
need(rows(operations).filter(row => row.actionType === 'COMPLETE_LOADING').length === 1, 'Recovered completion audit must be unique')
return { kind: 'A', ...facts, ...downstream.facts, originalCommandId: original.commandId,
oldOwnersAndDomesticInventoryUnchanged: true, wholeSkippedOwnerNotLoaded: true, completed: true }
}
export async function verifyRecoveredA(spec, original, before, request, checkpoint, sleep = ms => new Promise(done => setTimeout(done, ms))) {
const path = `${PDA}/tasks/${spec.source.pdaTaskId}`
const completed = dataOf(await request('GET', `${path}?warehouseId=${spec.source.warehouseId}`, undefined, 'pda'))
need(completed.taskStatus === 'COMPLETED' && completed.workflowStatus === 'COMPLETED'
&& completed.completionCommand?.status === 'COMPLETED' && id(completed.completionCommand.id) === original.commandId
&& completed.completionCommand.operationId === original.body.operationId
&& Number(completed.completionCommand.expectedTaskVersion) === original.body.expectedTaskVersion, 'Same recovered original command must remain completed')
verifyPhysicalFacts(spec.expected, completed)
const after = await sourceSnapshot(spec, request); verifyOldOwnersUnchanged(before, after, spec.expected)
const detail = dataOf(await request('GET', `${LEG}/loading-lists/${spec.source.loadingPlanId}?warehouseId=${spec.source.warehouseId}`))
const customs = dataOf(await request('GET', `${CUSTOMS}/${spec.source.loadingPlanId}`))
const plan = dataOf(await request('GET', `${LEG}/loading-plans/${spec.source.loadingPlanId}/lifecycle?warehouseId=${spec.source.warehouseId}`))
const facts = verifyActual(spec.expected, detail, customs)
need(plan.detailResource === 'LOADING_LIST' && plan.planStatus === 'COMPLETED', 'Completed original plan must remain completed')
const downstream = await waitDownstream(spec, request, sleep)
const operations = dataOf(await request('GET', `${path}/operations`, undefined, 'pda'))
need(rows(operations).filter(row => row.actionType === 'COMPLETE_LOADING').length === 1, 'One original completion audit')
await checkpoint({ stage: 'A_READONLY_VERIFIED_AFTER_ORIGINAL_RECOVERY', completed, after, detail, customs, plan, downstream, operations })
return { kind: 'A', ...facts, ...downstream.facts, originalCommandId: original.commandId, readOnlyVerification: true,
oldOwnersAndDomesticAllocationSnapshotsUnchanged: true, wholeSkippedOwnerNotLoaded: true, completed: true }
}
export function validateZeroWorkTask(source, task, ownUserId) {
need(id(task.id) === source.pdaTaskId && id(task.loadingPlanId) === source.loadingPlanId
&& id(task.containerBookingId) === source.containerBookingId && task.currentContainerNo === source.containerNo
&& task.taskStatus === 'LOADING' && Number(task.version) === Number(source.beforeTaskVersion), 'Original task identity/version changed')
need(task.workflowStatus === 'UNASSIGNED' && task.assignment == null
|| task.workflowStatus === 'IN_PROGRESS' && id(task.assignment?.assigneeId) === ownUserId,
'Task must be unassigned or assigned to this own PDA session')
need(rows(task.items).length > 0 && rows(task.items).every(item => item.itemStatus === 'PENDING'
&& Number(item.pickedPackageCount) === 0), 'Prepared task already has physical work')
}
export function validatePreparedResume(report) {
need(report.runId === 'PARTIAL-ACTUAL-20261004-recovery-r3' && report.success === false && rows(report.cases).length === 0
&& report.recovery?.originalOwner?.inboundId === '2106706513799155714' && report.recovery?.originalSha256,
'Only this original-source two-cabinet preparation may continue')
need(!rows(report.requests).some(item => item.method === 'POST' && /\/pda-loading\/tasks\/\d+\/(claim|batch|complete|evidence)$/.test(item.path)),
'Preparation continuation cannot repeat any PDA work')
const prepared = rows(report.preparation).filter(item => item.stage === 'PDA_UNASSIGNED')
need(prepared.length === 2 && prepared[0].kind === 'A' && prepared[1].kind === 'B'
&& prepared.every(item => item.source?.sourceMarker === `${report.runId}-${item.kind}` && item.spec?.kind === item.kind), 'Exactly the prepared A/B checkpoints required')
return prepared
}
export function multipartBody(file) {
if (file instanceof FormData) return file
need(file?.content !== undefined, 'Multipart descriptor requires content bytes')
const content = file.content instanceof Uint8Array ? file.content : new Uint8Array(file.content)
const form = new FormData()
form.append('file', new Blob([content], { type: file.type || 'application/octet-stream' }), file.name || 'fixture.bin')
return form
}
export function requestInit(method, body, token, extraHeaders = {}) {
const headers = new Headers({ Accept: 'application/json', ...extraHeaders })
if (token) headers.set('Authorization', `Bearer ${token}`)
if (body instanceof FormData) headers.delete('Content-Type') // fetch supplies the actual boundary
else if (body !== undefined) headers.set('Content-Type', 'application/json')
return { method, redirect: 'error', signal: AbortSignal.timeout(30_000), headers,
body: body === undefined ? undefined : body instanceof FormData ? body : JSON.stringify(body) }
}
export function multipartAdapter(request, roleFor) {
return (method, path, file, token, headers = {}) => request(method, path, multipartBody(file), roleFor(token), headers)
}
export function validateResumeProof(report, readback) {
const owner = { tenantId: '2106706477225971713', orderId: '2106706495895306242', inboundId: '2106706513799155714',
orderNo: 'FL2106706495895306242', inboundCode: 'NYEXX2610040025', marker: 'PARTIAL-ACTUAL-20261004-r1-A-REFUND' }
need(report.runId === 'PARTIAL-ACTUAL-20261004-r1' && report.success === false && rows(report.cases).length === 0,
'Only the explicitly authorized failed original run may resume')
const rejected = rows(report.requests).filter(item => item.path === `/api/platform/v1/files/measurements/${owner.tenantId}/${owner.inboundId}`)
need(rejected.length === 1 && rejected[0].http === 200 && rejected[0].code === 'S0500' && rejected[0].requestId, 'Original one rejected upload required')
const summary = readback.inbound?.summary, measurement = readback.measurement
need(readback.verified === true && readback.sessionClosed === true && id(summary?.id) === owner.inboundId
&& id(summary?.tenantId) === owner.tenantId && id(summary?.orderId) === owner.orderId && summary?.orderNo === owner.orderNo
&& summary?.inboundCode === owner.inboundCode && summary?.processStatus === 'WAIT_MEASURE'
&& measurement?.measurementStatus === 'DRAFT' && rows(measurement.weightResults).length === 0, 'Frozen readback must match original WAIT_MEASURE/DRAFT without weights')
return owner
}
export function validateResumeLive(owner, frozen, inbound, measurement) {
const summary = inbound?.summary
need(id(summary?.id) === owner.inboundId && id(summary?.tenantId) === owner.tenantId && id(summary?.orderId) === owner.orderId
&& summary?.orderNo === owner.orderNo && summary?.inboundCode === owner.inboundCode
&& summary?.processStatus === 'WAIT_MEASURE' && measurement?.measurementStatus === frozen.measurement.measurementStatus
&& ['DRAFT', 'RECEIVED'].includes(measurement?.measurementStatus)
&& rows(measurement.weightResults).length === 0 && id(measurement.measurementId) === id(frozen.measurement.measurementId)
&& measurement.version === frozen.measurement.version && summary.version === frozen.inbound.summary.version,
'Original sample changed; stop before any recovery write')
}
export async function resumeMeasurementDraft(context, owner, initial) {
const root = `${LEG}/inbounds/${owner.inboundId}`, query = `tenantId=${owner.tenantId}`
const ok = response => { need(response.http === 200 && String(response.body?.code) === '200', `Original measurement rejected ${response.path}`); return response.body.data }
const bytes = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64')
const uploaded = ok(await context.requestMultipart('POST', `/api/platform/v1/files/measurements/${owner.tenantId}/${owner.inboundId}`,
{ content: bytes, type: 'image/png', name: `recovery-weight-${owner.inboundId}.png` }))
need(uploaded.fileId, 'Original recovery upload did not return a file')
const declarations = rows(initial.latestDeclaration?.items)
need(declarations.length === 2 && declarations.map(item => Number(item.packageCount)).sort().join(',') === '1,3', 'Original two-SKU declaration must remain 3+1')
// Save the two declared SKU identities separately. The generic single-SKU fixture
// had aggregated all four boxes into the first detail; no carton scanning is repeated.
const draft = ok(await context.request('PUT', `${root}/measurement/draft?${query}`, {
measurementId: initial.measurementId, version: initial.version, actualGoodsName: '原样本两SKU恢复测量',
items: declarations.map(item => ({ inboundDetailId: item.inboundDetailId, itemNo: String(item.itemNo),
boxSpecification: item.cargoName, packageCount: Number(item.packageCount),
lengthCm: Number(item.lengthCm), widthCm: Number(item.widthCm), heightCm: Number(item.heightCm),
intelligentImageFileId: uploaded.fileId, coefficientMode: 'ACTUAL', remark: owner.marker })),
}))
const options = ok(await context.request('GET', `${root}/weight-pallet-options?${query}&page=1&size=20&keyword=YW-TP-002`))
const pallet = rows(options.records).find(item => item.palletCode === 'YW-TP-002')
need(pallet?.palletId, 'Original source warehouse needs the existing YW-TP-002 pallet')
const weight = ok(await context.request('POST', `${root}/weight-results?${query}`, { fileId: id(uploaded.fileId),
palletId: id(pallet.palletId), ocrProvider: 'ADMIN_ACCEPTANCE', ocrRawText: null, ocrWeightValue: null, ocrWeightUnit: null, confidence: null }))
const confirmed = ok(await context.request('POST', `${root}/weight-results/${weight.id}/confirm?${query}`, {
version: weight.version, adjustedWeightValue: 100, adjustedWeightUnit: 'KG', adjustmentReason: `${owner.marker} HTTP人工称重夹具` }))
need(confirmed.recognitionStatus === 'CONFIRMED', 'Original source weight must be confirmed')
const current = ok(await context.request('GET', `${root}/measurement?${query}`))
need(id(current.measurementId) === id(draft.measurementId) && current.measurementStatus === 'DRAFT', 'Original draft identity retained')
const submitted = ok(await context.request('POST', `${root}/measurement/submit?${query}`, { measurementId: current.measurementId, version: current.version }))
need(submitted.processStatus === 'MEASURE_PENDING', 'Original draft must enter actual measurement review')
const pending = ok(await context.request('GET', `/api/platform/inbounds/${owner.inboundId}?${query}`))
ok(await context.request('POST', `/api/platform/v1/inbounds/${owner.inboundId}/measurements/approve?${query}`,
{ measurementId: current.measurementId, expectedVersion: pending.summary.version, reason: null }, context.token,
{ 'Idempotency-Key': `${owner.marker}-recovery-approve` }))
let final
for (let attempt = 0; attempt < 30; attempt++) {
final = ok(await context.request('GET', `${root}/measurement?${query}`))
if (final.processStatus === 'WAIT_SPLIT' && final.measurementStatus === 'APPROVED') break
await new Promise(done => setTimeout(done, 1000))
}
need(final.processStatus === 'WAIT_SPLIT' && final.measurementStatus === 'APPROVED', 'Original recovery did not reach WAIT_SPLIT')
need(rows(final.items).length === 2 && rows(final.items).every(item => declarations.some(declaration =>
id(declaration.inboundDetailId) === id(item.inboundDetailId) && Number(declaration.packageCount) === Number(item.packageCount))), 'Original SKU measurements retain declaration identities and 3+1 counts')
context.createdResources.inboundMeasurement = { marker: owner.marker, tenantId: owner.tenantId, inboundId: owner.inboundId,
orderId: owner.orderId, inboundCode: owner.inboundCode, measurementId: id(final.measurementId),
finalStatus: final.measurementStatus, processStatus: final.processStatus }
}
export function redact(value) {
if (Array.isArray(value)) return value.map(redact)
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, child]) =>
[key, /password|token|secret|authorization/i.test(key) ? '[REDACTED]' : redact(child)]))
return value
}
// This is a bounded producer, not a run of the full acceptance module list.
// The three reused modules only create qualified customer + submitted/approved/measured order.
export async function prepareFreshCases(runId, context, savePrepared, resume = null) {
const { run: qualification } = await import('./admin-test-env-modules/merchant-qualification-fixture.mjs')
const { run: orders } = await import('./admin-test-env-modules/orders.mjs')
const { run: measurement } = await import('./admin-test-env-modules/inbound-measurement.mjs')
const { selectSessionPdaOperator } = await import('./admin-test-env-modules/domestic-pda-operator.mjs')
const { publishPlatformLogisticsChannels, resolveOceanBookingAgent } = await import('./admin-test-env-modules/ocean-booking-fixture.mjs')
const { ensureMatsonCarrierPartner } = await import('./admin-test-env-modules/carrier-partner-fixture.mjs')
const { channelGroup } = await import('./admin-test-env-modules/smart-loading-channel-group.mjs')
const { iso6346ContainerNumber } = await import('./admin-test-env-modules/iso6346-container-number.mjs')
const ok = response => { need(response.http === 200 && String(response.body?.code) === '200', `Fresh fixture rejected ${response.path}`); return response.body.data }
const produced = []
for (const [kind, definitions] of [['A', [['REFUND', 'TAX_REFUND', [3,1]], ['BUY', 'BUY_DECLARATION', [1]], ['SKIP', 'TAX_REFUND', [1]]]],
['B', [['BUY', 'BUY_DECLARATION', [2]], ['SKIP', 'TAX_REFUND', [1]]]]]) {
const owners = []
for (const [role, taxMode, counts] of definitions) {
context.marker = `${runId}-${kind}-${role}`
context.context = { orderProfile: { taxMode, fulfillmentType: 'STANDARD' }, orderFulfillmentType: 'STANDARD',
waitSplitVariant: { skuCount: counts.length, packageCounts: counts, packageCountPerSku: counts[0] } }
context.createdResources = {}
const originalRecovery = resume && kind === 'A' && role === 'REFUND'
let originalMeasurement
if (originalRecovery) {
const original = resume.owner
const inbound = ok(await context.request('GET', `/api/platform/inbounds/${original.inboundId}?tenantId=${original.tenantId}`))
const measurementView = ok(await context.request('GET', `${LEG}/inbounds/${original.inboundId}/measurement?tenantId=${original.tenantId}`))
validateResumeLive(original, resume.readback, inbound, measurementView)
context.marker = original.marker
context.createdResources.orders = { ...original, receivingWarehouseId: id(inbound.summary.warehouseId),
fulfillmentType: 'STANDARD', taxMode: 'TAX_REFUND' }
await savePrepared({ stage: 'ORIGINAL_RECOVERY_CHECKED', kind, role, ...original, measurementId: measurementView.measurementId,
inboundVersion: inbound.summary.version, measurementVersion: measurementView.version })
originalMeasurement = measurementView
} else {
await qualification(context, { publishEnvironment: false })
await orders(context)
}
await savePrepared({ stage: 'APPROVED_ORDER', kind, role, tenantId: id(context.createdResources.orders?.tenantId),
inboundId: id(context.createdResources.orders?.inboundId), orderId: id(context.createdResources.orders?.orderId) })
if (originalRecovery) await resumeMeasurementDraft(context, resume.owner, originalMeasurement)
else await measurement(context)
const source = context.createdResources.inboundMeasurement
need(source?.inboundId && source.processStatus === 'WAIT_SPLIT' && source.finalStatus === 'APPROVED', 'New legal measurement did not reach WAIT_SPLIT')
owners.push({ role, taxMode, counts, tenantId: id(source.tenantId), inboundId: id(source.inboundId),
orderId: id(source.orderId || context.createdResources.orders?.orderId), orderNo: context.createdResources.orders?.orderNo,
inboundCode: source.inboundCode || context.createdResources.orders?.inboundCode })
await savePrepared({ stage: 'WAIT_SPLIT', kind, owners: structuredClone(owners) })
}
context.marker = `${runId}-${kind}`
const primary = owners[0]
const inbound = ok(await context.request('GET', `/api/platform/inbounds/${primary.inboundId}?tenantId=${primary.tenantId}`))
const warehouseId = id(inbound.summary.warehouseId), channelCode = inbound.summary.channelCode
need(warehouseId && channelCode, 'Fresh source warehouse/channel missing')
await publishPlatformLogisticsChannels(context, warehouseId, 'partial-loading-prepare')
const agent = await resolveOceanBookingAgent(context, 'partial-loading-prepare', 'MATSON', { deploymentBlocker: true })
const carrier = await ensureMatsonCarrierPartner(context, 'partial-loading-prepare', { deploymentBlocker: true })
need(agent?.id && carrier?.id, 'Existing booking master data required')
const container = ok(await context.request('POST', '/api/platform/v1/containers', {
channelGroupCode: channelGroup(channelCode), warehouseId, containerType: '40HQ', capacityCbm: 68, maxWeightKg: 19500 }))
const bookingId = id(container.id)
let smartPlan = ok(await context.request('GET', `/api/platform/v1/first-leg/smart-loading/plans/by-booking/${bookingId}`))
const planId = id(smartPlan.id)
await savePrepared({ stage: 'EDITING', kind, owners, containerBookingId: bookingId, loadingPlanId: planId })
const candidateResponse = ok(await context.request('GET', `/api/platform/v1/first-leg/smart-loading/candidates?bookingId=${bookingId}&warehouseId=${warehouseId}`))
const candidates = rows(candidateResponse)
for (const owner of owners) {
const own = candidates.filter(item => id(item.inboundId) === owner.inboundId && id(item.accountId) === owner.tenantId)
.sort((a,b) => id(a.inboundDetailId).localeCompare(id(b.inboundDetailId)))
need(own.length === owner.counts.length, 'Fresh owner candidate SKU set mismatch')
for (const candidate of own) {
const packages = Number(candidate.availablePackageCount)
need(owner.counts.includes(packages), 'Candidate count differs from fresh declared boxes')
smartPlan = ok(await context.request('POST', `/api/platform/v1/first-leg/smart-loading/plans/${planId}/assignments`, {
inboundId: owner.inboundId, inboundDetailId: id(candidate.inboundDetailId), packageCount: packages,
expectedVersion: Number(smartPlan.version), operationId: `${context.marker}-assign-${candidate.inboundDetailId}` }))
}
}
const booking = ok(await context.request('POST', '/api/platform/v1/ocean-bookings', { expectedVersion: 0, data: {
channelCode: channelGroup(channelCode), warehouseId, billOfLadingNo: `BL-${context.marker}`,
shippingCompanyId: id(carrier.id), shippingCompanyName: carrier.name, carrierCode: 'MATSON',
bookingAgentId: id(agent.id), bookingAgentName: agent.name, vesselName: `测试船-${context.marker}`, voyageNo: 'PA-TEST',
departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶', placeOfDelivery: 'LAX',
estimatedSailingAt: '2026-10-05T08:00:00', estimatedArrivalAt: '2026-10-25T08:00:00', remark: context.marker,
parties: ['SHIPPER','CONSIGNEE','NOTIFY_PARTY'].map(partyType => ({ partyType, partyName: `${context.marker}-${partyType}` })),
containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', quantity: 1, capacityCbm: 68, maxWeightKg: 19500 }],
} }, context.token, { 'Idempotency-Key': `${context.marker}-booking` }))
const oceanId = id(booking.oceanBooking.id), form = new FormData()
form.append('expectedVersion', String(booking.oceanBooking.version)); form.append('documentType', 'BOOKING_CONFIRMATION')
form.append('file', new Blob([`${context.marker} controlled TEST booking confirmation`], { type: 'text/plain' }), `${context.marker}-booking.txt`)
const document = ok(await context.requestMultipart('POST', `/api/platform/v1/ocean-bookings/${oceanId}/documents/upload`, form,
context.token, { 'Idempotency-Key': `${context.marker}-booking-file` }))
const released = ok(await context.request('POST', `/api/platform/v1/ocean-bookings/${oceanId}/release`,
{ expectedVersion: Number(document.bookingVersion), reason: context.marker }, context.token, { 'Idempotency-Key': `${context.marker}-release` }))
need(released.oceanBooking.status === 'RELEASED', 'New booking release required')
const currentContainer = ok(await context.request('GET', `/api/platform/v1/containers/domestic/${bookingId}`)).container
const containerNo = iso6346ContainerNumber(context.marker)
ok(await context.request('POST', `/api/platform/v1/containers/${bookingId}/ocean-booking-link`, {
oceanBookingId: oceanId, requirementId: id(released.containerRequirements[0].id), containerNo, expectedVersion: currentContainer.version }))
const preload = ok(await context.request('GET', `${LEG}/loading-plans/${planId}?warehouseId=${warehouseId}`))
const operator = selectSessionPdaOperator(ok(await context.request('GET', `/api/platform/v1/iam/users/domestic-pda-operators?warehouseId=${warehouseId}`)), context.pdaToken)
const confirmation = ok(await context.request('POST', `${LEG}/loading-plans/${planId}/confirm`,
{ expectedVersion: Number(preload.summary.version), assigneeId: id(operator.userId) }, context.token, { 'Idempotency-Key': `${context.marker}-confirm` }))
const taskId = id(confirmation.readiness.pdaTaskId)
const task = ok(await context.request('GET', `${PDA}/tasks/${taskId}?warehouseId=${warehouseId}`, undefined, context.pdaToken))
const source = { schemaVersion: 1, sourceMarker: context.marker, preparedAt: new Date().toISOString(), stage: 'MANUAL_PDA_CLAIM_AND_BATCH_READY',
pdaTaskId: taskId, loadingPlanId: planId, containerBookingId: bookingId, warehouseId, containerNo,
cabinetNo: container.cabinetNo, oceanBookingId: oceanId, bookingNo: released.oceanBooking.bookingNo, planNo: smartPlan.planNo,
beforeTaskVersion: task.version, taskItems: task.items.map(item => ({ id: id(item.id), loadingPlanItemId: id(item.loadingPlanItemId),
tenantId: id(item.tenantId), inboundId: id(item.inboundId), inboundDetailId: id(item.inboundDetailId), plannedPackageCount: item.plannedPackageCount })) }
const actions = task.items.map(item => {
const owner = owners.find(owner => owner.inboundId === id(item.inboundId))
need(owner, 'Task includes only fresh produced owners')
const partial = owner === primary && Number(item.plannedPackageCount) === (kind === 'A' ? 3 : 2)
return { itemId: id(item.id), action: partial ? 'PARTIAL' : owner.role === 'BUY' ? 'PICK' : 'SKIP',
...(partial ? { finalPickedPackageCount: kind === 'A' ? 2 : 1 } : {}) }
})
const spec = { kind, actions, sealNo: `SEAL-${context.marker}` }
const freshPreload = ok(await context.request('GET', `${LEG}/loading-plans/${planId}?warehouseId=${warehouseId}`))
expectedRows(spec, task, freshPreload)
produced.push({ spec, source, owners })
await savePrepared({ stage: 'PDA_UNASSIGNED', kind, source, owners, spec })
}
return produced
}
export function validateManifest(manifest, runId) {
need(manifest?.schemaVersion === 1 && manifest.environment === 'TEST' && manifest.runId === runId, 'Fixture run/environment mismatch')
need(rows(manifest.cases).length === 2 && manifest.cases[0].kind === 'A' && manifest.cases[1].kind === 'B', 'Exactly A then B required')
for (const item of manifest.cases) {
need(typeof item.sourceReport === 'string' && item.sourceReport.length > 0, 'New official fixture source report required')
need(rows(item.actions).length >= 2 && new Set(item.actions.map(row => row.itemId)).size === item.actions.length, 'Every PDA item needs one unique action')
for (const action of item.actions) need(positive(action.itemId) && ['PICK', 'PARTIAL', 'SKIP'].includes(action.action)
&& (action.action !== 'PARTIAL' || Number.isSafeInteger(action.finalPickedPackageCount) && action.finalPickedPackageCount > 0), 'Invalid PDA action')
need(typeof item.sealNo === 'string' && item.sealNo.trim().length > 0 && item.sealNo.length <= 64, 'Seal required')
need(TYPES.every(type => typeof item.evidence?.[type] === 'string' && item.evidence[type]), 'Five supplied evidence paths required')
}
return manifest
}
export function fixtureSource(report, runId, gateway, now = Date.now()) {
const value = report?.createdResources?.manualPdaBatchFixture
need(report.success === true && report.fatal == null && report.summary?.failed === 0
&& report.gateway?.replace(/\/$/, '') === gateway, 'Official fixture report did not pass in this TEST Gateway')
need(value?.schemaVersion === 1 && value.stage === 'MANUAL_PDA_CLAIM_AND_BATCH_READY'
&& typeof value.sourceMarker === 'string' && value.sourceMarker.startsWith(`${runId}-`)
&& /^[A-Za-z0-9_-]{1,80}$/.test(value.sourceMarker), 'Fresh run-bound PDA preparation required')
const age = now - Date.parse(value.preparedAt)
need(Number.isFinite(age) && age >= 0 && age <= 24 * 3600_000, 'Fixture must be prepared within this run/day')
for (const key of ['pdaTaskId', 'loadingPlanId', 'containerBookingId', 'warehouseId']) need(positive(value[key]), `Invalid source ${key}`)
need(rows(value.taskItems).length > 0, 'Missing original PDA identities')
return value
}
// Decimal arithmetic is exact; no binary floating point at HALF_UP boundaries.
export function scale(value, actual, planned, precision) {
if (value == null) return null
const text = String(value)
need(/^\d+(?:\.\d+)?$/.test(text) && Number.isSafeInteger(actual) && actual > 0
&& Number.isSafeInteger(planned) && planned >= actual, 'Invalid nonnegative frozen decimal/ratio')
const [whole, fraction = ''] = text.split('.')
const numerator = BigInt(whole + fraction) * BigInt(actual) * 10n ** BigInt(precision)
const denominator = BigInt(planned) * 10n ** BigInt(fraction.length)
const rounded = (2n * numerator + denominator) / (2n * denominator)
const padded = rounded.toString().padStart(precision + 1, '0')
return precision ? `${padded.slice(0, -precision)}.${padded.slice(-precision)}` : padded
}
export function expectedRows(caseSpec, task, preload) {
const byId = new Map(rows(task.items).map(item => [id(item.id), item]))
need(byId.size === task.items.length && byId.size === caseSpec.actions.length
&& caseSpec.actions.every(item => byId.has(item.itemId)), 'Actions must cover every live PDA item exactly once')
const planById = new Map(rows(preload.items).map(item => [id(item.itemId), item]))
need(planById.size === byId.size, 'Preload and PDA scope mismatch')
const expected = caseSpec.actions.map(action => {
const pick = byId.get(action.itemId), frozen = planById.get(id(pick.loadingPlanItemId))
need(frozen && id(frozen.accountId) === id(pick.tenantId) && id(frozen.inboundId) === id(pick.inboundId)
&& id(frozen.inboundDetailId) === id(pick.inboundDetailId), 'Frozen source/customer/detail identity mismatch')
const planned = Number(pick.plannedPackageCount)
need(pick.itemStatus === 'PENDING' && Number(pick.pickedPackageCount) === 0 && pick.unitType === 'PACKAGE'
&& !pick.palletId && planned > 0 && Number(frozen.packageCount) === planned, 'Require zero-picked PACKAGE sources')
const actual = action.action === 'SKIP' ? 0 : action.action === 'PICK' ? planned : action.finalPickedPackageCount
need(action.action !== 'PARTIAL' || actual < planned, 'PARTIAL must be less than original plan')
return { action: action.action, pickId: action.itemId, itemId: id(frozen.itemId), accountId: id(frozen.accountId),
inboundId: id(frozen.inboundId), inboundDetailId: id(frozen.inboundDetailId), taxMode: frozen.customsTaxMode,
planned, actual, frozen, orderId: id(frozen.orderId) }
})
const partial = expected.filter(item => item.action === 'PARTIAL'), full = expected.filter(item => item.action === 'PICK')
const skipped = expected.filter(item => item.action === 'SKIP'), active = expected.filter(item => item.actual > 0)
need(expected.every(item => ['TAX_REFUND', 'BUY_DECLARATION'].includes(item.taxMode)), 'Authoritative tax mode required')
if (caseSpec.kind === 'A') {
need(partial.length === 1 && partial[0].taxMode === 'TAX_REFUND' && partial[0].planned === 3 && partial[0].actual === 2
&& full.length === 1 && full[0].taxMode === 'BUY_DECLARATION' && full[0].accountId !== partial[0].accountId
&& skipped.some(item => item.inboundId === partial[0].inboundId)
&& skipped.some(item => item.taxMode === 'TAX_REFUND' && !active.some(row => row.accountId === item.accountId)), 'A needs mixed customers, partial refund, same-source skip and entirely skipped refund customer')
} else {
need(active.length === 1 && partial.length === 1 && full.length === 0
&& partial[0].taxMode === 'BUY_DECLARATION' && partial[0].planned === 2 && partial[0].actual === 1
&& skipped.length > 0 && skipped.every(item => item.taxMode === 'TAX_REFUND'), 'B must have exactly one actual BUY PARTIAL row and only skipped refund rows')
}
return expected
}
export function verifyActual(expected, detail, customs) {
const active = expected.filter(item => item.actual > 0)
assert.deepEqual(rows(detail.items).map(item => id(item.itemId)).sort(), active.map(item => item.itemId).sort(), 'Actual loading row set')
for (const item of active) {
const actual = detail.items.find(row => id(row.itemId) === item.itemId), frozen = item.frozen
assert.equal(Number(actual.packageCount), item.actual, 'Actual boxes')
assert.equal(id(actual.accountId), item.accountId, 'Actual customer')
assert.equal(id(actual.inboundId), item.inboundId, 'Actual source')
const units = Number(frozen.unitsPerPackage)
const quantity = item.actual === item.planned ? Number(frozen.productQuantity)
: units > 0 ? units * item.actual : Number(scale(frozen.productQuantity, item.actual, item.planned, 0))
assert.equal(Number(actual.productQuantity), quantity, 'Actual units')
for (const [field, precision] of [['grossWeightKg', 3], ['netWeightKg', 3], ['dimensionalWeightKg', 3], ['volumeCbm', 4]])
assert.equal(scale(actual[field], 1, 1, precision), scale(frozen[field], item.actual, item.planned, precision), `Actual ${field}`)
}
const boxes = active.reduce((sum, item) => sum + item.actual, 0)
assert.equal(Number(detail.summary.totalPackageCount), boxes, 'Actual total boxes')
assert.equal(Number(customs.actualBoxCount), boxes, 'Customs actual boxes')
const inboundKeys = values => [...new Set(values.map(item => `${id(item.tenantId ?? item.accountId)}:${id(item.inboundId)}`))].sort()
assert.deepEqual(inboundKeys(rows(customs.inbounds)), inboundKeys(active), 'Customs source/customer set')
assert.equal(Number(customs.taxRefundCount), new Set(active.filter(item => item.taxMode === 'TAX_REFUND').map(item => item.inboundId)).size, 'Refund coverage excludes skipped rows')
assert.equal(Number(detail.summary.totalQuantity), detail.items.reduce((sum, item) => sum + Number(item.productQuantity), 0), 'Quantity total')
for (const [field, summary, precision] of [['grossWeightKg', 'totalGrossWeightKg', 3], ['netWeightKg', 'totalNetWeightKg', 3], ['volumeCbm', 'totalVolumeCbm', 4]]) {
const sum = detail.items.reduce((value, item) => value + BigInt(scale(item[field] ?? 0, 1, 1, precision).replace('.', '')), 0n)
assert.equal(BigInt(scale(detail.summary[summary] ?? 0, 1, 1, precision).replace('.', '')), sum, `Actual ${summary}`)
}
return { actualBoxes: boxes, actualRows: active.length, taxRefundCount: Number(customs.taxRefundCount) }
}
export async function runCase(spec, source, prepared, request, sleep = ms => new Promise(done => setTimeout(done, ms))) {
const { expected, task, images } = prepared
const path = `${PDA}/tasks/${source.pdaTaskId}`, key = suffix => `${source.sourceMarker}-${suffix}`.slice(0, 128)
const data = response => { need(response.http === 200 && String(response.body?.code) === '200', `API rejected ${response.path}: HTTP=${response.http}, code=${response.body?.code ?? 'missing'}`); return response.body.data }
const readTask = async () => data(await request('GET', `${path}?warehouseId=${source.warehouseId}`, undefined, 'pda'))
const claimed = data(await request('POST', `${path}/claim`, { expectedAssignmentVersion: Number(task.assignment?.version ?? 0), operationId: key('claim') }, 'pda'))
need(claimed.workflowStatus === 'IN_PROGRESS', 'Claim must enter IN_PROGRESS')
let picked
// Batch must contain every still-pending row, and forbids SKIP + physical picks
// for one owner in the same command. Resolve that owner's zero-picked SKU through
// the formal individual SKIP first; then one atomic batch handles all remaining rows.
const individualSkips = expected.filter(item => item.action === 'SKIP'
&& expected.some(other => other.inboundId === item.inboundId && other.actual > 0))
for (const item of individualSkips) {
const current = await readTask(), row = current.items.find(row => id(row.id) === item.pickId)
data(await request('POST', `${path}/items/${item.pickId}/skip`, { warehouseId: source.warehouseId,
version: Number(row.version), operationId: key(`skip-${item.pickId}`), reason: source.sourceMarker }, 'pda'))
}
const lines = expected.filter(item => !individualSkips.some(skip => skip.pickId === item.pickId))
{
const current = await readTask()
const inventory = data(await request('GET', `${path}/inventory-snapshot?warehouseId=${source.warehouseId}`, undefined, 'pda'))
const batch = { warehouseId: source.warehouseId, loadingPlanId: source.loadingPlanId, containerBookingId: source.containerBookingId,
expectedVersion: Number(current.version), inventorySnapshotToken: inventory.snapshotToken,
items: lines.map(item => ({ itemId: item.pickId, action: item.action,
version: Number(current.items.find(row => id(row.id) === item.pickId)?.version),
...(item.action === 'PARTIAL' ? { finalPickedPackageCount: item.actual } : item.action === 'PICK' ? { pickedPackageCount: item.actual } : { reason: source.sourceMarker }) })) }
need(inventory.snapshotToken && batch.items.every(item => Number.isSafeInteger(item.version)), 'Fresh inventory and item versions required')
picked = data(await request('POST', `${path}/batch`, batch, 'pda', { 'Idempotency-Key': key('resolved-batch') }))
}
need(picked.workflowStatus === 'AWAITING_REVIEW' && expected.every(item => {
const row = rows(picked.items).find(row => id(row.id) === item.pickId)
return row && Number(row.pickedPackageCount) === item.actual && row.itemStatus === ({ PICK: 'PICKED', PARTIAL: 'PARTIAL', SKIP: 'SKIPPED' })[item.action]
}), 'Resolved picks and all-PARTIAL B must be persisted before completion')
for (const type of TYPES) {
const image = images[type], form = new FormData()
for (const [name, value] of Object.entries({ warehouseId: source.warehouseId, expectedVersion: 0, evidenceType: type,
remark: source.sourceMarker, ...(type === 'SEAL' ? { sealNo: spec.sealNo } : {}) })) form.append(name, String(value))
form.append('file', new Blob([image.bytes], { type: image.type }), `${source.sourceMarker}-${type}.${image.type === 'image/png' ? 'png' : 'jpg'}`)
data(await request('POST', `${path}/evidence`, form, 'pda', { 'Idempotency-Key': key(`evidence-${type}`) }))
}
const evidence = data(await request('GET', `${path}/evidence?warehouseId=${source.warehouseId}`, undefined, 'pda'))
need(TYPES.every(type => rows(evidence.evidences).some(item => item.evidenceType === type && item.status === 'ACTIVE'
&& (type !== 'SEAL' || item.sealNo === spec.sealNo))), 'Five active evidence types and seal required')
const ready = await readTask(), body = { expectedTaskVersion: Number(ready.version), operationId: key('complete'), remark: source.sourceMarker }
data(await request('POST', `${path}/complete`, body, 'pda'))
let completed
for (let attempt = 0; attempt < 60; attempt++) {
completed = await readTask()
if (completed.taskStatus === 'COMPLETED' && completed.workflowStatus === 'COMPLETED') break
need(completed.taskStatus === 'COMPLETING', `Completion stopped: task=${completed.taskStatus}, command=${completed.completionCommand?.status ?? 'missing'}, stage=${completed.completionCommand?.failureStage ?? 'missing'}, code=${completed.completionCommand?.failureCode ?? 'missing'}`)
await sleep(1000)
}
need(completed.taskStatus === 'COMPLETED' && completed.workflowStatus === 'COMPLETED', 'Completion polling exhausted')
const snapshot = async () => ({ task: await readTask(),
detail: data(await request('GET', `${LEG}/loading-lists/${source.loadingPlanId}?warehouseId=${source.warehouseId}`)),
customs: data(await request('GET', `${CUSTOMS}/${source.loadingPlanId}`)),
operations: data(await request('GET', `${path}/operations`, undefined, 'pda')) })
const before = await snapshot(), facts = verifyActual(expected, before.detail, before.customs)
need(rows(before.operations).filter(item => item.actionType === 'COMPLETE_LOADING').length === 1, 'One COMPLETE_LOADING audit required')
data(await request('POST', `${path}/complete`, body, 'pda')) // original key, version and body
const after = await snapshot()
assert.equal(stableSnapshot(after), stableSnapshot(before), 'Exact completion replay has zero projection/version/history drift')
if (spec.kind === 'B') {
need(before.customs.taxRefundCount === 0 && before.customs.status === 'READY_TO_START', 'Actual all-buy readiness')
const start = data(await request('POST', `${CUSTOMS}/${source.loadingPlanId}/start`,
{ expectedVersion: before.customs.version, confirm: true, remark: source.sourceMarker }, 'admin', { 'Idempotency-Key': key('customs-start') }))
need(start.status === 'DECLARING', 'All-buy start')
const reject = await request('POST', `${CUSTOMS}/${source.loadingPlanId}/send-to-broker`,
{ expectedVersion: start.version, confirm: true, remark: source.sourceMarker }, 'admin', { 'Idempotency-Key': key('refund-send-negative') })
need(reject.http === 200 && String(reject.body?.code) === 'A0409', 'All-buy must reject refund send')
const unchanged = data(await request('GET', `${CUSTOMS}/${source.loadingPlanId}`))
assert.equal(stableSnapshot(unchanged), stableSnapshot(start), 'Rejected refund send leaves customs unchanged')
const result = data(await request('POST', `${CUSTOMS}/${source.loadingPlanId}/complete-all-buy`,
{ expectedVersion: unchanged.version, confirm: true, remark: source.sourceMarker }, 'admin', { 'Idempotency-Key': key('customs-complete') }))
need(result.status === 'COMPLETED', 'All-buy completion')
}
return { kind: spec.kind, ...facts, source, actions: spec.actions, allActualRowsPartial: expected.filter(item => item.actual > 0).every(item => item.action === 'PARTIAL'),
completed: true, completionReplayZeroDrift: true }
}
export async function execute(args = process.argv.slice(2), execArgs = process.execArgv, env = process.env) {
const options = executionOptions(args, execArgs, env)
if (!options.execute) return plan()
const directory = resolve(ROOT, 'reports', `partial-loading-actual-${options.runId}`)
const bOnlyMode = typeof options.fixture === 'object' && options.fixture.bOnlyReport
const verifyAMode = typeof options.fixture === 'object' && options.fixture.verifyAReport
const completionMode = typeof options.fixture === 'object' && (options.fixture.completionReport || options.fixture.verifyAReport)
const preparedMode = typeof options.fixture === 'object' && options.fixture.preparedReport
const resumeMode = typeof options.fixture === 'object' && options.fixture.resumeReport
const fresh = options.fixture === 'NEW' || resumeMode
let resume
if (resumeMode) {
const originalBytes = await readFile(options.fixture.resumeReport)
const readbackPath = resolve(dirname(options.fixture.resumeReport), 'blocked-readback.json')
const readbackBytes = await readFile(readbackPath), readback = JSON.parse(readbackBytes)
const original = JSON.parse(originalBytes)
resume = { owner: validateResumeProof(original, readback), readback,
originalReport: options.fixture.resumeReport, originalSha256: sha(originalBytes), readbackSha256: sha(readbackBytes) }
const attemptedPath = resolve(ROOT, 'reports/partial-loading-actual-PARTIAL-ACTUAL-20261004-recovery-r1/report.json')
let attemptedBytes
try { attemptedBytes = await readFile(attemptedPath) } catch (error) { if (error.code !== 'ENOENT') throw error }
if (attemptedBytes) {
const attempted = JSON.parse(attemptedBytes)
const mutations = rows(attempted.requests).filter(item => item.method !== 'GET' && !item.path.startsWith('/api/auth/') && !item.path.includes('carton-label-pdf'))
need(attempted.success === false && attempted.recovery?.originalSha256 === resume.originalSha256
&& mutations.length === 1 && mutations[0].path === `${LEG}/inbounds/${resume.owner.inboundId}/measurement/carton-scans?tenantId=${resume.owner.tenantId}`
&& mutations[0].http === 200 && mutations[0].code === '200' && mutations[0].response.data.created === false
&& mutations[0].response.data.measurementVersion === readback.measurement.version + 1,
'Prior recovery must prove its sole accepted original re-scan; no blind retry')
resume.readback = structuredClone(readback)
resume.readback.measurement.version = mutations[0].response.data.measurementVersion
resume.readback.measurement.measurementStatus = 'RECEIVED' // the proven scan changed this draft into RECEIVED
resume.priorRecovery = { report: attemptedPath, sha256: sha(attemptedBytes),
knownOriginalRescanRequestId: mutations[0].requestId, expectedMeasurementVersion: resume.readback.measurement.version,
expectedMeasurementStatus: 'RECEIVED' }
}
}
const manifestBytes = bOnlyMode ? await readFile(options.fixture.bOnlyReport) : completionMode ? await readFile(options.fixture.completionReport || options.fixture.verifyAReport) : preparedMode ? await readFile(options.fixture.preparedReport) : fresh ? Buffer.from(resumeMode ? 'RESUME' : 'NEW') : await readFile(options.fixture)
const manifest = fresh || preparedMode || completionMode || bOnlyMode ? null : validateManifest(JSON.parse(manifestBytes), options.runId)
const sources = [], specs = []
let original
let originalBefore
let aEventProof
if (bOnlyMode) {
const eventProofBytes = await readFile(resolve(ROOT, 'reports/partial-A-completion-event-proof-20261004-r1.json'))
const eventProof = JSON.parse(eventProofBytes)
need(eventProof.readOnly === true && eventProof.verified === true && Object.values(eventProof.checks).every(value => value === true)
&& rows(eventProof.rows).length === 2 && rows(eventProof.rows).every(row => row.status === 'PROCESSED' && row.eventVersion === 3),
'Controller exact A Outbox/ACK/Inbox proof required before B')
aEventProof = { report: 'reports/partial-A-completion-event-proof-20261004-r1.json', sha256: sha(eventProofBytes), checks: eventProof.checks, events: eventProof.rows }
let evidence = JSON.parse(manifestBytes)
need(evidence.success === true && evidence.cases?.length === 1 && evidence.cases[0].kind === 'A'
&& evidence.cases[0].originalCommandId === '2106716718935052290' && evidence.cases[0].actualPackages === 3
&& evidence.cases[0].oldOwnersAndDomesticAllocationSnapshotsUnchanged === true && evidence.cases[0].wholeSkippedOwnerNotLoaded === true,
'B requires the verified original A recovery first')
for (let index = 0; index < 2; index++) {
const parent = await readFile(evidence.completionRecovery.originalReport)
need(sha(parent) === evidence.completionRecovery.originalSha256, 'A evidence chain SHA changed')
evidence = JSON.parse(parent)
}
originalCompletionCommand(evidence)
const preparationBytes = await readFile(evidence.preparedRecovery.report)
need(sha(preparationBytes) === evidence.preparedRecovery.sha256, 'Original prepared B SHA changed')
const preparation = JSON.parse(preparationBytes), checkpoint = validatePreparedResume(preparation)[1]
const taskPath = `${PDA}/tasks/${checkpoint.source.pdaTaskId}?warehouseId=${checkpoint.source.warehouseId}`
const preloadPath = `${LEG}/loading-plans/${checkpoint.source.loadingPlanId}?warehouseId=${checkpoint.source.warehouseId}`
const initialTask = rows(preparation.requests).filter(row => row.path === taskPath && row.method === 'GET').at(-1).response.data
const initialPreload = rows(preparation.requests).filter(row => row.path === preloadPath && row.method === 'GET').at(-1).response.data
const bytes = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64')
specs.push({ ...checkpoint, expected: expectedRows(checkpoint.spec, initialTask, initialPreload),
images: Object.fromEntries(TYPES.map(type => [type, { bytes, type: 'image/png', sha256: sha(bytes) }])) }); sources.push(checkpoint.source)
}
if (completionMode) {
let failed = JSON.parse(manifestBytes)
if (verifyAMode) {
originalBefore = rows(failed.checkpoints).find(row => row.stage === 'A_BEFORE_ORIGINAL_RECOVERY')?.before
need(originalBefore && failed.completionRecovery?.originalReport, 'Original recovery before snapshot required')
const originalBytes = await readFile(failed.completionRecovery.originalReport)
need(sha(originalBytes) === failed.completionRecovery.originalSha256, 'Original failed report SHA changed')
failed = JSON.parse(originalBytes)
}
original = originalCompletionCommand(failed)
const preparationBytes = await readFile(failed.preparedRecovery.report)
need(sha(preparationBytes) === failed.preparedRecovery.sha256, 'Original preparation report SHA changed')
const preparation = JSON.parse(preparationBytes)
for (const checkpoint of validatePreparedResume(preparation)) {
const taskPath = `${PDA}/tasks/${checkpoint.source.pdaTaskId}?warehouseId=${checkpoint.source.warehouseId}`
const preloadPath = `${LEG}/loading-plans/${checkpoint.source.loadingPlanId}?warehouseId=${checkpoint.source.warehouseId}`
const initialTask = rows(preparation.requests).filter(row => row.path === taskPath && row.method === 'GET').at(-1)?.response?.data
const initialPreload = rows(preparation.requests).filter(row => row.path === preloadPath && row.method === 'GET').at(-1)?.response?.data
specs.push({ ...checkpoint, expected: expectedRows(checkpoint.spec, initialTask, initialPreload) }); sources.push(checkpoint.source)
}
}
if (preparedMode) {
const bytes = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64')
for (const checkpoint of validatePreparedResume(JSON.parse(manifestBytes))) {
specs.push({ ...checkpoint, images: Object.fromEntries(TYPES.map(type => [type, { bytes, type: 'image/png', sha256: sha(bytes) }])) })
sources.push(checkpoint.source)
}
}
for (const spec of manifest?.cases || []) {
const sourceBytes = await readFile(resolve(dirname(options.fixture), spec.sourceReport))
const source = fixtureSource(JSON.parse(sourceBytes), options.runId, options.gateway)
const images = {}
for (const type of TYPES) {
const bytes = await readFile(resolve(dirname(options.fixture), spec.evidence[type]))
const png = bytes.subarray(0, 8).equals(Buffer.from([137,80,78,71,13,10,26,10]))
const jpeg = bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255
need(bytes.length > 16 && bytes.length <= 20 * 1024 * 1024 && (png || jpeg), 'Evidence must be supplied bounded PNG/JPEG bytes')
images[type] = { bytes, type: png ? 'image/png' : 'image/jpeg', sha256: sha(bytes) }
}
sources.push(source); specs.push({ spec, source, sourceSha256: sha(sourceBytes), images })
}
if (!fresh && !bOnlyMode) {
for (const key of ['pdaTaskId', 'loadingPlanId', 'containerBookingId']) need(new Set(sources.map(value => value[key])).size === 2, 'Two independent new cabinet identities required')
need(!sources[0].taskItems.some(a => sources[1].taskItems.some(b => id(a.inboundId) === id(b.inboundId))), 'Cases cannot share source inbound resources')
}
await mkdir(directory) // exclusive; never overwrite evidence or silently rerun business writes
const report = { success: false, runId: options.runId, startedAt: new Date().toISOString(), manifestSha256: sha(manifestBytes), requests: [], cases: [], sessions: {},
...(bOnlyMode ? { BContinuation: { verifiedAReport: options.fixture.bOnlyReport, verifiedASha256: sha(manifestBytes), readyCommit: options.readyCommit, aEventProof }, syntheticHttpEvidence: true } : {}),
...(completionMode ? { completionRecovery: { originalReport: options.fixture.completionReport || options.fixture.verifyAReport, originalSha256: sha(manifestBytes), original, readyCommit: options.readyCommit, BDeferred: true, readOnly: Boolean(verifyAMode) } } : {}),
...(preparedMode ? { preparedRecovery: { report: options.fixture.preparedReport, sha256: sha(manifestBytes) }, syntheticHttpEvidence: true } : {}),
...(resume ? { recovery: { originalReport: resume.originalReport, originalSha256: resume.originalSha256,
readbackSha256: resume.readbackSha256, originalOwner: resume.owner, originalCreationSkipped: true, priorRecovery: resume.priorRecovery } } : {}),
scope: completionMode ? ['direct Outbox/ACK audit', 'raw Warehouse SQL audit', 'browser/device acceptance', 'B deferred in this dispatch'] : plan().excluded,
fixtures: specs.map(({ spec, source, sourceSha256, images }) => ({ kind: spec.kind, source, sourceSha256,
...(images ? { imageHashes: Object.fromEntries(TYPES.map(type => [type, images[type].sha256])) } : {}) })) }
const sessions = {}, save = () => writeFile(resolve(directory, 'report.json'), JSON.stringify(redact(report), null, 2))
const nativeFetch = globalThis.fetch
let requestQueue = Promise.resolve(), transportUnknown = false
const serialFetch = (url, init) => {
const pending = requestQueue.then(() => { need(!transportUnknown || init.method === 'GET' || new URL(url).pathname === '/api/auth/logout', 'Earlier unknown transport result; stop new commands and preserve samples'); return nativeFetch(url, init) })
requestQueue = pending.then(() => {}, () => { transportUnknown = true })
return pending
}
const recordSession = (payload, role) => {
if (payload?.data?.accessToken) {
const already = Object.entries(sessions).find(([, value]) => value.token === payload.data.accessToken)
const name = already?.[0] || (role === 'admin' || role === 'pda' ? role : `customer-${Object.keys(sessions).length}`)
sessions[name] = { token: payload.data.accessToken, refresh: payload.data.refreshToken }
}
}
async function request(method, path, body, role = 'admin', headers = {}) {
need(!verifyAMode || method === 'GET' || ['/api/auth/login','/api/auth/logout'].includes(path), 'A post-recovery verification is read-only')
const auth = path.startsWith('/api/auth/')
const entry = { method, path, role, intendedAt: new Date().toISOString(), state: 'PENDING',
...(auth ? {} : body instanceof FormData ? { multipart: true } : body === undefined ? {} : { request: body }),
idempotencyKey: headers['Idempotency-Key'] }
if (body instanceof FormData) {
entry.multipartParts = []
for (const [name, value] of body.entries()) entry.multipartParts.push(typeof value === 'string' ? { name, value }
: { name, fileName: value.name, size: value.size, type: value.type, sha256: sha(new Uint8Array(await value.arrayBuffer())) })
}
report.requests.push(entry); await save()
try {
const response = await serialFetch(options.gateway + path, requestInit(method, body, sessions[role]?.token, headers))
const payload = await response.json()
if (path === '/api/auth/login') recordSession(payload, role)
Object.assign(entry, { state: 'RECEIVED', http: response.status, code: payload.code, requestId: payload.requestId, traceId: payload.traceId,
...(auth ? {} : { response: payload }) }); await save()
return { method, path, http: response.status, body: payload, requestId: payload.requestId, traceId: payload.traceId }
} catch { entry.state = 'UNKNOWN'; await save(); throw new CheckFailure(`Transport/response failed: ${method} ${path}; preserve original request, do not auto-retry`) }
}
try {
for (const [role, username, password] of [['admin', env.CHONGCHONG_ADMIN_USERNAME, env.CHONGCHONG_ADMIN_PASSWORD], ['pda', env.ADMIN_PDA_USERNAME, env.ADMIN_PDA_PASSWORD]]) {
const response = await request('POST', '/api/auth/login', { loginId: username, password }, role)
const data = response.body?.data
if (data?.accessToken) sessions[role] = { token: data.accessToken, refresh: data.refreshToken }
need(response.http === 200 && String(response.body?.code) === '200' && data?.accessToken && data?.refreshToken, 'Incomplete own login session')
report.sessions[role] = { loggedIn: true, closed: false }
}
if (fresh) {
// Reused producers have a few raw binary/multipart fetches. Instrument them too,
// serialize through the same queue, persist intent first, and never expose credentials.
globalThis.fetch = async (url, init = {}) => {
const parsed = new URL(url); need(parsed.origin === options.gateway, 'Producer network must stay in TEST Gateway')
const entry = { method: init.method || 'GET', path: parsed.pathname + parsed.search, producerRaw: true, intendedAt: new Date().toISOString(), state: 'PENDING' }
const form = init.body instanceof FormData
if (form) {
entry.multipart = []
for (const [name, value] of init.body.entries()) entry.multipart.push(typeof value === 'string'
? { name, value } : { name, size: value.size, type: value.type, sha256: sha(new Uint8Array(await value.arrayBuffer())) })
} else if (init.body) { try { entry.request = redact(JSON.parse(init.body)) } catch { entry.requestSha256 = sha(String(init.body)) } }
report.requests.push(entry); await save()
let response
try { response = await serialFetch(url, { ...init, redirect: 'error' }) }
catch { entry.state = 'UNKNOWN'; await save(); throw new CheckFailure(`Producer transport unknown: ${entry.method} ${entry.path}`) }
const bytes = new Uint8Array(await response.clone().arrayBuffer())
entry.http = response.status; entry.state = 'RECEIVED'
if ((response.headers.get('content-type') || '').includes('json')) {
const payload = JSON.parse(new TextDecoder().decode(bytes)); entry.code = payload.code
entry.requestId = payload.requestId; entry.traceId = payload.traceId; entry.response = redact(payload)
if (parsed.pathname === '/api/auth/login') recordSession(payload, 'producer')
} else { entry.size = bytes.length; entry.sha256 = sha(bytes) }
await save(); return response
}
const { AcceptanceRun, parseArguments } = await import('./admin-test-env-acceptance.mjs')
const context = new AcceptanceRun({ ...parseArguments(['--modules','orders,inbound-measurement','--quiet','true']), gateway: options.gateway,
username: env.CHONGCHONG_ADMIN_USERNAME, password: env.CHONGCHONG_ADMIN_PASSWORD, customerName: '', waitSplitCount: 1,
routeMode: 'fixed', orderProfileMode: 'fixed' })
context.token = sessions.admin.token; context.pdaToken = sessions.pda.token
const originalStep = context.addStep.bind(context)
context.addStep = (...args) => { const passed = originalStep(...args); if (!passed) throw new CheckFailure(`Fresh producer gate failed: ${args[0]} / ${args[1]}`); return passed }
const roleFor = token => token ? Object.entries(sessions).find(([, value]) => value.token === token)?.[0] || 'admin' : 'producer'
context.request = (method, path, body, token = context.token, headers = {}) => {
need(method === 'GET' || !/^\/api\/platform\/v1\/(suppliers|business-partners|ocean-bookings\/channels)/.test(path), 'Shared booking master data writes require controller coordination')
return request(method, path, body, roleFor(token), headers)
}
const adapter = multipartAdapter(request, roleFor)
context.requestMultipart = (method, path, file, token = context.token, headers = {}) => adapter(method, path, file, token, headers)
const freshCases = await prepareFreshCases(options.runId, context, async checkpoint => {
report.preparation ||= []; report.preparation.push(checkpoint); await save()
}, resume)
globalThis.fetch = nativeFetch
const bytes = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64')
report.syntheticHttpEvidence = true // controlled HTTP fixture; never camera/device proof
for (const value of freshCases) specs.push({ ...value, images: Object.fromEntries(TYPES.map(type => [type, { bytes, type: 'image/png', sha256: sha(bytes) }])) })
report.fixtures = specs.map(({ spec, source, owners }) => ({ kind: spec.kind, source, owners }))
await save()
}
const ok = response => { need(response.http === 200 && String(response.body?.code) === '200', `Preflight API rejected ${response.path}`); return response.body.data }
const ownPdaId = JSON.parse(Buffer.from(sessions.pda.token.split('.')[1], 'base64url').toString()).sub
need(positive(ownPdaId), 'Own PDA subject required')
// Both cabinets are checked before the first claim/batch/evidence/complete write.
if (completionMode) {
const checkpoint = async snapshot => {
report.checkpoints ||= []; report.checkpoints.push(snapshot); await save()
}
report.cases.push(verifyAMode ? await verifyRecoveredA(specs[0], original, originalBefore, request, checkpoint)
: await recoverOriginalA(specs[0], original, request, checkpoint))
report.success = true // this dispatch is A-only; B is explicitly deferred and untouched
} else {
for (const item of specs) {
const { source, spec } = item, path = `${PDA}/tasks/${source.pdaTaskId}`
const task = ok(await request('GET', `${path}?warehouseId=${source.warehouseId}`, undefined, 'pda'))
validateZeroWorkTask(source, task, ownPdaId)
const originalIds = source.taskItems.map(row => id(row.id)).sort()
assert.deepEqual(task.items.map(row => id(row.id)).sort(), originalIds, 'Source fixture item set')
const preload = ok(await request('GET', `${LEG}/loading-plans/${source.loadingPlanId}?warehouseId=${source.warehouseId}`))
need(['EDITING', 'LOADING'].includes(preload.summary.status), 'Zero-pick preload must be EDITING or own claimed LOADING')
const operations = ok(await request('GET', `${path}/operations`, undefined, 'pda'))
need(!rows(operations).some(row => ['BATCH', 'COMPLETE_LOADING'].includes(row.actionType))
&& rows(operations).filter(row => row.actionType === 'CLAIM').every(row => row.operationId === `${source.sourceMarker}-claim`),
'No earlier physical picking/completion or foreign claim allowed')
const evidence = ok(await request('GET', `${path}/evidence?warehouseId=${source.warehouseId}`, undefined, 'pda'))
need(!rows(evidence.evidences).some(row => row.status === 'ACTIVE'), 'New task must have empty active evidence baseline')
item.task = task; item.expected = expectedRows(spec, task, preload)
}
for (const item of specs) {
const result = await runCase(item.spec, item.source, item, request)
if (bOnlyMode) {
const downstream = await waitDownstream(item, request, ms => new Promise(done => setTimeout(done, ms)))
result.downstream = downstream.facts; report.BAfter = { source: await sourceSnapshot(item, request), downstream }
}
report.cases.push(result); await save()
}
report.success = report.cases.length === (bOnlyMode ? 1 : 2)
}
} catch (error) {
report.failure = error instanceof CheckFailure || error instanceof assert.AssertionError ? error.message : 'Local response/fixture format failure'
} finally {
globalThis.fetch = nativeFetch
// A transport-unknown business command stops all further business writes;
// own session logout is still allowed and does not replay that command.
transportUnknown = false
for (const [role, session] of Object.entries(sessions).reverse()) {
try {
const response = await request('POST', '/api/auth/logout', { refreshToken: session.refresh }, role)
const closed = response.http === 200 && String(response.body?.code) === '200'
report.sessions[role] = { loggedIn: true, closed }; if (!closed) report.success = false
} catch { report.sessions[role] = { loggedIn: true, closed: false }; report.success = false }
}
report.finishedAt = new Date().toISOString(); await save()
}
return { success: report.success, report: resolve(directory, 'report.json') }
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
execute().then(result => { console.log(JSON.stringify(result)); if (result.success === false) process.exitCode = 1 })
.catch(() => { console.error('Partial-loading acceptance did not start; check TEST env-file, new run-id and legal fixture manifest.'); process.exitCode = 1 })
}