176 lines
14 KiB
JavaScript
176 lines
14 KiB
JavaScript
import test from 'node:test'
|
|||
|
|
import assert from 'node:assert/strict'
|
||
|
|
import { readFileSync, mkdtempSync, rmSync } from 'node:fs'
|
||
|
|
import { join } from 'node:path'
|
||
|
|
import { tmpdir } from 'node:os'
|
||
|
|
import { installRetainedJournal, validateRetainedScope, validateNativeRetainedDispatch, runRetainedNativeDispatch,
|
||
|
|
saveRetainedReport } from './mixed-container-outbound-test-env.mjs'
|
||
|
|
import { retainedRelabelSource } from './overseas-relabel-fulfillment-acceptance.mjs'
|
||
|
|
|
||
|
|
const BASE = '/api/platform/v1/last-leg/dispatches'
|
||
|
|
const DOCS = '/api/platform/v1/last-leg/delivery-orders/DL-1/documents?customerId=100'
|
||
|
|
const ok = data => ({ http: 200, body: { code: '200', data }, requestId: 'REQ-1', traceId: 'TRACE-1' })
|
||
|
|
const scope = () => ({ marker: 'RUN-1', sourceSha256: 'a'.repeat(64), customerId: '100', warehouseId: '80', inboundCodes: ['IN-1'], expectedPackages: 4 })
|
||
|
|
function nativeProof() {
|
||
|
|
return { marker: 'RUN-1', customerId: '100', dispatchNo: 'DS-1',
|
||
|
|
createBody: { warehouseId: '80', idempotencyKey: 'RUN-1-CREATE', cargo: [{ sourceType: 'INBOUND_PLAN', planPalletId: '9007199254740993',
|
||
|
|
warehousePalletId: '9007199254740994', sourceVersion: 'b'.repeat(64) }] },
|
||
|
|
createResult: { dispatchNo: 'DS-1', action: 'CREATE', recordVersion: '0', status: 'ACTIVE', stage: 'BEFORE_LOADING', result: { dispatchNo: 'DS-1', changedItemIds: ['9007199254740995'] } },
|
||
|
|
detail: { dispatchNo: 'DS-1', recordVersion: '0', status: 'ACTIVE', stage: 'BEFORE_LOADING', counts: { packageCount: '4' },
|
||
|
|
orderDetails: [{ sourceKey: 'PLAN:9007199254740993', customerId: '100', inboundCode: 'IN-1' }] } }
|
||
|
|
}
|
||
|
|
const resultFor = () => ({ schema: 'NATIVE_RETAINED_OUTBOUND_V1', environment: 'TEST', ...scope(), nativeDispatch: nativeProof(), requests: [] })
|
||
|
|
|
||
|
|
test('retained scope rejects legacy data, foreign marker/source identity and unresolved original writes before continuation', () => {
|
||
|
|
assert.throws(() => validateRetainedScope({ dispatchNo: 'DS-1' }, scope()), /legacy reports/)
|
||
|
|
for (const mutate of [value => { value.marker = 'HISTORY' }, value => { value.sourceSha256 = 'other' },
|
||
|
|
value => { value.customerId = '200' }, value => { value.warehouseId = '81' },
|
||
|
|
value => { value.requests.push({ status: 'UNKNOWN' }) }, value => { value.requests.push({ status: 'SENDING' }) }]) {
|
||
|
|
const value = resultFor(); mutate(value); assert.throws(() => validateRetainedScope(value, scope()))
|
||
|
|
}
|
||
|
|
assert.deepEqual(validateNativeRetainedDispatch(nativeProof(), scope()), ['PLAN:9007199254740993'])
|
||
|
|
for (const mutate of [value => { delete value.createBody }, value => { value.createResult.recordVersion = 9007199254740992 },
|
||
|
|
value => { value.createResult.action = 'EDIT' }, value => { value.detail.orderDetails[0].customerId = '200' },
|
||
|
|
value => { value.createBody.cargo[0].sourceVersion = 'unknown' }, value => { value.detail.orderDetails[0].sourceKey = 'PLAN:OTHER' }]) {
|
||
|
|
const value = nativeProof(); mutate(value); assert.throws(() => validateNativeRetainedDispatch(value, scope()))
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
test('journal saves exact original intent before network, blocks unknown/malformed native successes, and refuses replacement commands', async () => {
|
||
|
|
for (const response of [{ http: 0 }, { http: 503, body: { code: 'A0500' } }, ok(null), { http: 200, body: null }]) {
|
||
|
|
const result = resultFor(), snapshots = [], calls = []
|
||
|
|
const context = installRetainedJournal({ createdResources: {}, context: {},
|
||
|
|
async request(method, path, body) { calls.push({ method, path, body }); assert.equal(snapshots.at(-1).requests.at(-1).status, 'SENDING'); return response },
|
||
|
|
}, result, async () => { snapshots.push(structuredClone(result)) })
|
||
|
|
const body = { recordVersion: '0', idempotencyKey: 'ORIGINAL' }
|
||
|
|
await assert.rejects(context.request('POST', `${BASE}/DS-1/depart`, body), /Unknown write/)
|
||
|
|
assert.equal(result.requests[0].status, 'UNKNOWN')
|
||
|
|
assert.deepEqual(result.requests[0].body, body)
|
||
|
|
await assert.rejects(context.request('POST', `${BASE}/DS-1/depart`, { ...body, idempotencyKey: 'NEW' }), /stopped/)
|
||
|
|
assert.equal(calls.length, 1)
|
||
|
|
assert.throws(() => validateRetainedScope(result, scope()), /Unknown write/)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
test('retained report stores file hashes and redacts credentials and binary bytes', () => {
|
||
|
|
const directory = mkdtempSync(join(tmpdir(), 'native-retained-contract-'))
|
||
|
|
try {
|
||
|
|
const path = join(directory, 'report.json')
|
||
|
|
saveRetainedReport(path, { marker: 'RUN-1', password: 'SECRET-PASSWORD', accessToken: 'SECRET-TOKEN',
|
||
|
|
requests: [{ file: { sha256: 'a'.repeat(64), content: 'SECRET-BYTES' } }] })
|
||
|
|
const text = readFileSync(path, 'utf8')
|
||
|
|
assert.doesNotMatch(text, /SECRET-/)
|
||
|
|
assert.ok(text.includes('a'.repeat(64)))
|
||
|
|
} finally { rmSync(directory, { recursive: true, force: true }) }
|
||
|
|
})
|
||
|
|
|
||
|
|
function stock(after) {
|
||
|
|
const quantity = after ? 0 : 4
|
||
|
|
return [{ id: '1', totalPackages: quantity, balance: { id: '1', totalPackages: quantity },
|
||
|
|
positions: [{ id: '11', balanceId: '1', customerId: '100', overseasWarehouseId: '80', inboundCode: 'IN-1', palletNo: 'P-1',
|
||
|
|
totalPackages: quantity, availablePackages: quantity, holdPackages: 0, allocatedPackages: 0, pickedPackages: 0, receivingPackages: 0 }],
|
||
|
|
globalGroups: [{ totalPackages: quantity }] }]
|
||
|
|
}
|
||
|
|
function nativeRunFixture() {
|
||
|
|
const result = resultFor(), events = [], calls = []
|
||
|
|
let stage = 'BEFORE_LOADING', picked = false, signed = false, departure, pod
|
||
|
|
result.loadingMeasurements = { environment: 'TEST', marker: 'RUN-1', dispatchNo: 'DS-1', pallets: [] }
|
||
|
|
const delivery = { customerId: '100', deliveryNo: 'DL-1', outboundNo: 'OB-1', packageCount: '4', destination: { key: 'FBA:ABE4', warehouseCode: 'ABE4' } }
|
||
|
|
const context = { token: 'PLATFORM-IN-MEMORY', marker: 'RUN-1', createdResources: {}, context: {},
|
||
|
|
apiSuccess(_module, _label, response) { return response.body?.data }, assert(_m, _l, _r, passed) { assert.equal(passed, true) },
|
||
|
|
async requestBinary() { return { http: 200, contentType: 'application/pdf', bytes: Buffer.from('%PDF-1.4 TEST') } },
|
||
|
|
async request(method, path, body, token) {
|
||
|
|
calls.push({ method, path, body: structuredClone(body), token })
|
||
|
|
if (method === 'GET' && path.includes('/commands/')) return ok(result.nativeDispatch.createResult)
|
||
|
|
if (method === 'GET' && path.endsWith('/items')) return ok({ items: [{ sourceKey: 'PLAN:9007199254740993', customerId: '100', inboundCode: 'IN-1' }] })
|
||
|
|
if (method === 'GET' && path.startsWith('/api/pda/')) return ok({ dispatchNo: 'DS-1', warehouseId: '80', counts: { pickedPackageCount: picked ? '4' : '0' },
|
||
|
|
items: [{ customerId: '100', scheduledPackages: '4', positions: [{ positionId: '11', balanceId: '1' }] }] })
|
||
|
|
if (method === 'GET' && path === `${BASE}/DS-1`) return ok({ dispatchNo: 'DS-1', status: 'ACTIVE', stage, recordVersion: '9007199254740997', counts: { packageCount: '4' } })
|
||
|
|
if (method === 'POST' && path.endsWith('/depart')) {
|
||
|
|
events.push('depart'); assert.equal(body.recordVersion, '9007199254740996'); assert.equal(token, 'PDA-IN-MEMORY')
|
||
|
|
if (departure) { assert.deepEqual(body, result.departBody); return ok({ ...departure, replayed: true }) }
|
||
|
|
stage = 'DELIVERING'
|
||
|
|
departure = { dispatchNo: 'DS-1', action: 'DEPART', status: 'ACTIVE', stage, recordVersion: '9007199254740997', result: { shippedPackages: '4', deliveries: [delivery] } }
|
||
|
|
return ok(departure)
|
||
|
|
}
|
||
|
|
if (method === 'GET' && path.includes('/outbound-orders/')) return ok({ outbound: { status: 'OUTBOUNDED', customerId: '100', outboundNo: 'OB-1', dispatchNo: 'DS-1', packageCount: 4 }, pallets: [{ cartonMarks: 'NEW-1,NEW-2,NEW-3,NEW-4' }] })
|
||
|
|
if (method === 'GET' && path.includes('/delivery-orders/')) return ok({ order: { customerId: '100', outboundNo: 'OB-1', status: signed ? 'DELIVERED' : 'DELIVERING', signedAt: signed ? '2026-10-19T01:00:00' : null } })
|
||
|
|
if (method === 'PUT' && path === DOCS) {
|
||
|
|
events.push('pod')
|
||
|
|
if (pod) return ok({ ...pod, replayed: true })
|
||
|
|
signed = true; stage = 'DELIVERED'; pod = { version: '9007199254740998', categories: [{ category: 'RECEIPT' }] }
|
||
|
|
return ok(pod)
|
||
|
|
}
|
||
|
|
throw Error(`Unexpected offline ${method} ${path}`)
|
||
|
|
},
|
||
|
|
}
|
||
|
|
installRetainedJournal(context, result, async () => { events.push('checkpoint') })
|
||
|
|
const deps = {
|
||
|
|
async prepareFleetTrip(_context, _module, dispatchNo, warehouseId) { events.push('prepare'); stage = 'LOADING'; return { dispatchNo, warehouseId, tripNo: 'TR-1', preparation: 'VERIFIED' } },
|
||
|
|
async pickAll(_context, token) { events.push('pick'); assert.equal(token, 'PDA-IN-MEMORY'); picked = true; return true },
|
||
|
|
async confirmFleetLoading(_context, _module, _proof, token, measurements) {
|
||
|
|
events.push('loading'); assert.equal(token, 'PDA-IN-MEMORY'); assert.equal(measurements, result.loadingMeasurements)
|
||
|
|
if (!measurements) throw Error('Actual palletSpaces measurement is missing')
|
||
|
|
stage = 'AFTER_LOADING'; return '9007199254740995'
|
||
|
|
},
|
||
|
|
async arrangeDispatchDelivery() { events.push('arrange'); return { data: { result: {} }, deliveries: [delivery], outbounds: [{ customerId: '100', outboundNo: 'OB-1' }] } },
|
||
|
|
async verifyDepartureReadiness() { events.push('ready'); return { data: { recordVersion: '9007199254740996' } } },
|
||
|
|
async saveReceiptAndComplete(current) { await current.request('PUT', DOCS, { expectedVersion: '0', idempotencyKey: 'ORIGINAL-POD' }); return { order: { status: 'DELIVERED' } } },
|
||
|
|
async finishFleetTrip(_context, _module, proof) { events.push('finish'); proof.closure = { status: 'VERIFIED' }; return proof.closure },
|
||
|
|
async inventorySnapshot() { return stock(picked) },
|
||
|
|
async ledger() { return picked ? [{ id: '81', customerId: '100', overseasWarehouseId: '80', businessType: 'SHIP', businessOrderNo: 'DS-1',
|
||
|
|
outboundOrderNo: 'DS-1', palletNo: 'P-1', beforePackages: 4, deltaPackages: -4, afterPackages: 0 }] : [] },
|
||
|
|
}
|
||
|
|
return { context, result, deps, events, calls }
|
||
|
|
}
|
||
|
|
|
||
|
|
test('retained native execution preserves real ordering, token/measurement scope, current zero-safe version, SHIP once, POD and same-run resume', async () => {
|
||
|
|
const f = nativeRunFixture()
|
||
|
|
await runRetainedNativeDispatch(f.context, f.result, scope(), 'PDA-IN-MEMORY', f.deps)
|
||
|
|
assert.equal(f.result.finalStatus, 'PASSED')
|
||
|
|
assert.deepEqual(f.events.filter(value => value !== 'checkpoint'), ['prepare', 'pick', 'loading', 'arrange', 'ready', 'depart', 'depart', 'pod', 'finish'])
|
||
|
|
assert.equal(f.result.inventory.conservation.totals.ledgerShipped, 4)
|
||
|
|
assert.deepEqual(f.result.inventory.ledgerAfter, f.result.inventory.ledgerAfterDepart)
|
||
|
|
assert.equal(f.result.departBody.recordVersion, '9007199254740996')
|
||
|
|
const originalDepartCount = f.calls.filter(row => row.method === 'POST').length
|
||
|
|
await runRetainedNativeDispatch(f.context, f.result, scope(), 'PDA-IN-MEMORY', f.deps)
|
||
|
|
assert.equal(f.calls.filter(row => row.method === 'POST').length, originalDepartCount)
|
||
|
|
assert.equal(f.events.filter(value => value === 'pod').length, 2)
|
||
|
|
assert.equal(f.events.filter(value => value === 'pick').length, 1)
|
||
|
|
assert.equal(f.calls.some(row => /pre-arrange|transfer-delivery/.test(row.path)), false)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('missing real measurements stops after scanned quantity proof and before arrangement, departure and POD', async () => {
|
||
|
|
const f = nativeRunFixture(); delete f.result.loadingMeasurements
|
||
|
|
await assert.rejects(runRetainedNativeDispatch(f.context, f.result, scope(), 'PDA-IN-MEMORY', f.deps), /measurement is missing/)
|
||
|
|
assert.equal(f.events.includes('arrange'), false); assert.equal(f.events.includes('depart'), false); assert.equal(f.events.includes('pod'), false)
|
||
|
|
assert.equal(f.result.inventory.conservation.totals.ledgerShipped, 4)
|
||
|
|
assert.equal(f.result.blocker.code, 'ACTUAL_MEASUREMENT_REQUIRED')
|
||
|
|
assert.equal(f.result.blocker.field, 'palletSpaces')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('relabel input retains original/new mark evidence and cannot upgrade legacy report or a bare business number into native success', () => {
|
||
|
|
assert.throws(() => retainedRelabelSource({ marker: 'RUN-1', scenarios: [{ name: 'SPLIT-TWO-TARGETS', dispatchNo: 'DS-1' }] }, '100'), /native source report/)
|
||
|
|
const native = nativeProof()
|
||
|
|
native.createBody.cargo[0] = { sourceType: 'RELABEL', relabelNo: 'RL-1', warehousePalletId: '9007199254740994', sourceVersion: 'b'.repeat(64) }
|
||
|
|
native.detail.orderDetails[0].sourceKey = 'RELABEL:RL-1:9007199254740994'
|
||
|
|
const source = { environment: 'TEST', marker: 'RUN-1', scenarios: [{ name: 'SPLIT-TWO-TARGETS', relabelNo: 'RL-1', nativeDispatch: native,
|
||
|
|
relabelCartonMarks: ['NEW-1', 'NEW-2', 'NEW-3', 'NEW-4'], sourceCartonMarks: ['OLD-1', 'OLD-2', 'OLD-3', 'OLD-4'] }] }
|
||
|
|
assert.equal(retainedRelabelSource(source, '100', 'DS-1').scope.expectedPackages, 4)
|
||
|
|
const missing = structuredClone(source); delete missing.scenarios[0].sourceCartonMarks
|
||
|
|
assert.throws(() => retainedRelabelSource(missing, '100', 'DS-1'), /original\/new/)
|
||
|
|
assert.throws(() => retainedRelabelSource(source, '200', 'DS-1'), /native source report/)
|
||
|
|
assert.throws(() => retainedRelabelSource(source, '100', 'DS-HISTORY'), /native source report/)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('both live entries use unified Auth and native helper without fixed physical measurements or an implicit historical delivery fallback', () => {
|
||
|
|
const mixed = readFileSync(new URL('./mixed-container-outbound-test-env.mjs', import.meta.url), 'utf8')
|
||
|
|
const relabel = readFileSync(new URL('./overseas-relabel-fulfillment-acceptance.mjs', import.meta.url), 'utf8')
|
||
|
|
for (const source of [mixed, relabel]) assert.doesNotMatch(source, /overseasAccessToken|ADMIN_OVERSEAS_GATEWAY|requestOverseas|READY_TO_DEPART|tripVersion|controlledPalletMeasurements/)
|
||
|
|
assert.match(mixed, /confirmFleetLoading\(context, MODULE, trip, pdaToken, result.loadingMeasurements \|\| native.loadingMeasurements\)/)
|
||
|
|
assert.match(mixed, /\/prints\/bol/)
|
||
|
|
assert.match(mixed, /OUTBOUND_PASSED_DELIVERY_NOT_SUPPORTED/)
|
||
|
|
assert.match(relabel, /runRetainedNativeDispatch/)
|
||
|
|
assert.doesNotMatch(relabel, /existingDeliveries|\/api\/merchant\/v1\/last-leg\/delivery-orders/)
|
||
|
|
})
|