700 lines
46 KiB
JavaScript
700 lines
46 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import test from 'node:test'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
import { expandOverseasCartonCandidates, resolveCartonFixtures, run, uniqueContainerProducer,
|
|
crossContainerCartonProbe, normalizeReceivingEvidence, receivingEvidenceValid,
|
|
receivingCompletionProof, captureReceivingEvidence, receivingEventRecoveryInventoryUnchanged,
|
|
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=[]
|
|
return {calls,steps,createdResources:{},addStep:(_module,name,_response,passed)=>steps.push({name,passed}),
|
|
request:async(method,path,_body,token)=>{calls.push({method,path,token});return responses[Math.min(calls.length-1,responses.length-1)]}}
|
|
}
|
|
const candidateResponse = data => ({http:200,body:{code:'200',data},requestId:'offline-request',traceId:'offline-trace'})
|
|
const exactCandidate = {containerNo:'TSTU6403402',sourceContainerBookingId:'2105838069516079106',operable:true,totalPackages:1}
|
|
const exactProducer = {containerNo:exactCandidate.containerNo,containerBookingId:exactCandidate.sourceContainerBookingId}
|
|
|
|
test('original r2 async shipping gate race waits same booking and container until visible without writing',async()=>{
|
|
const context=candidateContext([candidateResponse([]),candidateResponse([]),candidateResponse([exactCandidate])]),delays=[]
|
|
const result=await awaitReceivingCandidate(context,'offline-picker',exactProducer,{attempts:3,delay:async ms=>delays.push(ms)})
|
|
assert.deepEqual(result.candidate,exactCandidate)
|
|
assert.deepEqual(delays,[1000,1000])
|
|
assert.equal(context.calls.length,3)
|
|
assert.ok(context.calls.every(call=>call.method==='GET'&&call.token==='offline-picker'&&call.path.endsWith('/candidates')))
|
|
const proof=context.createdResources.phase4OverseasPda.candidateWaitEvidence[0]
|
|
assert.equal(proof.outcome,'READY');assert.deepEqual(proof.attempts.map(v=>v.exactMatches),[0,0,1])
|
|
assert.ok(proof.attempts.every(v=>v.requestId&&v.traceId))
|
|
})
|
|
test('same container from another booking, other container and nonoperable original cannot satisfy wait',async()=>{
|
|
for(const candidate of [{...exactCandidate,sourceContainerBookingId:'999'},
|
|
{...exactCandidate,containerNo:'ANOTHER'},{...exactCandidate,operable:false}]) {
|
|
const context=candidateContext([candidateResponse([candidate])]);let waits=0
|
|
const result=await awaitReceivingCandidate(context,'offline',exactProducer,{attempts:2,delay:async()=>waits++})
|
|
assert.equal(result.candidate,null);assert.equal(result.reason,'TIMED_OUT')
|
|
assert.equal(context.calls.length,2);assert.equal(waits,1)
|
|
}
|
|
})
|
|
test('ambiguous exact candidates and authentication or malformed replies stop without masking server errors',async()=>{
|
|
for(const response of [candidateResponse([exactCandidate,exactCandidate]),{http:401,body:{code:'401'}},candidateResponse({records:[exactCandidate]})]) {
|
|
const context=candidateContext([response]);let waits=0
|
|
const result=await awaitReceivingCandidate(context,'offline',exactProducer,{attempts:3,delay:async()=>waits++})
|
|
assert.equal(result.candidate,null);assert.equal(context.calls.length,1);assert.equal(waits,0)
|
|
assert.ok(['AMBIGUOUS','QUERY_FAILED'].includes(result.reason))
|
|
}
|
|
})
|
|
test('candidate waits are per invocation with no cross-mode cache and enforce exact scope/bounds',async()=>{
|
|
const context=candidateContext([candidateResponse([exactCandidate]),candidateResponse([])])
|
|
assert.ok((await awaitReceivingCandidate(context,'first',exactProducer,{attempts:1})).candidate)
|
|
assert.equal((await awaitReceivingCandidate(context,'second',exactProducer,{attempts:1})).candidate,null)
|
|
for(const producer of [{containerNo:'TSTU6403402'},{}])await assert.rejects(awaitReceivingCandidate(context,'offline',producer))
|
|
await assert.rejects(awaitReceivingCandidate(context,'offline',exactProducer,{attempts:31}))
|
|
assert.equal(context.calls.length,2)
|
|
})
|
|
|
|
test('single-container continuation retains malformed/unknown probes without requiring unshipped cancelled CHILD; default chain still blocks missing real other container', async () => {
|
|
const requests = [], gaps = [], checks = []
|
|
const context = { marker: 'ONE-CANCEL', createdResources: {},
|
|
request: async (_method, _path, body) => { requests.push(body.cartonMark); return { http: 200, body: { code: 'A0404', message: 'Carton mark was not found' }, requestId: 'request', traceId: 'trace' } },
|
|
addStep: (_module, _name, _response, passed) => checks.push(passed),
|
|
addCoverageGap: (...args) => gaps.push(args), assert: () => {} }
|
|
const current = { containerNo: 'REAL', inbounds: [{ inboundCode: 'ONE', destinationType: 'FBA' }] }
|
|
await probeCartonResolveNegatives(context, 'token', current, false)
|
|
assert.equal(requests.length, 3)
|
|
assert.ok(checks.every(Boolean))
|
|
assert.equal(gaps.length, 0)
|
|
await probeCartonResolveNegatives(context, 'token', current)
|
|
assert.equal(gaps.length, 1)
|
|
assert.match(gaps[0][3], /另一真实柜/)
|
|
assert.match(source, /probeCartonResolveNegatives\(context, token, current.data, runAuthorizationProbes\)/)
|
|
})
|
|
|
|
function receivingProofFixture() {
|
|
const scope = { customerId: '1', warehouseId: '2', bookingId: '3', containerNo: 'REAL',
|
|
inboundCodes: ['ONE', 'TWO'], sourceInboundIds: ['101', '102'], expectedPackagesByInbound: { ONE: 2, TWO: 2 },
|
|
expectedTransitPackages: 4, completeKey: 'REAL-COMPLETE', freshKey: 'REAL-FRESH' }
|
|
const buckets = { availablePackages: 0, holdPackages: 0, allocatedPackages: 0, pickedPackages: 0, receivingPackages: 0 }
|
|
const balance = (bid, available, transit) => ({ id: bid, customerId: '1', overseasWarehouseId: '2',
|
|
destinationWarehouseId: '4', destinationWarehouseCode: 'FBA', inboundOrderId: bid === '10' ? '101' : '102',
|
|
inboundCode: bid === '10' ? 'ONE' : 'TWO', skuCode: 'SAME-SKU', ...buckets, availablePackages: available, inTransitPackages: transit, procurementPackages: 0 })
|
|
const position = (pid, bid) => ({ id: pid, balanceId: bid, customerId: '1', overseasWarehouseId: '2',
|
|
destinationWarehouseCode: 'FBA', palletId: '70', palletNo: 'DP-REAL', batchNo: 'BATCH',
|
|
warehouseLocationId: '80', warehouseLocationCode: 'LOC', status: 'AVAILABLE', ...buckets, availablePackages: 2 })
|
|
const receipt = (rid, bid, pid, code, tail, task) => ({ id: rid, customerId: '1', planId: '90', taskId: task,
|
|
allocationId: '91', lineId: '92', orderId: tail, inboundCode: code, skuCode: 'SAME-SKU',
|
|
warehousePalletId: '70', palletNo: 'DP-REAL', warehouseLocationId: '80', warehouseLocationCode: 'LOC',
|
|
batchNo: 'BATCH', receivedPackages: 2, inventoryBalanceId: bid, inventoryPositionId: pid,
|
|
inventoryOperationKey: `PDA_STOCK_IN:${task}:91:SAME-SKU` })
|
|
const receipts = [receipt('1001', '10', '201', 'ONE', '101', '301'), receipt('1002', '20', '202', 'TWO', '102', '302')]
|
|
const operations = receipts.map((row, index) => ({ id: String(400 + index), customerId: '1',
|
|
idempotencyKey: row.inventoryOperationKey, operationType: 'STOCK_IN', requestHash: 'a'.repeat(64), status: 'COMPLETED' }))
|
|
const ledgers = receipts.map((row, index) => ({ id: String(500 + index), customerId: '1', balanceId: row.inventoryBalanceId,
|
|
positionId: row.inventoryPositionId, overseasWarehouseId: '2', inboundOrderId: row.orderId, inboundCode: row.inboundCode,
|
|
palletId: '70', palletNo: 'DP-REAL', batchNo: 'BATCH', skuCode: 'SAME-SKU', businessType: 'STOCK_IN', businessOrderNo: 'TAIL-REAL',
|
|
sourceEventId: 'EVENT-REAL', idempotencyKey: `${row.inventoryOperationKey}:${operations[index].id}`,
|
|
statusFrom: 'IN_TRANSIT', statusTo: 'AVAILABLE', beforePackages: 0, deltaPackages: 2, afterPackages: 2 }))
|
|
const before = { balances: [balance('10', 0, 2), balance('20', 0, 2)], positions: [], palletComposition: [],
|
|
stockReceipts: [], inventoryOperations: [], stockInLedger: [], receivingOperations: [],
|
|
httpGroups: [balance('10', 0, 4)], httpLedger: [] }
|
|
const after = { balances: [balance('10', 2, 0), balance('20', 2, 0)], positions: [position('201', '10'), position('202', '20')],
|
|
palletComposition: [position('201', '10'), position('202', '20')], stockReceipts: receipts, inventoryOperations: operations, stockInLedger: ledgers,
|
|
receivingOperations: [{ id: '601', sessionId: '701', action: 'COMPLETE', idempotencyKey: 'REAL-COMPLETE', requestHash: 'b'.repeat(64) }],
|
|
httpGroups: [balance('10', 4, 0)], httpLedger: ledgers }
|
|
const stages = { BEFORE_COMPLETE: normalizeReceivingEvidence(before), AFTER_COMPLETE: normalizeReceivingEvidence(after),
|
|
AFTER_SAME_KEY_REPLAY: normalizeReceivingEvidence(after), AFTER_FRESH_KEY_REJECTION: normalizeReceivingEvidence(after) }
|
|
return { scope, before, after, stages }
|
|
}
|
|
|
|
test('receiving completion freezes all raw balances when group MIN represents two tickets on one pallet', () => {
|
|
const { scope, stages } = receivingProofFixture()
|
|
assert.equal(receivingEvidenceValid(stages.BEFORE_COMPLETE, scope), true)
|
|
assert.equal(receivingEvidenceValid(stages.AFTER_COMPLETE, scope), true)
|
|
const proof = receivingCompletionProof(scope, stages)
|
|
assert.equal(proof.passed, true, JSON.stringify(proof))
|
|
assert.equal(stages.AFTER_COMPLETE.balances.length, 2)
|
|
assert.equal(stages.AFTER_COMPLETE.httpGroups.length, 1)
|
|
assert.equal(stages.AFTER_COMPLETE.httpGroups[0].availablePackages, 4)
|
|
assert.equal(stages.AFTER_COMPLETE.positions.length, 2)
|
|
})
|
|
|
|
test('unrelated canonical UNBOUND stock remains in all snapshots but new receiving still requires a real pallet', () => {
|
|
const { scope, stages } = receivingProofFixture()
|
|
const unboundPosition = { ...stages.AFTER_COMPLETE.positions[0], id: '301', balanceId: '30',
|
|
palletId: '0', palletNo: 'UNBOUND', availablePackages: 1 }
|
|
for (const snapshot of Object.values(stages)) {
|
|
const balance = { ...snapshot.balances[0], id: '30', inboundOrderId: '30',
|
|
inboundCode: 'UNBOUND-SOURCE', skuCode: 'UNRELATED', availablePackages: 1, inTransitPackages: 0 }
|
|
snapshot.balances.push(balance)
|
|
snapshot.httpGroups.push({ ...balance })
|
|
snapshot.positions.push({ ...unboundPosition })
|
|
snapshot.palletComposition.push({ ...unboundPosition })
|
|
assert.equal(receivingEvidenceValid(snapshot, scope), true)
|
|
}
|
|
assert.equal(receivingCompletionProof(scope, stages).passed, true)
|
|
stages.AFTER_COMPLETE.stockReceipts[0].warehousePalletId = '0'
|
|
assert.equal(receivingEvidenceValid(stages.AFTER_COMPLETE, scope), false)
|
|
const malformed = structuredClone(stages.BEFORE_COMPLETE)
|
|
malformed.positions[0].palletNo = 'UNKNOWN'
|
|
malformed.palletComposition[0].palletNo = 'UNKNOWN'
|
|
assert.equal(receivingEvidenceValid(malformed, scope), false)
|
|
})
|
|
|
|
test('receiving proof rejects missing raw source, wrong customer/warehouse, prefix source, missing bucket, ledger key, duplicate receipt and operation mismatch', () => {
|
|
const mutations = [
|
|
row => row.balances.pop(), row => row.positions[0].customerId = 'OTHER',
|
|
row => row.balances[0].overseasWarehouseId = 'OTHER', row => row.stockReceipts[0].inboundCode = 'ONE-CHILD',
|
|
row => row.balances[0].holdPackages = null, row => row.stockInLedger[0].idempotencyKey = row.inventoryOperations[0].idempotencyKey,
|
|
row => row.stockReceipts.push({ ...row.stockReceipts[0] }), row => row.inventoryOperations[0].status = 'PROCESSING',
|
|
row => row.palletComposition[0].availablePackages += 1, row => row.stockInLedger[0].deltaPackages += 1,
|
|
row => row.receivingOperations[0].idempotencyKey = 'ANOTHER',
|
|
]
|
|
for (const mutate of mutations) {
|
|
const { scope, after, stages } = receivingProofFixture()
|
|
mutate(after); stages.AFTER_COMPLETE = normalizeReceivingEvidence(after)
|
|
assert.equal(receivingCompletionProof(scope, stages).passed, false)
|
|
}
|
|
})
|
|
|
|
test('same-key replay and fresh-key rejection must preserve every bucket, position, pallet and immutable row', () => {
|
|
for (const stage of ['AFTER_SAME_KEY_REPLAY', 'AFTER_FRESH_KEY_REJECTION']) {
|
|
for (const mutate of [row => row.balances[0].availablePackages++, row => row.stockReceipts.pop(),
|
|
row => row.receivingOperations.push({ ...row.receivingOperations[0], id: '999' }),
|
|
row => row.stockInLedger[0].sourceEventId = 'OTHER']) {
|
|
const { scope, stages } = receivingProofFixture(); mutate(stages[stage])
|
|
assert.equal(receivingCompletionProof(scope, stages).passed, false)
|
|
}
|
|
}
|
|
})
|
|
|
|
test('normalization is deterministic, preserves zero buckets, and excludes token/operator/provider fields', () => {
|
|
const { after } = receivingProofFixture()
|
|
after.token = 'SECRET'; after.stockReceipts[0].password = 'SECRET'; after.receivingOperations[0].operatorKey = 'PERSON'
|
|
const normal = normalizeReceivingEvidence(after)
|
|
const reversed = { ...after, balances: [...after.balances].reverse(), stockReceipts: [...after.stockReceipts].reverse() }
|
|
assert.deepEqual(normalizeReceivingEvidence(reversed), normal)
|
|
assert.equal(normal.balances[0].procurementPackages, 0)
|
|
assert.doesNotMatch(JSON.stringify(normal), /SECRET|PERSON|password|operatorKey|token/)
|
|
assert.equal(normalizeReceivingEvidence({ ...after, stockReceipts: undefined }), null)
|
|
})
|
|
|
|
test('reverse event recovery requires complete owned inventory evidence and every immutable fact unchanged', () => {
|
|
const { scope, stages } = receivingProofFixture()
|
|
const before = stages.AFTER_FRESH_KEY_REJECTION
|
|
assert.equal(receivingEventRecoveryInventoryUnchanged(scope, before, structuredClone(before)), true)
|
|
assert.equal(receivingEventRecoveryInventoryUnchanged(scope, before, null), false)
|
|
assert.equal(receivingEventRecoveryInventoryUnchanged({ ...scope, customerId: 'OTHER' }, before, before), false)
|
|
for (const mutate of [row => row.balances[0].availablePackages++, row => row.stockReceipts.pop(),
|
|
row => row.positions[0].warehouseLocationCode = 'ANOTHER', row => row.stockInLedger[0].sourceEventId = 'OTHER',
|
|
row => row.receivingOperations.push({ ...row.receivingOperations[0], id: '999' })]) {
|
|
const changed = structuredClone(before)
|
|
mutate(changed)
|
|
assert.equal(receivingEventRecoveryInventoryUnchanged(scope, before, changed), false)
|
|
}
|
|
})
|
|
|
|
test('missing read-only receipt/key reader is a blocker and retains an unproven boundary without inventing HTTP', async () => {
|
|
const checks = [], target = { stages: {} }
|
|
const context = { context: {}, addCoverageGap() {}, assert(m, n, r, passed) { checks.push(passed) },
|
|
async request() { assert.fail('missing provider cannot pretend public DTO supplies raw keys') } }
|
|
const { scope } = receivingProofFixture()
|
|
assert.equal(await captureReceivingEvidence(context, scope, 'BEFORE_COMPLETE', target), null)
|
|
assert.deepEqual(checks, [false]); assert.equal(target.stages.BEFORE_COMPLETE.captured, false)
|
|
})
|
|
|
|
test('capture independently reads complete unfiltered customer warehouse pages and saves only sanitized live facts', async () => {
|
|
const { scope, after } = receivingProofFixture(); const calls = [], checks = []
|
|
const context = { context: { async phase4ReceivingEvidenceReader(actual) {
|
|
assert.equal(actual.stage, 'AFTER_COMPLETE'); assert.deepEqual(actual.inboundCodes, ['ONE', 'TWO'])
|
|
return { ...after, token: 'SECRET' }
|
|
} }, async request(method, requestPath) {
|
|
calls.push(requestPath)
|
|
const data = requestPath.includes('/balances?') ? after.httpGroups : after.httpLedger
|
|
return { http: 200, body: { code: '200', data: { records: data, total: data.length } } }
|
|
}, addStep(m, n, r, passed) { checks.push(passed) }, assert(m, n, r, passed) { checks.push(passed) }, addCoverageGap() { assert.fail('valid fixture must not gap') } }
|
|
const target = { stages: {} }
|
|
const snapshot = await captureReceivingEvidence(context, scope, 'AFTER_COMPLETE', target)
|
|
assert.equal(receivingEvidenceValid(snapshot, scope), true); assert.ok(checks.every(Boolean))
|
|
assert.ok(calls.every(requestPath => requestPath.includes('customerId=1') && requestPath.includes('overseasWarehouseId=2') && !requestPath.includes('inboundCode=')))
|
|
assert.doesNotMatch(JSON.stringify(target), /SECRET/)
|
|
})
|
|
|
|
test('same-root FBA cross-container rejection uses explicit real receiving prefix while preserving original physical box code',() => {
|
|
const current={containerNo:'ROOT-CABINET',inbounds:[{inboundCode:'ROOT'}]}
|
|
const child={containerNo:'CHILD-CABINET',containerBookingId:'20',sourceOrderId:'101',sourceInboundId:'201',
|
|
sourceStatus:'LOADED',inboundCode:'CHILD',destinationType:'FBA',fulfillmentType:'STANDARD',
|
|
cartonMarks:['ROOT#2-1'],receivingCartonMarks:['CHILD#PDA-CONTRACT-1']}
|
|
const before=JSON.stringify(child),probe=crossContainerCartonProbe([child],current)
|
|
assert.equal(probe.cartonMark,'CHILD#PDA-CONTRACT-1')
|
|
assert.equal(probe.mode,'FBA_RECEIVING_PREFIX');assert.equal(probe.sourceOrderId,'101')
|
|
assert.equal(JSON.stringify(child),before)
|
|
for(const fault of [
|
|
{...child,receivingCartonMarks:undefined}, {...child,receivingCartonMarks:['ROOT#PDA-CONTRACT-1']},
|
|
{...child,receivingCartonMarks:['CHILD#PDA-CONTRACT-']}, {...child,receivingCartonMarks:['CHILD#PDA-CONTRACT-0']},
|
|
{...child,containerNo:'ROOT-CABINET'}, {...child,sourceOrderId:null}, {...child,sourceInboundId:null},
|
|
{...child,containerBookingId:null}, {...child,sourceStatus:'WAIT_SPLIT'}, {...child,inboundCode:'ROOT'},
|
|
])assert.equal(crossContainerCartonProbe([fault],current),null)
|
|
assert.equal(crossContainerCartonProbe([],current),null)
|
|
assert.equal(crossContainerCartonProbe([child],{containerNo:'ROOT-CABINET',inbounds:[]}),null)
|
|
})
|
|
test('physical OVERSEAS and dropship probes still require another real loaded producer and keep the exact sealed code',() => {
|
|
const other={containerNo:'OTHER',containerBookingId:'20',sourceOrderId:'101',sourceInboundId:'201',
|
|
sourceStatus:'LOADED',inboundCode:'OTHER-SOURCE',destinationType:'OVERSEAS',fulfillmentType:'STANDARD',
|
|
cartonMarks:['ORIGINAL-PHYSICAL#2-1']}
|
|
const current={containerNo:'CURRENT',inbounds:[{inboundCode:'CURRENT-SOURCE'}]}
|
|
assert.equal(crossContainerCartonProbe([other],current).cartonMark,'ORIGINAL-PHYSICAL#2-1')
|
|
assert.equal(crossContainerCartonProbe([other],current).mode,'PHYSICAL_CARTON')
|
|
assert.equal(crossContainerCartonProbe([{...other,sourceInboundId:null}],current),null)
|
|
})
|
|
|
|
test('receiving selects one current booking and supplies mandatory booking identity', () => {
|
|
assert.match(source, /item.containerNo === containerNo && id\(item.sourceContainerBookingId\) === bookingId/)
|
|
assert.match(source, /matchingCandidates.length === 1/)
|
|
assert.match(source, /if \(!longId\(sourceContainerBookingId\)\)/)
|
|
assert.match(source, /sourceContainerBookingId=\$\{encodeURIComponent\(sourceContainerBookingId\)\}/)
|
|
assert.equal((source.match(/\/prepare\$\{bookingQuery\}/g) || []).length, 3)
|
|
})
|
|
|
|
test('one physical container is received once even when matrix roles overlap', () => {
|
|
const fba = { containerNo: 'TEST-C1', fulfillmentType: 'STANDARD' }
|
|
const overseas = { containerNo: 'TEST-C2' }
|
|
assert.deepEqual([null, fba, { ...fba }, overseas].filter(uniqueContainerProducer), [fba, overseas])
|
|
assert.doesNotMatch(source, /dropship \|\| matrix/)
|
|
})
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
const source = fs.readFileSync(path.join(here, 'phase4-overseas-pda.mjs'), 'utf8')
|
|
|
|
test('dropship original counted cartons must resolve exactly before any receiving or inventory checks', async () => {
|
|
const inbounds = [{ inboundCode: 'REAL-INBOUND', destinationType: 'OVERSEAS', totalPackages: 2 }]
|
|
const producer = { fulfillmentType: 'ONE_CLICK_DROPSHIP', inboundCode: 'REAL-INBOUND', destinationType: 'OVERSEAS',
|
|
sealedCartonMarks: ['REAL-INBOUND#2-2', 'REAL-INBOUND#2-1'] }
|
|
for (const fault of [null, 'missing-carton', 'wrong-carton', 'wrong-inbound', 'wrong-destination']) {
|
|
const calls = []; const checks = []
|
|
const context = {
|
|
async request(method,requestPath,body) {
|
|
calls.push({ method, requestPath, body })
|
|
const data = { cartonMark: body.cartonMark, inboundCode: 'REAL-INBOUND', destinationType: 'OVERSEAS' }
|
|
if (fault === 'wrong-carton') data.cartonMark = 'ANOTHER-CARTON'
|
|
if (fault === 'wrong-inbound') data.inboundCode = 'ANOTHER-INBOUND'
|
|
if (fault === 'wrong-destination') data.destinationType = 'FBA'
|
|
return { http: 200, body: { code: fault === 'missing-carton' ? 'A0404' : '200',
|
|
message: 'Carton mark was not found', data }, requestId: 'request', traceId: 'trace' }
|
|
},
|
|
assert(module, name, response, passed) { checks.push({ name, passed }) },
|
|
addStep(module, name, response, passed) { checks.push({ name, passed }) },
|
|
addCoverageGap() {},
|
|
}
|
|
const result = await resolveCartonFixtures(context, 'token', inbounds, ['OLD-CANDIDATE#20-1'], producer)
|
|
assert.ok(calls.every(call => call.method === 'POST' && call.requestPath.endsWith('/current/cartons/resolve')))
|
|
assert.ok(calls.every(call => producer.sealedCartonMarks.includes(call.body.cartonMark)))
|
|
if (fault) {
|
|
assert.equal(result, null, fault)
|
|
assert.ok(checks.some(check => !check.passed), fault)
|
|
assert.ok(!checks.some(check => check.name.includes('排除未实装箱码候选')), fault)
|
|
} else {
|
|
assert.deepEqual(result.map(item => item.cartonMark), producer.sealedCartonMarks)
|
|
assert.equal(calls.length, 2)
|
|
assert.ok(checks.every(check => check.passed))
|
|
}
|
|
}
|
|
for (const sealedCartonMarks of [undefined, [], ['REAL-INBOUND#2-1'], ['REAL-INBOUND#2-1', 'REAL-INBOUND#2-1']]) {
|
|
const checks = []
|
|
const result = await resolveCartonFixtures({ assert(m,n,r,passed) { checks.push(passed) },
|
|
async request() { assert.fail('incomplete physical fixture must not issue HTTP') } },
|
|
'token', inbounds, producer.sealedCartonMarks, { ...producer, sealedCartonMarks })
|
|
assert.equal(result, null)
|
|
assert.deepEqual(checks, [false])
|
|
}
|
|
})
|
|
|
|
test('current loading event binds each pallet carton group to an explicit source detail', () => {
|
|
const model = fs.readFileSync(path.join(here, '../../common-event-contract/src/main/java/com/baiyue/common/event/FirstLegLoadingPlanCompletedV2.java'), 'utf8')
|
|
const producer = fs.readFileSync(path.join(here, '../../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletedOutboxService.java'), 'utf8')
|
|
assert.match(model, /record SkuPackageSnapshot\(String sourceInboundDetailId, String skuCode/)
|
|
assert.match(model, /sourceInboundDetailId == null \|\| sourceInboundDetailId\.isBlank\(\)/)
|
|
assert.match(producer, /SkuPackageSnapshot\(detail\.getSourceInboundDetailId\(\)\.toString\(\), detail\.getSkuCode\(\)/)
|
|
assert.match(producer, /groupingBy\(PalletRow::getSourceInboundDetailId/)
|
|
assert.match(producer, /map\(PalletRow::getActualSkuQuantity\)/)
|
|
assert.match(producer, /cartonQuantities\(detailRows\.getValue\(\), marks\)/)
|
|
const measurement = fs.readFileSync(path.join(here, 'inbound-measurement.mjs'), 'utf8')
|
|
const loading = fs.readFileSync(path.join(here, 'phase3-loading.mjs'), 'utf8')
|
|
const finalize = fs.readFileSync(path.join(here, 'phase3-loading-finalize.mjs'), 'utf8')
|
|
assert.match(measurement, /sealedCartonMarks\.push\(resolved\.boxCode\)/)
|
|
assert.match(loading, /phase3Loading\.cartonMarks = marks/)
|
|
assert.match(finalize, /sealedCartonMarks: Array\.isArray\(fixture\.sealedCartonMarks\)/)
|
|
const entry = fs.readFileSync(path.join(here, '../admin-test-env-acceptance.mjs'), 'utf8')
|
|
assert.match(entry, /sealedCartonMarks: Array\.isArray\(fixture\.sealedCartonMarks\)/)
|
|
assert.match(entry, /runPhase4ProducerVariant\('OVERSEAS-PRIMARY', 'OVERSEAS', 'ONE_CLICK_DROPSHIP', true\)/)
|
|
assert.match(source, /resolveCartonFixtures\(context, token, current\.data\.inbounds, producerCartonMarks, preferredProducer\)/)
|
|
assert.ok(source.indexOf('const cartonFixtures = await resolveCartonFixtures') < source.indexOf('const completed = await pda'))
|
|
})
|
|
|
|
test('PDA module is injected, token-scoped, traceable, and contains no direct network or secret source', () => {
|
|
assert.match(source, /export async function run\(context\)/)
|
|
assert.match(source, /request\(/)
|
|
assert.match(source, /requestMultipart\(/)
|
|
assert.match(source, /requestId=/)
|
|
assert.match(source, /traceId=/)
|
|
assert.doesNotMatch(source, /https?:\/\//)
|
|
assert.doesNotMatch(source, /fetch\(|process\.env/)
|
|
assert.doesNotMatch(source, /planNo/)
|
|
// Allocation identity is read-only evidence; PDA command URLs/bodies remain container-scoped.
|
|
assert.doesNotMatch(source, /\/allocations\//)
|
|
})
|
|
|
|
test('PDA module covers the container-number receiving contract and excludes outbound domains', () => {
|
|
for (const token of ['/candidates', '/prepare', '/current/unload-photos/files', '/current/sign',
|
|
'/current/cartons/resolve', '/current/pallets/resolve',
|
|
'/current/receipts/bulk', '/current/receipts/carton', '/current/receipts/pallet', '/current/complete']) assert.ok(source.includes(token), token)
|
|
for (const token of ['/inventory/resolve', '/locations/resolve', '/pallets/resolve', '/operations?page=1&size=50']) {
|
|
assert.ok(source.includes(token), token)
|
|
}
|
|
for (const role of ['LICENSE_PLATE', 'CONTAINER_SEAL', 'DOOR_OPEN']) assert.ok(source.includes(role), role)
|
|
for (const forbidden of ['/outbound', '/dispatch', '/delivery', '/internal/']) assert.equal(source.includes(forbidden), false, forbidden)
|
|
assert.match(source, /同键同载荷重放/)
|
|
assert.match(source, /inbound-plans\/container-preview/)
|
|
assert.match(source, /orders\/container-arrival-times\/query/)
|
|
assert.match(source, /container-summaries\/\$\{encodeURIComponent\(bookingId\)\}\/expected-arrival/)
|
|
assert.match(source, /estimatedWarehouseArrivalAt, expectedVersion: Number\(arrivalTime\.version\)/)
|
|
assert.match(source, /snapshotVersion: Number\(preview\.snapshotVersion\)/)
|
|
assert.match(source, /snapshotHash: String\(preview\.snapshotHash\)/)
|
|
assert.match(source, /rememberGeneratedPlans\(context, plans\)/)
|
|
assert.match(source, /resolveGeneratedPallet\(context, inbound\)/)
|
|
assert.match(source, /同键异载荷冲突/)
|
|
assert.match(source, /跨柜切换锁返回409/)
|
|
assert.match(source, /item\.operable === true && item\.allPlansReady === true/)
|
|
assert.match(source, /第二仓库操作员抢占同柜返回409/)
|
|
assert.match(source, /OVERSEAS_WAREHOUSE_PDA_OPERATOR/)
|
|
assert.match(source, /lastleg:pda:operate/)
|
|
assert.match(source, /安全投影已收敛/)
|
|
assert.match(source, /projectionReady = success\(projectionResponse\) \|\| notFound\(projectionResponse\)/)
|
|
assert.match(source, /setTimeout\(resolve, 500\)/)
|
|
assert.match(source, /重复累计不产生超收/)
|
|
assert.match(source, /重复箱新键被拒绝/)
|
|
assert.match(source, /key\(context, `fba-bulk-\$\{index\}`\)/)
|
|
assert.match(source, /inbounds\.reduce\(\(total, inbound\) => total \+ Number\(inbound\.totalPackages \|\| 0\), 0\)/)
|
|
assert.match(source, /resolvedInboundCodes\.includes\(scannedInboundCode\)/)
|
|
assert.match(source, /duplicate\.data\?\.result === 'ALREADY_RECEIVED'/)
|
|
assert.match(source, /if \(packageCount <= 0\) return true/)
|
|
assert.match(source, /key\(context, `overseas-carton-\$\{index\}`\)/)
|
|
assert.match(source, /模式硬拒绝/)
|
|
assert.match(source, /current返回404正常空态/)
|
|
assert.match(source, /安全扫描返回A0400/)
|
|
assert.match(source, /warehouse-admin\?tenantId=0&.*capabilityCode=RECEIVING&warehouseType=OVERSEAS&status=ACTIVE/)
|
|
assert.match(source, /193000000000200003.*193000000000200004/)
|
|
assert.doesNotMatch(source, /request\('POST', '\/api\/platform\/v1\/warehouse-admin'/)
|
|
assert.doesNotMatch(source, /request\('GET', '\/api\/platform\/v1\/warehouse-admin'/)
|
|
assert.match(source, /receiving-containers\/current\/pallets\/resolve/)
|
|
assert.match(source, /overseasWarehouseId, password/)
|
|
assert.match(source, /inbound\?\.overseasWarehouseId/)
|
|
assert.doesNotMatch(source, /const warehouseId = id\(inbound\?\.destinationWarehouseId\)/)
|
|
assert.match(source, /禁止在PDA收货时临时新建脱离计划的托盘/)
|
|
assert.match(source, /rememberGeneratedPlans\(context, plans\)/)
|
|
assert.match(source, /resolveGeneratedPallet\(context, inbound\)/)
|
|
assert.match(source, /generatedPalletsByInbound/)
|
|
assert.match(source, /standardFbaProducers = \(Array\.isArray\(matrix\?\.containers\) \? matrix\.containers : \[\]\)/)
|
|
assert.match(source, /item\?\.fulfillmentType !== 'ONE_CLICK_DROPSHIP'/)
|
|
assert.match(source, /FBA-OPERATIONS/)
|
|
assert.match(source, /FBA-ROUTE/)
|
|
assert.match(source, /\.slice\(0, 4\)/)
|
|
assert.match(source, /const producers = \[dropship, \.\.\.standardFbaProducers,/)
|
|
assert.doesNotMatch(source, /创建当前柜实收托盘|创建当前柜实收库位|async function createPallet/)
|
|
assert.match(source, /retained as acceptance evidence/)
|
|
assert.match(source, /preferredProducer\?\.cartonMarks\?\.length/)
|
|
assert.match(source, /const cartonCandidates =/)
|
|
assert.doesNotMatch(source, /current\/skus\/resolve/)
|
|
assert.doesNotMatch(source, /skuCode:/)
|
|
assert.match(source, /FBA箱码与OVERSEAS真实manifest箱码必须HTTP 200\/code 200并返回当前柜唯一入仓码/)
|
|
assert.match(source, /排除未实装箱码候选/)
|
|
assert.match(source, /const matches = inbounds\.filter/)
|
|
assert.match(source, /!Array\.isArray\(resolved\?\.inboundCode\)/)
|
|
assert.match(source, /matches\.length === 1/)
|
|
assert.match(source, /destinations\.size === 1/)
|
|
assert.doesNotMatch(source, /\.split\([^\n]*['"]#['"]/)
|
|
assert.match(source, /`\$\{item\.inboundCode\}#PDA-CONTRACT-\$\{index \+ 1\}`/)
|
|
for (const negative of ['无#箱码被服务端拒绝', '空#前缀箱码被服务端拒绝',
|
|
'未命中入仓码前缀被服务端拒绝', '其他柜箱码前缀被当前锁定柜拒绝',
|
|
'OVERSEAS缺少真实manifest行时不得按标准箱码推导']) {
|
|
assert.ok(source.includes(negative), negative)
|
|
}
|
|
assert.doesNotMatch(source, /FBA标准箱码序号超范围被拒绝/)
|
|
assert.match(source, /FBA与OVERSEAS箱码resolve均为纯查询且零状态写入/)
|
|
assert.match(source, /receivingSnapshot\(afterResolve\.data\) === beforeResolve/)
|
|
assert.match(source, /const fixturesConserved = overseasFixtures\.length === expectedPending/)
|
|
assert.match(source, /OVERSEAS实装箱唛与待收箱数守恒/)
|
|
assert.match(source, /for \(const \[index, fixture\] of overseasFixtures\.entries\(\)\)/)
|
|
assert.doesNotMatch(source, /for \(const \[index, cartonMark\] of cartonMarks\.entries\(\)\)/)
|
|
assert.match(source, /last-leg\/orders\?page=1&size=200&containerNo=/)
|
|
assert.match(source, /phase4OverseasPda\.orderId = id\(order\.id\)/)
|
|
assert.match(source, /phase4OverseasPda\.customerId = id\(order\.customerId\)/)
|
|
assert.match(source, /现场库存查询只返回Token绑定仓且包含库存位置/)
|
|
assert.match(source, /作业历史包含本轮柜号收货动作且分页稳定/)
|
|
assert.match(source, /awaitProducerConsumption\(context, allProducers\)/)
|
|
assert.match(source, /last-leg\/orders\?page=1&size=200&containerNo=/)
|
|
assert.match(source, /跨区广播消费观察/)
|
|
assert.match(source, /跨区广播消费观察 \$\{containerNo\}`,[\s\S]*?response,[\s\S]*?true,/)
|
|
assert.match(source, /OBSERVATION_OPTIONS = \{ scenario: SCENARIO, deploymentBlocker: false \}/)
|
|
assert.doesNotMatch(source, /blockerGap\(context, '跨区消费生产矩阵'/)
|
|
assert.match(source, /awaitPickupReadyCandidates\(context, primary\.token, producers, allProducers\)/)
|
|
assert.match(source, /missingPickupReady=/)
|
|
assert.match(source, /item\?\.shippingReady === false/)
|
|
assert.match(source, /!expectedSet\.has\(id\(item\?\.containerNo\)\)/)
|
|
assert.match(source, /unexpectedAwaiting=/)
|
|
assert.match(source, /candidatesByContainer\.get\(containerNo\)\?\.operable === true/)
|
|
assert.match(source, /candidatesByContainer\.get\(containerNo\)\?\.operable !== true/)
|
|
assert.match(source, /missingPickupReady\.length === 0 && unexpectedAwaiting\.length === 0/)
|
|
assert.doesNotMatch(source, /shippingReady == null|shippingReady !== true/)
|
|
assert.match(source, /nonBlank\(item\?\.resourceKey\)/)
|
|
assert.match(source, /item\?\.shippingReady !== false/)
|
|
assert.match(source, /unexpectedHistorical=/)
|
|
assert.doesNotMatch(source, /awaitProducerCandidates/)
|
|
})
|
|
|
|
test('OVERSEAS carton discovery probes the frozen sequence and lets the server exclude skipped cartons', () => {
|
|
assert.deepEqual(expandOverseasCartonCandidates([
|
|
{ inboundCode: 'NYEXX2609020042', destinationType: 'OVERSEAS' },
|
|
{ inboundCode: 'FBA2609020001', destinationType: 'FBA' },
|
|
], ['NYEXX2609020042#8-5', 'NYEXX2609020042#8-2', 'NYEXX2609020042#8-1']), [
|
|
'NYEXX2609020042#8-1', 'NYEXX2609020042#8-2', 'NYEXX2609020042#8-3', 'NYEXX2609020042#8-4',
|
|
'NYEXX2609020042#8-5', 'NYEXX2609020042#8-6', 'NYEXX2609020042#8-7', 'NYEXX2609020042#8-8',
|
|
])
|
|
assert.deepEqual(expandOverseasCartonCandidates([
|
|
{ inboundCode: 'NYEXX2609020042', destinationType: 'OVERSEAS' },
|
|
], ['NYEXX2609020042#2001-1']), [])
|
|
})
|
|
|
|
test('real-device legacy FBA carton rejection is retained as regression evidence', () => {
|
|
const evidence = {
|
|
http: 200,
|
|
code: 'A0409',
|
|
message: 'FBA destination must use bulk count',
|
|
requestId: '5e925a3d-8184-41e1-b0d2-f4e1aae86639',
|
|
traceId: '62fdf603b65fd7f43ec452bb8a6ca185',
|
|
}
|
|
assert.deepEqual(evidence, {
|
|
http: 200,
|
|
code: 'A0409',
|
|
message: 'FBA destination must use bulk count',
|
|
requestId: '5e925a3d-8184-41e1-b0d2-f4e1aae86639',
|
|
traceId: '62fdf603b65fd7f43ec452bb8a6ca185',
|
|
})
|
|
assert.match(source, /a0409Message\(response, \/\^FBA destination must use bulk count\$\//)
|
|
assert.match(source, /a0409Message\(response, \/\^OVERSEAS destination must use carton-by-carton scanning; bulk count is forbidden\$\//)
|
|
})
|
|
|
|
test('real-device deployed FBA resolve A0404 is retained until server-side inbound-prefix matching lands', () => {
|
|
const evidence = {
|
|
http: 200,
|
|
code: 'A0404',
|
|
message: 'Carton mark was not found',
|
|
requestId: 'a716e2c2-dc10-4d07-8014-7438afb63090',
|
|
traceId: '6ab8fcfcd812056ad6570d3b1652157b',
|
|
databaseFact: 'current FBA has no ll_inbound_plan_carton manifest',
|
|
}
|
|
assert.equal(evidence.code, 'A0404')
|
|
assert.equal(evidence.message, 'Carton mark was not found')
|
|
assert.match(evidence.databaseFact, /no ll_inbound_plan_carton manifest/)
|
|
assert.match(source, /a0404Message\(response, \/\^Carton mark was not found\$\//)
|
|
})
|
|
|
|
test('PDA receiving lifecycle is ordered from candidate claim through photos, mode-specific receipts, pallet and complete', () => {
|
|
const orderedTokens = [
|
|
'const candidates = await awaitReceivingCandidate', "'选择柜号并建立WAIT_SIGN会话'",
|
|
'uploadPhotos(context, token)', "'绑定车牌封签开柜三角色照片并签收'",
|
|
'resolveGeneratedPallet(context, inbound)', "`解析${inbound.destinationType}实物托盘`", 'receiveFba(context, token',
|
|
'receiveOverseas(context, token', "'回查柜内逐入仓码实收进度'",
|
|
"'完成当前柜并写入库存与卸柜事件'", "'完成后current返回404正常空态'",
|
|
]
|
|
let cursor = -1
|
|
for (const token of orderedTokens) {
|
|
const next = source.indexOf(token, cursor + 1)
|
|
assert.ok(next > cursor, `${orderedTokens[Math.max(0, orderedTokens.indexOf(token) - 1)]} -> ${token}`)
|
|
cursor = next
|
|
}
|
|
for (const token of [
|
|
'/current/receipts/bulk', '/current/receipts/carton',
|
|
"'FBA模式硬拒绝逐箱收货'", "'OVERSEAS模式硬拒绝批量收货'",
|
|
'PHOTO_ROLES.every', "finalStatus === 'COMPLETED'", 'completedContainers.push',
|
|
'completedContainerFacts.push', 'expectedPalletCost', 'physicalPallets', 'unitPrice',
|
|
]) assert.ok(source.includes(token), token)
|
|
})
|
|
|
|
test('missing OVERSEAS_WAREHOUSE_PDA_OPERATOR overseas authority is a deployment blocker', async () => {
|
|
const calls = []; const gaps = []; const steps = []
|
|
const context = {
|
|
marker: 'PDA-CONTRACT', context: {}, createdResources: {},
|
|
async request(...args) {
|
|
const route = /\/(?:last-leg|overseas-warehouse-resources)(?:\/|\?|$)/.test(String(args[1]));
|
|
const handler = route ? async function(...args) { calls.push(args); throw new Error('must not request without credentials') } : async function(method,requestPath) {
|
|
calls.push({ method, requestPath })
|
|
return { method, path: requestPath, http: 200, body: { code: '200', data: [] },
|
|
requestId: 'request-contract-1', traceId: 'trace-contract-0001' }
|
|
};
|
|
return handler.apply(this,args);
|
|
},
|
|
|
|
async requestMultipart(...args) { calls.push(args); throw new Error('must not request without credentials') },
|
|
apiSuccess(module, name, response) { return response.body.data },
|
|
addStep(...args) { steps.push(args) },
|
|
assert(module, name, response, passed, evidence, options) { steps.push({ module, name, passed, evidence, options }) },
|
|
addCoverageGap(...args) { gaps.push(args) },
|
|
}
|
|
await run(context)
|
|
assert.ok(calls.some((call) => call.requestPath === '/api/platform/v1/iam/roles'))
|
|
assert.ok(calls.some((call) => call.requestPath === '/api/platform/v1/iam/permissions'))
|
|
assert.ok(gaps.some((gap) => gap[2] === '海外PDA仓库操作员权限'))
|
|
assert.ok(steps.some((step) => step?.passed === false && step?.options?.deploymentBlocker))
|
|
})
|
|
|
|
test('missing original producer identity blocks before candidate lookup and still cleans temporary identities', async () => {
|
|
const calls = []; const gaps = []; const steps = []
|
|
const response = (data, requestPath, method = 'GET') => ({ method, path: requestPath, http: 200,
|
|
body: { code: '200', data }, requestId: 'req-contract', traceId: 'trace-contract' })
|
|
let userSequence = 0
|
|
const roleId = '4000005'; const permissionId = '5107015'
|
|
const context = {
|
|
marker: 'PDA-CONTRACT', token: '', createdResources: {}, context: {},
|
|
async findAvailablePhone() { userSequence += 1; return `1750000000${userSequence}` },
|
|
async request(...args) {
|
|
const route = /\/(?:last-leg|overseas-warehouse-resources)(?:\/|\?|$)/.test(String(args[1]));
|
|
const handler = route ? async function(method,requestPath,body,token,headers) {
|
|
calls.push({ channel: 'overseas', method, requestPath })
|
|
if (requestPath.endsWith('/current')) return { method, path: requestPath, http: 200,
|
|
body: { code: 'A0404', message: 'not found' }, requestId: 'req-current', traceId: 'trace-current' }
|
|
return response([], requestPath, method)
|
|
} : async function(method,requestPath,body) {
|
|
calls.push({ channel: 'domestic', method, requestPath })
|
|
if (method === 'GET' && requestPath.endsWith('/roles')) return response([
|
|
{ id: roleId, roleCode: 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', status: 'ACTIVE', permissionIds: [permissionId] },
|
|
], requestPath, method)
|
|
if (method === 'GET' && requestPath.endsWith('/permissions')) return response([
|
|
{ id: permissionId, permissionCode: 'lastleg:pda:operate' },
|
|
], requestPath, method)
|
|
if (method === 'GET' && requestPath.startsWith('/api/platform/v1/warehouse-admin?')) return response({
|
|
records: [{ id: '193000000000200003', warehouseName: '洛杉矶仓', warehouseType: 'OVERSEAS', status: 'ACTIVE' }],
|
|
page: 1, size: 200, total: 1, pages: 1,
|
|
}, requestPath, method)
|
|
if (method === 'POST' && requestPath.endsWith('/users')) return response({
|
|
membershipId: String(9000 + userSequence), username: body.username, tenantId: '0', roleIds: [roleId],
|
|
overseasWarehouseId: String(body.overseasWarehouseId), status: 'ACTIVE',
|
|
}, requestPath, method)
|
|
if (method === 'POST' && requestPath === '/api/auth/login') return response({ accessToken: `pda-token-${userSequence}` }, requestPath, method)
|
|
if (method === 'PUT' && /\/users\/\d+\/status$/.test(requestPath)) return response({ status: 'DISABLED' }, requestPath, method)
|
|
throw new Error(`unexpected domestic request ${method} ${requestPath}`)
|
|
};
|
|
return handler.apply(this,args);
|
|
},
|
|
|
|
apiSuccess(module, name, result) { return result.body.data },
|
|
addStep(module, name, result, passed, evidence, options) { steps.push({ module, name, result, passed, evidence, options }) },
|
|
assert(module, name, result, passed, evidence, options) { steps.push({ module, name, result, passed, evidence, options }) },
|
|
addCoverageGap(...args) { gaps.push(args) },
|
|
}
|
|
await run(context)
|
|
assert.equal(calls.some((call) => call.requestPath.endsWith('/candidates')),false)
|
|
assert.ok(steps.some((step) => step.name.includes('正常空态') && step.passed))
|
|
assert.ok(steps.some((step) => step.name.includes('本轮原柜候选身份覆盖前置') && !step.passed && step.options.deploymentBlocker))
|
|
assert.equal(gaps.length, 1)
|
|
assert.equal(context.createdResources.phase4OverseasPda.finalStatus, null)
|
|
assert.equal(context.createdResources.phase4OverseasPda.pickerMembershipIds.length, 2)
|
|
assert.equal(calls.filter((call) => /\/users\/\d+\/status$/.test(call.requestPath)).length, 2)
|
|
})
|