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) })