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-', environment: 'TEST', cases: ['A', 'B'].map(kind => ({ kind, sourceReport: '', actions: [{ itemId: '', action: 'PARTIAL', finalPickedPackageCount: kind === 'A' ? 2 : 1 }], evidence: Object.fromEntries(TYPES.map(type => [type, ''])), sealNo: '' })) }, execution: 'node --env-file= scripts/partial-loading-actual-test-env.mjs --execute --environment=TEST --run-id= --fixture=' } } 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 }) }