Files
baiyue-firstleg/scripts/admin-test-env-modules/phase4-overseas-dispatch-contract.test.mjs
T

194 lines
12 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { dispatchBusinessSnapshot, dispatchRouteSnapshot, dispatchRouteMatches,
selectDispatchRouteFixture, verifyDispatchRouteLifecycle } from './phase4-overseas-dispatch.mjs'
const moduleUrl = new URL('./phase4-overseas-dispatch.mjs', import.meta.url)
const source = await readFile(fileURLToPath(moduleUrl), 'utf8')
test('phase4 overseas dispatch module freezes only stable platform query and smart pre-arrange paths', () => {
for (const token of [
"const BASE = '/api/platform/v1/last-leg/dispatches'",
"query(BASE, { page: 1, size: 20 })",
'`${BASE}/candidates`', '`${BASE}/drafts`', '`${BASE}/drafts/snapshot`',
'`${BASE}/draft-locks/lock`', '`${BASE}/pre-arrange`', '`${BASE}/draft-locks/release`',
'`${BASE}/${encodeURIComponent(dispatchNo)}`',
]) assert.match(source, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
for (const forbidden of ['/api/pda/', '/outbound', '/delivery', '/depart', '/cancel']) {
assert.equal(source.includes(forbidden), false, `must exclude ${forbidden}`)
}
})
test('module uses marker-owned completed PDA pallets and never borrows historical candidates', () => {
assert.match(source, /fixture\?\.marker !== context\.marker/)
assert.match(source, /fixture\?\.finalStatus !== 'COMPLETED'/)
assert.match(source, /fixture\.palletIds\.has\(id\(value\?\.warehousePalletId\)\)/)
assert.match(source, /id\(value\?\.overseasWarehouseId\) === id\(fixture\.overseasWarehouseId\)/)
assert.doesNotMatch(source, /candidates\[0\]/)
})
test('ordinary dispatch selects one current-run pallet per destination for multi-FBA delivery proof', () => {
assert.match(source, /const candidatesByDestination = new Map\(selectedCandidates.map/)
assert.match(source, /row\?\.fbaWarehouseCode \|\| row\?\.destinationWarehouseCode/)
assert.match(source, /selectedCandidates\.length >= 2/)
assert.match(source, /planPalletIds, fbaAppointmentNo: null/)
assert.match(source, /Number\(arranged\.data\?\.palletCount\) === planPalletIds\.length/)
})
function routeCandidates() {
return [['20', '120', 'ABE4', 2], ['30', '130', 'ABE8', 3], ['21', '121', 'ABE4', 4]].map(([planPalletId, warehousePalletId, fbaWarehouseCode, packageCount]) => ({
planId: '10', planPalletId, warehousePalletId, fbaWarehouseCode, destinationWarehouseCode: fbaWarehouseCode,
overseasWarehouseId: '900', customerId: '100', inboundCode: `IN-${planPalletId}`, sourceStage: 'RECEIVED', destinationType: 'FBA', packageCount,
}))
}
function responseFor(candidates) {
return { id: '1', dispatchNo: 'DS-TEST', overseasWarehouseId: '900', status: 'PENDING_DEPARTURE',
route: [...new Set(candidates.map(row => row.fbaWarehouseCode))].join(','),
items: candidates.map(row => ({ ...row, id: '8' + row.planPalletId })),
itemCount: candidates.length, palletCount: candidates.length,
packageCount: candidates.reduce((sum, row) => sum + row.packageCount, 0), pickedPackageCount: 0, shippedPackageCount: 0 }
}
test('C52 fixture needs two destinations and two distinct physical pallets at one destination', () => {
const rows = routeCandidates()
assert.equal(selectDispatchRouteFixture(rows.slice(0, 2)), null)
assert.equal(selectDispatchRouteFixture([rows[0], rows[2]]), null)
assert.equal(selectDispatchRouteFixture([rows[0], rows[1], { ...rows[2], warehousePalletId: rows[0].warehousePalletId }]), null)
const result = selectDispatchRouteFixture(rows)
assert.deepEqual(result.selected.map(row => row.planPalletId), ['20', '30'])
assert.equal(result.duplicate.planPalletId, '21')
assert.match(source, /generatedPalletsByInbound/)
assert.match(source, /id\(value\?\.customerId\) === id\(fixture.customerId\)/)
assert.match(source, /value\?\.destinationType === 'FBA'/)
})
test('C52 independently derives route from actual rows and rejects fabricated destinations, sources, totals and duplicate route codes', () => {
const rows = routeCandidates(), value = responseFor(rows)
assert.equal(dispatchRouteMatches(value, rows), true)
for (const route of ['ABE4', 'ABE4,ABE8,ABE9', 'ABE4,ABE8,ABE4', 'ABE4,ABE8,', '']) {
assert.equal(dispatchRouteMatches({ ...value, route }, rows), false)
}
for (const patch of [{ itemCount: 9 }, { packageCount: 10 }, { overseasWarehouseId: '901' }, { status: 'PICKING' }]) {
assert.equal(dispatchRouteMatches({ ...value, ...patch }, rows), false)
}
for (const patch of [{ inboundCode: 'FOREIGN' }, { warehousePalletId: '999' }, { fbaWarehouseCode: 'ABE9' }, { planPalletId: '999' }]) {
assert.equal(dispatchRouteMatches({ ...value, items: [{ ...value.items[0], ...patch }, ...value.items.slice(1)] }, rows), false)
}
assert.notDeepEqual(dispatchRouteSnapshot(value), dispatchRouteSnapshot({ ...value, items: [{ ...value.items[0], id: '999' }, ...value.items.slice(1)] }))
assert.notEqual(dispatchBusinessSnapshot(value), dispatchBusinessSnapshot({ ...value, route: 'ABE9' }))
})
function lifecycleHarness(fault = '') {
const rows = routeCandidates(), selected = rows.slice(0, 2)
let state = responseFor(selected), nextItemId = 900, preArrangeCount = 0
const calls = [], checks = []
const base = '/api/platform/v1/last-leg/dispatches'
const ok = data => ({ http: 200, body: { code: '200', data: structuredClone(data) }, requestId: 'REQ', traceId: 'TRACE' })
const error = code => ({ http: 200, body: { code }, requestId: 'REQ', traceId: 'TRACE' })
const context = { marker: 'CURRENT', createdResources: { phase4OverseasDispatch: {} },
apiSuccess: (_module, _name, response) => response.body.data,
assert: (_module, name, _response, passed) => checks.push({ name, passed }),
request: async (method, path, body) => {
calls.push({ method, path, body })
if (path.includes('/candidates')) return ok(rows.filter(row => !state.items.some(item => item.planPalletId === row.planPalletId)))
if (path.endsWith('/draft-locks/lock')) return ok(null)
if (method === 'GET') return ok(fault === 'wrong-route' ? { ...state, route: 'ABE9' } : state)
if (method === 'DELETE') {
const itemId = path.split('/').at(-1), remaining = state.items.filter(row => row.id !== itemId)
if (remaining.length === state.items.length) {
if (fault === 'rejected-delete-mutates') state.route = 'ABE9'
return error('A0404')
}
state = { ...responseFor(remaining), items: remaining }
return ok(state)
}
if (path === `${base}/pre-arrange`) {
if (new Set(body.planPalletIds).size !== body.planPalletIds.length) return error('A0400')
if (body.draftToken !== 'DRAFT') return error('A0409')
preArrangeCount++
const picked = [...body.planPalletIds].sort().map(palletId => rows.find(row => row.planPalletId === palletId))
const items = picked.map(row => state.items.find(item => item.planPalletId === row.planPalletId) || { ...row, id: String(nextItemId++) })
state = { ...responseFor(picked), items }
if (fault === 'replay-id-drift' && preArrangeCount === 2) state.items[0].id = String(nextItemId++)
return ok(state)
}
throw new Error(`unexpected request ${method} ${path}`)
} }
return { context, calls, checks, options: { dispatchNo: 'DS-TEST', candidates: selected, duplicate: rows[2], draftToken: 'DRAFT', draftContainerId: '50' } }
}
test('C52 HTTP orchestration adds duplicate destination, removes last destination, restores and freezes exact replay/rejection facts', async () => {
const h = lifecycleHarness()
await verifyDispatchRouteLifecycle(h.context, h.options)
assert.equal(h.context.createdResources.phase4OverseasDispatch.routeLifecycle.status, 'VERIFIED')
assert.ok(h.checks.length >= 20 && h.checks.every(row => row.passed))
for (const label of ['同目的地第二托增加箱托数但路线稳定去重', '最后一托移除后路线删除该目的地', '重新加入后恢复原目的地集合', '重复托盘ID拒绝A0400', '其他草稿抢占原排车拒绝A0409']) {
assert.ok(h.checks.some(row => row.name.includes(label)))
}
assert.ok(h.calls.filter(row => row.path.endsWith('/pre-arrange')).every(row => !Object.hasOwn(row.body, 'route')))
assert.equal(h.calls.filter(row => row.method === 'DELETE').length, 3)
assert.deepEqual(h.context.createdResources.phase4OverseasDispatch.routeLifecycle.stages.at(-1).snapshot.items.map(row => row.planPalletId), ['20', '30'])
})
test('C52 stops on wrong route, replay row identity drift or side effects after rejected removal', async () => {
for (const fault of ['wrong-route', 'replay-id-drift', 'rejected-delete-mutates']) {
const h = lifecycleHarness(fault)
await assert.rejects(verifyDispatchRouteLifecycle(h.context, h.options), /C52/)
assert.equal(h.context.createdResources.phase4OverseasDispatch.routeLifecycle.status, 'UNPROVEN')
assert.ok(h.checks.some(row => !row.passed))
}
})
test('module proves draft idempotency, lock isolation, editable pre-arrange replay and readback', () => {
assert.match(source, /相同草稿快照重复保存/)
assert.match(source, /同草稿重复锁定托盘幂等成功/)
assert.match(source, /其他草稿锁定同一托盘返回A0409/)
assert.match(source, /出库前同草稿重复预排复用原排车且业务快照不漂移/)
assert.match(source, /重复释放草稿幂等成功/)
assert.match(source, /排车详情按记录自身客户范围加载托盘明细/)
assert.match(source, /response\?\.http === 409.*codeOf\(response\) === 'A0409'/s)
})
test('module does not invent optimistic version or idempotency headers absent from DispatchRequests', () => {
assert.doesNotMatch(source, /expectedVersion|recordVersion|Idempotency-Key/)
assert.match(source, /PENDING_DEPARTURE/)
assert.match(source, /data == null/)
})
test('module records the marker-owned dispatch without attempting excluded cleanup actions', () => {
assert.match(source, /createdResources\.phase4OverseasDispatch/)
assert.match(source, /dispatchNo = dispatchNo/)
assert.match(source, /finalStatus = arranged\.data\?\.status/)
assert.match(source, /PENDING_DEPARTURE dispatch is retained as marker-owned acceptance evidence/)
})
test('module prefers marker-owned relabel pallets and proves only new FBA carton marks are frozen', () => {
assert.match(source, /phase4OverseasRelabel/)
assert.match(source, /sourceStage === 'RELABEL'/)
assert.match(source, /relabel\.targetPalletIds\.includes/)
assert.match(source, /relabelCartonMarks/)
assert.match(source, /!String\(value\?\.cartonMarks \|\| ''\)\.trim\(\)/)
assert.match(source, /relabelNo: relabel\.relabelNo/)
})
test('relabel settlement follows pre-arrange and does not replace the ordinary dispatch lifecycle', () => {
assert.match(source, /await runRelabelDispatch\(context, relabel\)[\s\S]*if \(relabel.dispatchNo\) await settleRelabelFulfillment\(context, relabel\)/)
const branch = source.slice(source.indexOf('const relabel = context.createdResources'), source.indexOf('const fixture = markerFixture(context)', source.indexOf('const relabel = context.createdResources')))
assert.doesNotMatch(branch, /\breturn\b/)
assert.match(source, /Object.assign\(relabel, \{/)
})
test('dispatch replay snapshot ignores regenerated item ids but detects business drift', () => {
const a = { id: '1', dispatchNo: 'DS-1', status: 'PENDING_DEPARTURE', packageCount: 2,
palletCount: 1, items: [{ id: '10', planPalletId: '20', warehousePalletId: '30', packageCount: 2 }] }
const replay = { ...a, items: [{ ...a.items[0], id: '11' }] }
assert.equal(dispatchBusinessSnapshot(a), dispatchBusinessSnapshot(replay))
for (const changed of [{ ...replay, id: '2' }, { ...replay, packageCount: 4 },
{ ...replay, items: [{ ...a.items[0], warehousePalletId: '31' }] }]) {
assert.notEqual(dispatchBusinessSnapshot(a), dispatchBusinessSnapshot(changed))
}
assert.doesNotMatch(source, /item\?\.cartonMarks/)
})