Files
baiyue-firstleg/scripts/prod-release-delivery-stage.test.mjs

161 lines
8.7 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { executeDelivery, runDeliveryStage, validateDeliveryInput, verifyOutbound, withDeliveryJournal } from './prod-release-delivery-stage.mjs'
const input = { batch: 'acceptance-1234', orderId: '101', orderNo: 'FL-101', tailOrderId: '201',
warehouseId: '301', inboundCode: 'IN-101', dispatchNo: 'DS-101', outboundNo: 'OB-101', deliveryNo: 'DL-101', packageCount: 1 }
function fixture() {
const dispatch = { dispatchNo: 'DS-101', overseasWarehouseId: '301', status: 'DEPARTED', packageCount: 1,
shippedPackageCount: 1, pickedPackageCount: 1, remainingPackageCount: 0, items: [{ inboundCode: 'IN-101' }] }
const outbound = { outbound: { outboundNo: 'OB-101', dispatchNo: 'DS-101', overseasWarehouseId: '301', status: 'OUTBOUNDED', packageCount: 1 },
deliveries: [{ deliveryNo: 'DL-101' }], pallets: [{ inboundCode: 'IN-101', packageCount: 1, pickedPackageCount: 1, status: 'PICKED' }] }
const tail = { order: { id: '201', customerId: '501', sourceOrderNo: 'FL-101', inboundCode: 'IN-101', overseasWarehouseId: '301', subOrderStatus: 'OUTBOUND:OUTBOUNDED' } }
return { dispatch, outbound, tail }
}
function harness(state = { steps: {} }) {
const facts = fixture(), writes = []
let status = 'WAIT_DELIVERY', files = [], uploads = 0
const request = async (region, method, path, body) => {
if (method !== 'GET') writes.push({ path, body })
if (path.endsWith('/status?customerId=501')) { status = 'DELIVERING'; return {} }
if (path.includes('/documents?')) {
if (method === 'PUT') { files = [{ fileId: '701' }]; status = 'DELIVERED' }
return { version: 0, categories: [{ category: 'RECEIPT', status: files.length ? 'UPLOADED' : 'MISSING', files }] }
}
if (path.includes('/dispatches/')) return facts.dispatch
if (path.includes('/outbound-orders/')) return facts.outbound
if (path.includes('/last-leg/orders/')) return facts.tail
if (path.includes('/delivery-orders/')) return { order: { customerId: '501', deliveryNo: 'DL-101', dispatchNo: 'DS-101', outboundNo: 'OB-101',
remark: 'RELEASE-ACCEPTANCE:acceptance-1234', status, version: 0 }, operations: status === 'DELIVERED' ? [{ operationNode: 'RECEIPT_AUTO_COMPLETE' }] : [] }
if (region === 'cn' && path.includes('/platform/inbounds?')) return { records: [{ inboundCode: 'IN-101', orderId: '101', processStatus: 'DELIVERED' }] }
if (region === 'cn') return { summary: { mainStatus: 'COMPLETED' } }
throw new Error('unexpected request')
}
const options = { input, customerId: '501', state, save: async patch => Object.assign(state, patch), request,
upload: async (_path, _name, content) => { assert.match(content, /NOT A REAL PROOF/); uploads++; return { fileId: '701' } }, wait: async () => {} }
return { options, facts, writes, uploads: () => uploads }
}
test('reject path injection, imprecise ids and excessive scope', () => {
validateDeliveryInput(input)
for (const patch of [{ dispatchNo: '../wrong' }, { orderId: 101 }, { packageCount: 100 }]) assert.throws(() => validateDeliveryInput({ ...input, ...patch }))
})
test('receipt closes delivery and both domestic states; completed rerun has no writes', async () => {
const h = harness()
const first = await runDeliveryStage(h.options)
assert.equal(first.deliveryClosureVerified, true)
assert.equal(first.fullFlowVerified, false)
assert.equal(first.status, 'DELIVERY_STAGE_PASSED')
const count = h.writes.length
await runDeliveryStage(h.options)
assert.equal(h.writes.length, count)
assert.equal(h.uploads(), 1)
})
test('stale pallet detail blocks release despite successful dispatch and receipt', async () => {
const h = harness()
h.facts.outbound.pallets[0].pickedPackageCount = 0
h.facts.tail.order.subOrderStatus = 'OUTBOUND:WAIT_OUTBOUND;DELIVERY:WAIT_DELIVERY'
const result = await runDeliveryStage(h.options)
assert.equal(result.status, 'DELIVERY_STAGE_WITH_BLOCKERS')
assert.equal(result.deliveryClosureVerified, true)
assert.deepEqual(result.blockers, ['OUTBOUND_PALLET_PICK_STATE_STALE', 'TAIL_OUTBOUND_SUB_STATUS_STALE'])
})
test('wrong source order or additional inbound blocks before mutations', async () => {
for (const mutate of [h => { h.facts.tail.order.sourceOrderNo = 'OTHER' }, h => { h.facts.dispatch.items.push({ inboundCode: 'OTHER' }) }]) {
const h = harness(); mutate(h)
await assert.rejects(runDeliveryStage(h.options))
assert.equal(h.writes.length, 0)
assert.equal(h.uploads(), 0)
}
})
test('uncertain upload and uncertain status mutation are never resent', async () => {
const h = harness({ uploadIntent: true })
await assert.rejects(runDeliveryStage(h.options), /禁止重复上传/)
assert.equal(h.uploads(), 0)
const h2 = harness({ startIntent: true })
await assert.rejects(runDeliveryStage(h2.options), /禁止重发/)
assert.equal(h2.writes.length, 0)
})
test('lost document save response reuses persisted payload and key', async () => {
const h = harness()
const request = h.options.request
let lost = false
h.options.request = async (...args) => {
const value = await request(...args)
if (!lost && args[1] === 'PUT' && args[2].includes('/documents?')) { lost = true; throw new Error('timeout') }
return value
}
await assert.rejects(runDeliveryStage(h.options), /timeout/)
const body = structuredClone(h.options.state.saveBody)
const result = await runDeliveryStage(h.options)
assert.equal(result.deliveryClosureVerified, true)
assert.deepEqual(h.options.state.saveBody, body)
assert.equal(h.uploads(), 1)
})
test('missing domestic event remains a blocker', async () => {
const h = harness(), original = h.options.request
h.options.request = (...args) => args[0] === 'cn' ? Promise.resolve({ records: [], summary: { mainStatus: 'IN_TRANSIT' } }) : original(...args)
const result = await runDeliveryStage(h.options)
assert.equal(result.deliveryClosureVerified, false)
assert.ok(result.blockers.includes('CN_DELIVERY_EVENT_NOT_RECONCILED'))
})
test('journal rejects changed binding and concurrent processes', async () => {
const dir = await mkdtemp(join(tmpdir(), 'prod-delivery-test-')), path = join(dir, 'journal.json')
try {
await withDeliveryJournal(path, 'first', async (_state, save) => {
await save({ fileId: '701' })
await assert.rejects(withDeliveryJournal(path, 'first', async () => {}), /EEXIST/)
})
await assert.rejects(withDeliveryJournal(path, 'changed', async () => {}), /不符/)
} finally { await rm(dir, { recursive: true, force: true }) }
})
test('missing or malformed delivery version blocks status write and upload', async () => {
for (const version of [undefined, null, '', ' ', false, -1, '9007199254740993']) {
const h = harness(), original = h.options.request
h.options.request = async (...args) => {
const result = await original(...args)
if (result?.order?.deliveryNo) result.order.version = version
return result
}
await assert.rejects(runDeliveryStage(h.options), /版本缺失或无效/)
assert.equal(h.writes.length, 0)
assert.equal(h.uploads(), 0)
}
})
test('delivery executor uses one gateway/accessToken for status, multipart receipt and domestic reconciliation', async t => {
const dir = await mkdtemp(join(tmpdir(), 'single-delivery-test-'))
t.after(() => rm(dir, { recursive: true, force: true }))
const h = harness(), calls = []
const settings = { environment: 'gray', apiBase: 'https://fixture.invalid/api', customer: { id: '501' },
credentials: { ADMIN: { loginId: 'admin-fixture', password: 'secret-fixture' } } }
const fetcher = async (url, options) => {
assert.ok(url.startsWith(settings.apiBase + '/'))
const path = url.slice(settings.apiBase.length)
calls.push(path)
let data
if (path === '/auth/login') data = { accessToken: 'single-access' }
else {
assert.equal(options.headers.Authorization, 'Bearer single-access')
if (options.body instanceof FormData) {
assert.equal(path, '/platform/v1/last-leg/delivery-orders/DL-101/documents/RECEIPT/files?customerId=501')
assert.match(await options.body.get('file').text(), /NOT A REAL PROOF/)
data = { fileId: '701' }
} else {
data = await h.options.request(path.startsWith('/platform/v1/last-leg/') ? 'overseas' : 'cn',
options.method, path, options.body ? JSON.parse(options.body) : undefined)
}
}
return { ok: true, status: 200, json: async () => ({ code: '200', data }) }
}
const result = await executeDelivery(settings, input, join(dir, 'journal.json'), fetcher)
assert.equal(result.status, 'DELIVERY_STAGE_PASSED')
assert.equal(result.fullFlowVerified, false)
assert.equal(calls.filter(path => path === '/auth/login').length, 1)
})