Files
baiyue-firstleg/scripts/admin-test-env-modules/grouped-dispatch-acceptance-contract.test.mjs
T

94 lines
6.2 KiB
JavaScript
Raw Normal View History

2026-10-08 17:07:57 +08:00
import assert from 'node:assert/strict'
import test from 'node:test'
import { readFileSync } from 'node:fs'
import { groupedDispatchMatches, verifyGroupedDispatch } from './grouped-dispatch-acceptance.mjs'
const candidates = [1, 2].map(n => ({ planId: '11', planPalletId: `2${n}`, warehousePalletId: `3${n}`,
overseasWarehouseId: '44', customerId: '55', inboundCode: `IN-${n}`, packageCount: n }))
const groups = candidates.map((row, n) => ({ draftContainerId: `6${n}`, planPalletIds: [row.planPalletId], returnReceiptIds: [] }))
const results = () => groups.map((group, n) => ({ draftContainerId: group.draftContainerId,
dispatch: { dispatchNo: `DS-${n}`, status: 'PENDING_DEPARTURE', palletCount: 1,
packageCount: candidates[n].packageCount, pickedPackageCount: 0, shippedPackageCount: 0,
items: [{ ...candidates[n], id: `7${n}` }] } }))
test('registered dispatch flow consumes current-run selected candidates before its main draft', () => {
const source = readFileSync(new URL('./phase4-overseas-dispatch.mjs', import.meta.url), 'utf8')
const call = source.indexOf('await verifyGroupedDispatch(context, MODULE, selectedCandidates)')
assert.ok(call > source.indexOf('const selectedCandidates'))
assert.ok(call < source.indexOf('const draftToken = `${context.marker}-DISPATCH-DRAFT`'))
})
test('two loading groups preserve distinct dispatches and exact physical sources', () => {
assert.equal(groupedDispatchMatches(results(), groups, candidates), true)
assert.equal(groupedDispatchMatches(results().reverse(), groups, candidates), true)
for (const mutate of [r => r.pop(), r => { r[1].dispatch.dispatchNo = r[0].dispatch.dispatchNo },
r => { r[1].draftContainerId = r[0].draftContainerId },
r => { r[0].dispatch.items = r[1].dispatch.items },
r => { r[0].dispatch.packageCount++ }, r => { r[0].dispatch.items[0].warehousePalletId = '999' },
r => { r[0].dispatch.items[0].inboundCode = 'FOREIGN' }, r => { r[0].dispatch.status = 'PICKING' }]) {
const value = results(); mutate(value); assert.equal(groupedDispatchMatches(value, groups, candidates), false)
}
})
function server(fault = '') {
let draft, next = 0, arranged = false
const calls = [], checks = [], orders = new Map()
const response = (data, code = '200') => ({ http: 200, body: { code, data }, requestId: 'offline', traceId: 'offline' })
const context = { marker: 'CURRENT', createdResources: {},
assert: (_m, label, _r, passed) => checks.push({ label, passed }),
async request(method, path, body) {
calls.push({ method, path, body: structuredClone(body) })
if (path.endsWith('/drafts/snapshot')) { draft = { draftToken: body.draftToken, containers: [] }; return response(null) }
if (path.endsWith('/drafts/containers')) {
assert.equal(body.capacity, 15, 'formal draft supports 15 or 30 pallet vehicle capacity')
const container = { id: groups[next++].draftContainerId, palletIds: [], capacity: body.capacity }
draft.containers.push(container); return response(container)
}
if (path.endsWith('/draft-locks/lock')) {
draft.containers.find(row => row.id === body.draftContainerId).palletIds.push(body.planPalletId); return response(null)
}
if (path.endsWith('/drafts')) return response(structuredClone(draft ? [draft] : []))
if (path.endsWith('/drafts/pre-arrange')) {
if (body.groups[0].planPalletIds[0] !== candidates[0].planPalletId) {
if (fault === 'reject-mutates-draft') draft.containers[0].palletIds = []
return response(null, 'A0409')
}
if (fault === 'unknown-post') return { http: 0, body: null }
if (!arranged) { for (const row of results()) orders.set(row.dispatch.dispatchNo, row.dispatch); arranged = true }
const result = groups.map(group => ({ draftContainerId: group.draftContainerId,
dispatch: structuredClone(orders.get(`DS-${groups.indexOf(group)}`)) }))
if (fault === 'merged-groups') result[1].dispatch.dispatchNo = result[0].dispatch.dispatchNo
return response(result)
}
if (path.endsWith('/draft-locks/release')) { draft = null; return response(null) }
if (path.includes('/candidates?')) return response(candidates)
const match = path.match(/\/dispatches\/(DS-\d)(?:\/items\/(\d+))?$/)
const order = orders.get(match?.[1]); assert.ok(order, path)
if (method === 'DELETE') { order.items = []; order.palletCount = 0; order.packageCount = 0; return response(null) }
return response(structuredClone(order))
} }
return { context, calls, checks }
}
test('grouped orchestration proves rejection, same-group replay and releases owned unpicked sources', async () => {
const { context, calls, checks } = server()
const proof = await verifyGroupedDispatch(context, 'offline', candidates)
assert.equal(proof.status, 'GROUPED_PREARRANGE_VERIFIED')
assert.equal(proof.cleanup, 'OWNED_UNPICKED_SOURCES_RELEASED')
assert.equal(proof.results.length, 2)
assert.ok(checks.every(row => row.passed))
assert.equal(calls.filter(row => row.path.endsWith('/drafts/pre-arrange')).length, 3)
assert.equal(calls.filter(row => row.method === 'DELETE').length, 2)
assert.ok(calls.every(row => !/\/depart|\/pick|\/delivery/.test(row.path)))
})
test('rejected grouping that mutates the draft stops before any valid pre-arrangement', async () => {
const { context, calls } = server('reject-mutates-draft')
await assert.rejects(verifyGroupedDispatch(context, 'offline', candidates), /原草稿/)
assert.equal(calls.filter(row => row.path.endsWith('/drafts/pre-arrange')).length, 1)
assert.notEqual(context.createdResources.groupedDispatch.status, 'GROUPED_PREARRANGE_VERIFIED')
})
test('unknown successful-write transport result is not automatically retried or deleted', async () => {
const { context, calls } = server('unknown-post')
await assert.rejects(verifyGroupedDispatch(context, 'offline', candidates), /原子创建/)
assert.equal(calls.filter(row => row.path.endsWith('/drafts/pre-arrange')).length, 2)
assert.equal(calls.filter(row => row.method === 'DELETE').length, 0)
assert.equal(calls.filter(row => row.path.endsWith('/draft-locks/release')).length, 0)
assert.equal(context.createdResources.groupedDispatch.cleanup, 'PRESERVED_FOR_REVIEW')
})