test(pda): bind receiving fixtures to exact source container booking

This commit is contained in:
2026-10-08 21:36:53 +08:00
parent 6b6d217774
commit 4f9433c39a
2 changed files with 144 additions and 31 deletions
@@ -7,7 +7,102 @@ import { fileURLToPath } from 'node:url'
import { expandOverseasCartonCandidates, resolveCartonFixtures, run, uniqueContainerProducer,
crossContainerCartonProbe, normalizeReceivingEvidence, receivingEvidenceValid,
receivingCompletionProof, captureReceivingEvidence, receivingEventRecoveryInventoryUnchanged,
probeCartonResolveNegatives, awaitReceivingCandidate } from './phase4-overseas-pda.mjs'
probeCartonResolveNegatives, awaitReceivingCandidate, convertReceivingContainers,
awaitProducerConsumption, awaitPickupReadyCandidates } from './phase4-overseas-pda.mjs'
function conversionContext({ existing = false, eta = null, wrongIdentity = false } = {}) {
const bookingId = '2108183323464437762', historicalId = '2099376270288678913'
const producer = { containerNo: 'TSTU1800264', containerBookingId: bookingId,
resourceKey: 'THIS-RUN-P4-FBA-SECOND:FBA:TSTU1800264' }
const summary = { containerNo: producer.containerNo, sourceContainerBookingId: wrongIdentity ? historicalId : bookingId,
totalOrderCount: 1, inboundCodeCount: 1, missingInboundCodeCount: 0,
planCount: existing ? 1 : 0, pendingPlanCount: existing ? 0 : 1, allPlansReady: existing }
const historical = { ...summary, sourceContainerBookingId: historicalId, planCount: 1, pendingPlanCount: 0, allPlansReady: true }
const plans = [{ lines: [{ inboundCode: 'THIS-RUN-INBOUND' }], pallets: [{ palletNo: 'PLT-THIS-RUN', warehousePalletId: '88' }] }]
const calls = [], steps = []
let converted = existing
const context = { marker: 'THIS-RUN', token: 'offline', calls, steps,
createdResources: { phase4OverseasPda: { generatedPalletsByInbound: {} }, phase4TailOrderControls: { generatedPlans: plans } },
addStep: (_module, name, _response, passed) => steps.push({ name, passed }),
assert: (_module, name, _response, passed) => steps.push({ name, passed }),
request: async (method, path, body, _token, headers) => {
calls.push({ method, path, body, headers })
let data
if (path.includes('/container-summaries?')) data = { records: [historical, summary] }
else if (path.endsWith(`/container-summaries/${bookingId}`)) data = { summary: converted ? { ...summary, planCount: 1, pendingPlanCount: 0, allPlansReady: true } : summary }
else if (path.endsWith('/container-arrival-times/query')) data = [{ sourceContainerBookingId: body.sourceContainerBookingIds[0], estimatedWarehouseArrivalAt: eta, version: 0 }]
else if (path.endsWith('/expected-arrival')) {
if (existing || path.includes(historicalId)) return { http: 200, body: { code: 'A0409', message: '该柜已生成入库计划,预计到仓时间不可修改' } }
data = { sourceContainerBookingId: bookingId, estimatedWarehouseArrivalAt: body.estimatedWarehouseArrivalAt, version: 1 }
} else if (path.includes('/container-preview?')) data = { sourceContainerBookingId: bookingId, ready: true, totalOrderCount: 1,
pendingOrderCount: existing ? 0 : 1, existingPlannedPalletCount: existing ? 1 : 0, snapshotVersion: 1, snapshotHash: 'snapshot' }
else if (path.endsWith('/convert-container')) { converted = true; data = plans }
else throw Error(`Unexpected offline request: ${method} ${path}`)
return { http: 200, body: { code: '200', data }, requestId: 'offline-request', traceId: 'offline-trace' }
} }
return { context, producer, bookingId, historicalId }
}
test('r1 same-number historical booking cannot replace the current producer during ETA and plan conversion', async () => {
const { context, producer, bookingId, historicalId } = conversionContext()
assert.equal(await convertReceivingContainers(context, [producer]), true)
assert.ok(context.steps.every(step => step.passed))
assert.equal(context.calls[0].path, `/api/platform/v1/last-leg/orders/container-summaries/${bookingId}`)
assert.ok(context.calls.every(call => !call.path.includes(historicalId) && !call.path.includes('container-summaries?')))
assert.deepEqual(context.calls.filter(call => call.path.endsWith('/convert-container')).map(call => call.body.sourceContainerBookingId), [bookingId, bookingId])
assert.equal(context.calls.filter(call => call.method === 'PUT').length, 1)
})
test('existing current-run plans reuse their ETA without writes; missing locked ETA blocks rather than repairs', async () => {
for (const eta of ['2026-10-10T10:00:00', null]) {
const { context, producer } = conversionContext({ existing: true, eta })
assert.equal(await convertReceivingContainers(context, [producer]), Boolean(eta))
assert.equal(context.calls.filter(call => call.method === 'PUT' || call.path.endsWith('/convert-container')).length, 0)
assert.equal(context.steps.every(step => step.passed), Boolean(eta))
}
})
test('missing producer booking or mismatched detail identity fails before any ETA query or write', async () => {
for (const missing of [false, true]) {
const { context, producer } = conversionContext({ wrongIdentity: true })
if (missing) delete producer.containerBookingId
assert.equal(await convertReceivingContainers(context, [producer]), false)
assert.ok(context.steps.some(step => !step.passed))
assert.ok(context.calls.every(call => call.method === 'GET'))
}
})
test('producer consumption waits for exact booking projection; same-number history is never consumed', async () => {
const { context, producer, historicalId } = conversionContext()
const actualRequest = context.request
let attempt = 0
const delays = []
context.request = async (...args) => {
const result = await actualRequest(...args)
if (++attempt === 1) result.body.data.summary.sourceContainerBookingId = historicalId
else result.body.data.summary.sourceContainerBookingId = producer.containerBookingId
return result
}
assert.equal(await awaitProducerConsumption(context, [producer], { attempts: 2, delay: async ms => delays.push(ms) }), true)
assert.deepEqual(delays, [1000])
assert.equal(context.calls.length, 2)
assert.ok(context.calls.every(call => call.method === 'GET' && call.path.endsWith(`/${producer.containerBookingId}`)))
const wrong = conversionContext({ wrongIdentity: true })
assert.equal(await awaitProducerConsumption(wrong.context, [wrong.producer], { attempts: 1 }), false)
assert.equal(wrong.context.calls.length, 1)
})
test('pickup readiness ignores historical same-number rows in either order and cannot borrow their operable state', async () => {
const { producer, historicalId } = conversionContext()
const current = { containerNo: producer.containerNo, sourceContainerBookingId: producer.containerBookingId, operable: true }
const historical = { ...current, sourceContainerBookingId: historicalId, operable: false }
for (const rows of [[current, historical], [historical, current], [{ ...historical, operable: true }]]) {
const context = candidateContext([candidateResponse(rows)])
assert.equal(await awaitPickupReadyCandidates(context, 'offline', [producer], [producer], { attempts: 1 }), rows.length === 2)
assert.equal(context.calls.length, 1)
assert.equal(context.calls[0].method, 'GET')
}
})
function candidateContext(responses) {
const calls=[],steps=[]