优化
This commit is contained in:
@@ -5,7 +5,137 @@ import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { expandOverseasCartonCandidates, resolveCartonFixtures, run, uniqueContainerProducer,
|
||||
crossContainerCartonProbe } from './phase4-overseas-pda.mjs'
|
||||
crossContainerCartonProbe, normalizeReceivingEvidence, receivingEvidenceValid,
|
||||
receivingCompletionProof, captureReceivingEvidence, probeCartonResolveNegatives } from './phase4-overseas-pda.mjs'
|
||||
|
||||
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('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('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'}]}
|
||||
@@ -126,7 +256,9 @@ test('PDA module is injected, token-scoped, traceable, and contains no direct ne
|
||||
assert.match(source, /traceId=/)
|
||||
assert.doesNotMatch(source, /https?:\/\//)
|
||||
assert.doesNotMatch(source, /fetch\(|process\.env/)
|
||||
assert.doesNotMatch(source, /planNo|allocationId/)
|
||||
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', () => {
|
||||
|
||||
Reference in New Issue
Block a user