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

976 lines
78 KiB
JavaScript
Raw Normal View History

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 })
}