feat(dispatch): unify whole vehicle scheduling, source ownership and staged execution

This commit is contained in:
2026-10-19 23:40:42 +08:00
parent e0677e7ae7
commit 9dd34791f7
346 changed files with 29245 additions and 12602 deletions
@@ -1,193 +1,180 @@
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'
import test from 'node:test'
import { readFileSync } from 'node:fs'
import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve, basename } from 'node:path'
import { DISPATCH_BASE as BASE, dispatchEvidenceContext } from './grouped-dispatch-acceptance.mjs'
import { run, selectDispatchRouteFixture, dispatchRouteMatches, dispatchBusinessSnapshot, 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')
const clone = value => structuredClone(value)
const rows = () => [1, 2, 3].map(n => ({ selection: { sourceType: 'INBOUND_PLAN', planPalletId: `2${n}`, returnReceiptId: null, relabelNo: null, warehousePalletId: `3${n}`, sourceVersion: `${n}`.repeat(64) },
sourceKey: `PLAN:2${n}`, sourceVersion: `${n}`.repeat(64), customerId: '55', warehouseId: '44', inboundCode: `IN-${n}`, palletNo: `P-${n}`,
destination: { key: `FBA:A${n === 3 ? 1 : n}`, type: 'FBA', warehouseCode: `A${n === 3 ? 1 : n}` }, availablePackages: n + 1, appointmentBatches: [], blockReasons: [] }))
// All state here is offline test data. This server rejects missing versions and preserves original-key results.
function server(fault = '', initial = false) {
const candidates = rows(), drafts = new Map(), orders = new Map(), ledger = new Map(), calls = [], checks = []
let serial = 0, containerId = 60, itemId = 70
const counts = o => ({ itemCount: o.items.length, palletCount: o.items.length, packageCount: o.items.reduce((n, i) => n + i.scheduledPackages, 0), pickedPackageCount: 0, shippedPackageCount: 0 })
const item = c => ({ dispatchItemId: String(++itemId), sourceType: c.selection.sourceType, sourceKey: c.sourceKey, sourceVersion: c.sourceVersion, customerId: c.customerId,
planPalletId: c.selection.planPalletId, warehousePalletId: c.selection.warehousePalletId, inboundCode: c.inboundCode, destination: clone(c.destination), scheduledPackages: c.availablePackages, pickedPackages: 0, remove: { allowed: true } })
const syncRoute = o => { o.keys = [...new Set([...(fault === 'append-only-route' ? o.keys || [] : []), ...o.items.map(i => i.destination.key)])] }
const create = cargo => { const o = { dispatchNo: `DS-${orders.size + 1}`, recordVersion: initial && orders.size === 0 ? 0 : 1, items: cargo.map(s => item(candidates.find(c => c.selection.planPalletId === s.planPalletId))) }; syncRoute(o); orders.set(o.dispatchNo, o); return o }
const command = (o, action) => ({ dispatchNo: o.dispatchNo, action, recordVersion: o.recordVersion, status: 'ACTIVE', stage: 'BEFORE_LOADING', counts: counts(o), replayed: false })
const view = o => ({ dispatchNo: o.dispatchNo, status: 'ACTIVE', stage: 'BEFORE_LOADING', recordVersion: o.recordVersion, counts: counts(o),
dispatchInfo: { route: [...new Set(o.items.map(i => i.destination.warehouseCode))].join(',') },
orderDetails: o.items.map(i => ({ sourceKey: i.sourceKey, customerId: i.customerId, inboundCode: i.inboundCode, destinationFbaWarehouse: i.destination.warehouseCode })) })
const stops = o => [{ index: 0, name: 'Actual Origin', address: 'Warehouse 44', destinationKey: 'ORIGIN:44' }, ...o.keys.map((key, n) => ({ index: n + 1, name: key, destinationKey: key }))]
const response = (data, code = '200') => ({ http: 200, body: { code, data: clone(data) }, requestId: 'OFFLINE-REQ', traceId: 'OFFLINE-TRACE' })
const context = { marker: 'CURRENT', createdResources: { phase4OverseasPda: { marker: 'CURRENT', finalStatus: 'COMPLETED', customerId: '55', overseasWarehouseId: '44', palletIds: ['31', '32', '33'] } },
checkpoint: async () => {}, // Explicit offline double; full-run test below removes it and verifies actual disk writes.
assert: (_module, label, _response, passed) => checks.push({ label, passed }),
async request(method, raw, body) {
calls.push({ method, path: raw, body: clone(body) })
const path = raw.split('?')[0], params = new URLSearchParams(raw.split('?')[1]), write = method !== 'GET'
const key = `${method}:${path}:${body?.idempotencyKey}`
if (write) {
assert.ok(body?.idempotencyKey, raw)
if (ledger.has(key)) {
const old = ledger.get(key); assert.equal(JSON.stringify(body), old.body, 'replay must use original body and version')
if (fault === 'replay-row-drift' && path.endsWith('/items')) orders.get('DS-1').items[0].dispatchItemId = '9999'
const data = clone(old.data); if (data?.action || data?.vehicles) data.replayed = true; return response(data)
}
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)
const remember = data => { ledger.set(key, { body: JSON.stringify(body), data: clone(data) }); return response(data) }
const draft = drafts.get(body?.draftToken)
if (method === 'POST' && path === `${BASE}/drafts`) { const d = { draftToken: `SERVER-${++serial}`, recordVersion: 0, warehouseId: body.warehouseId, status: 'ACTIVE', sources: [], containers: [] }; drafts.set(d.draftToken, d); return remember(d) }
if (method === 'GET' && path === `${BASE}/drafts`) return response([...drafts.values()].filter(d => d.status === 'ACTIVE'))
if (path === `${BASE}/candidates`) {
assert.equal(params.get('warehouseId'), '44'); assert.equal(params.get('sourceStage'), 'RECEIVED')
const claimed = new Set([...orders.values()].flatMap(o => o.items.map(i => i.sourceKey)))
for (const d of drafts.values()) if (d.status === 'ACTIVE' && d.draftToken !== params.get('draftToken')) for (const c of d.sources) claimed.add(c.sourceKey)
return response({ candidates: candidates.filter(c => !claimed.has(c.sourceKey)), complete: true, snapshotAt: '2026-10-19' })
}
throw new Error(`unexpected request ${method} ${path}`)
if (path === `${BASE}/drafts/containers`) { assert.equal(body.recordVersion, draft.recordVersion); draft.containers.push({ draftContainerId: String(++containerId), sequenceNo: draft.containers.length + 1, capacity: body.capacity, sourceKeys: [] }); draft.recordVersion++; return remember(draft) }
const deleteContainer = path.match(/\/drafts\/([^/]+)\/containers\/(\d+)$/)
if (deleteContainer) { const d = drafts.get(deleteContainer[1]); assert.equal(body.recordVersion, d.recordVersion); const c = d.containers.find(c => c.draftContainerId === deleteContainer[2]); if (c.sourceKeys.length) return response(null, 'A0409'); d.containers = d.containers.filter(c => c.draftContainerId !== deleteContainer[2]); d.recordVersion++; return remember(d) }
if (path === `${BASE}/draft-locks/lock` || path === `${BASE}/draft-locks/unlock`) {
assert.equal(body.recordVersion, draft.recordVersion)
const c = candidates.find(c => c.selection.planPalletId === body.source.planPalletId), container = draft.containers.find(g => g.draftContainerId === body.draftContainerId)
assert.deepEqual(body.source, c.selection)
if (path.endsWith('/lock')) {
if ([...orders.values()].some(o => o.items.some(i => i.sourceKey === c.sourceKey))) return response(null, 'A0409')
draft.sources.push(clone(c)); container.sourceKeys.push(c.sourceKey)
} else { draft.sources = draft.sources.filter(s => s.sourceKey !== c.sourceKey); container.sourceKeys = container.sourceKeys.filter(k => k !== c.sourceKey) }
draft.recordVersion++; return remember(draft)
}
if (path === `${BASE}/draft-locks/release`) { assert.equal(body.recordVersion, draft.recordVersion); assert.equal(draft.sources.length, 0); draft.status = 'RELEASED'; draft.recordVersion++; return remember(draft) }
if (path === `${BASE}/drafts/snapshot`) { assert.equal(body.recordVersion, draft.recordVersion); assert.ok(Array.isArray(body.planIds)); assert.ok(Array.isArray(body.containers)); draft.containers = clone(body.containers); draft.recordVersion++; return remember(draft) }
if (path === `${BASE}/drafts/pre-arrange`) {
assert.equal(body.recordVersion, draft.recordVersion)
if (!body.groups.every(g => JSON.stringify(g.dispatch.cargo.map(s => `PLAN:${s.planPalletId}`).sort()) === JSON.stringify([...draft.containers.find(c => c.draftContainerId === g.draftContainerId).sourceKeys].sort()))) return response(null, 'A0409')
const vehicles = body.groups.map(g => ({ draftContainerId: g.draftContainerId, dispatch: command(create(g.dispatch.cargo), 'CREATE') })); draft.status = 'CONVERTED'
return remember({ draftToken: draft.draftToken, vehicles, replayed: false })
}
if (path === BASE && method === 'POST') { assert.equal(body.vehicle, null); assert.equal(body.driver, null); return remember(command(create(body.cargo), 'CREATE')) }
if (path === BASE && method === 'GET') return response({ records: [...orders.values()].filter(o => o.dispatchNo === params.get('dispatchNo')).map(o => ({ ...command(o, 'READ'), warehouseId: '44' })) })
const match = path.slice(BASE.length).match(/^\/([^/]+)(?:\/(items|route)(?:\/([^/]+)\/remove)?)?$/), o = orders.get(match?.[1]); assert.ok(o, `${method} ${raw}`)
if (write) {
assert.equal(body.recordVersion, o.recordVersion, 'explicit server version, including zero')
if (match[2] === 'route') { assert.equal(method, 'PUT'); assert.deepEqual([...body.destinationKeys].sort(), [...o.keys].sort()); o.keys = clone(body.destinationKeys); o.recordVersion++; return remember(command(o, 'UPDATE_ROUTE')) }
if (match[3]) {
if (!o.items.some(i => i.dispatchItemId === match[3])) { if (fault === 'rejected-remove-mutates') o.recordVersion++; return response(null, 'A0404') }
o.items = o.items.filter(i => i.dispatchItemId !== match[3]); o.recordVersion++; syncRoute(o); return remember(command(o, 'REMOVE_CARGO'))
}
assert.equal(method, 'POST'); assert.equal(match[2], 'items')
if (new Set(body.cargo.map(s => s.planPalletId)).size !== body.cargo.length) return response(null, 'A0400')
o.items.push(...body.cargo.map(s => item(candidates.find(c => c.selection.planPalletId === s.planPalletId)))); o.recordVersion++; syncRoute(o)
if (fault === 'unknown-add') return { http: 0, body: null }
if (fault === 'route-drift') o.keys = ['FBA:FOREIGN']
return remember(command(o, 'ADD_CARGO'))
}
if (match[2] === 'items') return response({ dispatchNo: o.dispatchNo, recordVersion: o.recordVersion, status: 'ACTIVE', stage: 'BEFORE_LOADING', items: o.items })
if (match[2] === 'route') return response({ dispatchNo: o.dispatchNo, recordVersion: o.recordVersion, stops: stops(o), edit: { allowed: true } })
return response(view(o))
} }
return { context, calls, checks, options: { dispatchNo: 'DS-TEST', candidates: selected, duplicate: rows[2], draftToken: 'DRAFT', draftContainerId: '50' } }
if (initial) { create(candidates.slice(0, 2).map(c => c.selection)); context.createdResources.phase4OverseasDispatch = {} }
return { context, candidates, calls, checks, orders, bundle: () => { const o = orders.get('DS-1'); return { data: view(o), warehouseId: '44', items: { items: clone(o.items) }, route: { stops: stops(o) } } } }
}
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('native route fixture uses real physical pallets and requires two destinations plus a repeated destination', () => {
const c = rows(), selected = selectDispatchRouteFixture(c)
assert.deepEqual(selected.selected.map(c => c.sourceKey), ['PLAN:21', 'PLAN:22']); assert.equal(selected.duplicate.sourceKey, 'PLAN:23')
assert.equal(selectDispatchRouteFixture(c.slice(0, 2)), null)
c[2].selection.warehousePalletId = c[0].selection.warehousePalletId; assert.equal(selectDispatchRouteFixture(c), null)
})
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('native route proof rejects drift in actual identity, physical scope, counts and destination keys', () => {
const h = server('', true), original = h.bundle(), expected = h.candidates.slice(0, 2)
assert.equal(dispatchRouteMatches(original, expected), true)
for (const mutate of [b => { b.warehouseId = '999' }, b => { b.items.items[0].customerId = '999' }, b => { b.items.items[0].warehousePalletId = '999' }, b => { b.items.items[0].sourceVersion = '0'.repeat(64) }, b => { b.data.counts.packageCount++ }, b => { b.route.stops[1].destinationKey = 'FBA:OTHER' }, b => { b.data.dispatchInfo.route = 'A1,A1,A2' }, b => { b.items.items[1].dispatchItemId = b.items.items[0].dispatchItemId }, b => { b.data.orderDetails[0].customerId = '999' }, b => { b.data.orderDetails[0].sourceKey = 'PLAN:999' }, b => { b.data.orderDetails[0].destinationFbaWarehouse = 'OTHER' }]) { const bad = clone(original); mutate(bad); assert.equal(dispatchRouteMatches(bad, expected), false) }
const changed = clone(original); changed.items.items[0].dispatchItemId = '9999'; assert.notEqual(dispatchBusinessSnapshot(changed), dispatchBusinessSnapshot(original))
})
test('C52 uses native versioned cargo commands, immutable replay, actual readback and route keys', async () => {
const h = server('', true), current = await verifyDispatchRouteLifecycle(h.context, { dispatchNo: 'DS-1', candidates: h.candidates.slice(0, 2), duplicate: h.candidates[2] })
assert.equal(h.context.createdResources.phase4OverseasDispatch.routeLifecycle.status, 'VERIFIED'); assert.ok(h.checks.every(c => c.passed))
assert.equal(current.data.counts.packageCount, 5); assert.equal(current.data.recordVersion, 6)
const firstAdd = h.calls.find(c => c.method === 'POST' && c.path.endsWith('/items')); assert.equal(firstAdd.body.recordVersion, 0)
assert.deepEqual(h.calls.filter(c => c.method === 'PUT').map(c => c.body.destinationKeys), [['FBA:A2', 'FBA:A1'], ['FBA:A2', 'FBA:A1'], ['FBA:A1', 'FBA:A2'], ['FBA:A1', 'FBA:A2']])
assert.equal(h.calls.some(c => c.method === 'DELETE'), false)
})
test('C52 rejects replay row drift, rejection side effects, wrong route and unknown writes without cleanup', async () => {
for (const fault of ['replay-row-drift', 'rejected-remove-mutates', 'route-drift', 'unknown-add']) {
const h = server(fault, true)
await assert.rejects(verifyDispatchRouteLifecycle(h.context, { dispatchNo: 'DS-1', candidates: h.candidates.slice(0, 2), duplicate: h.candidates[2] }))
assert.notEqual(h.context.createdResources.phase4OverseasDispatch.routeLifecycle.status, 'VERIFIED')
assert.equal(h.calls.some(c => c.method === 'PUT' || c.method === 'DELETE'), false)
if (fault === 'unknown-add') { assert.equal(h.context.createdResources.dispatchWriteBoundary.outcomeUnknown, true); assert.equal(h.calls.filter(c => c.method !== 'GET').length, 1); assert.equal(h.orders.get('DS-1').items.length, 3) }
}
})
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('phase4 missing current marker or real multi-destination fixture is explicit NOT_RUN before writes', async () => {
const h = server(); h.context.createdResources.phase4OverseasPda.marker = 'OLD'; assert.equal((await run(h.context)).status, 'NOT_RUN'); assert.equal(h.calls.length, 0)
const other = server(); other.candidates.pop(); assert.equal((await run(other.context)).status, 'NOT_RUN'); assert.equal(other.calls.filter(c => c.method !== 'GET').length, 0)
const ambiguous = server(); ambiguous.candidates[2].appointmentBatches = [{}, {}]; assert.equal((await run(ambiguous.context)).status, 'NOT_RUN'); assert.equal(ambiguous.calls.filter(c => c.method !== 'GET').length, 0)
})
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))
test('complete offline phase4 hands off the actual native Detail and current source keys at BEFORE_LOADING', async () => {
const directory = await mkdtemp(join(tmpdir(), 'phase4-dispatch-contract-'))
try {
const h = server(); delete h.context.checkpoint; h.context.options = { output: join(directory, 'report.json') }
h.context.createdResources.dispatchWriteBoundary = { marker: 'CURRENT', stopped: false, password: 'PRIVATE_PASSWORD', token: 'PRIVATE_LOGIN_TOKEN' }
const originalRequest = h.context.request
h.context.request = async (method, path, body) => {
if (method !== 'GET') {
const evidence = JSON.parse(await readFile(join(directory, 'phase4-dispatch-checkpoints', 'CURRENT.json'), 'utf8'))
const intents = [...(evidence.phase4OverseasDispatch.requests || []), ...(evidence.groupedDispatch?.requests || []), ...(evidence.phase4OverseasDispatch.routeLifecycle?.requests || [])]
assert.ok(intents.some(r => r.status === 'SENDING' && r.method === method && r.path === path && JSON.stringify(r.body) === JSON.stringify(body)), 'original write must already exist on disk')
}
return originalRequest(method, path, body)
}
const proof = await run(h.context)
assert.equal(proof.status, 'VERIFIED'); assert.equal(proof.finalStatus, 'ACTIVE'); assert.equal(proof.stage, 'BEFORE_LOADING'); assert.ok(h.checks.every(c => c.passed))
assert.deepEqual(proof.sourceKeys, ['PLAN:21', 'PLAN:22']); assert.deepEqual(proof.destinationWarehouseCodes, ['A1', 'A2']); assert.equal(proof.warehouseId, '44')
const actual = await h.context.request('GET', `${BASE}/${proof.dispatchNo}`); assert.deepEqual(proof.data, actual.body.data)
assert.equal(Object.hasOwn(proof.data, 'items'), false); assert.equal(Object.hasOwn(proof.data, 'route'), false); assert.equal(Object.hasOwn(proof, 'dispatchId'), false)
assert.equal(Object.hasOwn(proof, 'loadingMeasurements'), false); assert.equal(proof.loadingEvidenceStatus, 'NOT_RUN_MISSING_ACTUAL_MEASUREMENTS')
assert.equal(h.calls.some(c => /\/review|\/pick|\/scan|\/loading|\/depart|\/transfer-delivery/.test(c.path)), false)
assert.equal(h.calls.some(c => c.path === `${BASE}/pre-arrange` || c.body?.containersJson), false)
const text = await readFile(proof.checkpointPath, 'utf8'), journal = JSON.parse(text)
assert.equal(journal.phase4OverseasDispatch.status, 'VERIFIED'); assert.ok(journal.phase4OverseasDispatch.draftToken)
assert.equal(text.includes('PRIVATE_PASSWORD'), false); assert.equal(text.includes('PRIVATE_LOGIN_TOKEN'), false)
await assert.rejects(dispatchEvidenceContext({ marker: 'CURRENT', options: h.context.options, createdResources: { phase4OverseasDispatch: {} } }), /已存在本轮排车检查点/)
const stale = server('append-only-route'); await assert.rejects(run(stale.context), /最后实物移出后真实数量归零/)
assert.equal(stale.context.createdResources.phase4OverseasDispatch.status, 'UNPROVEN'); assert.equal(stale.orders.size, 1)
assert.equal(stale.calls.some(c => c.path.endsWith('/drafts')), false)
} finally {
assert.ok(basename(directory).startsWith('phase4-dispatch-contract-')); assert.equal(resolve(directory, '..'), resolve(tmpdir()))
await rm(directory, { recursive: true, force: true })
}
assert.doesNotMatch(source, /item\?\.cartonMarks/)
})
test('relabel creation cannot prematurely settle fulfillment or fabricate loading measurements', () => {
const source = readFileSync(new URL('./phase4-overseas-dispatch.mjs', import.meta.url), 'utf8')
assert.equal(source.includes('settleRelabelFulfillment'), false)
assert.equal(source.includes('vehicleAppointmentStatus'), false)
assert.equal(source.includes('loadingMeasurements:'), false)
assert.ok(source.includes('fixture.targetPalletIds')); assert.ok(source.includes('READY_FOR_OUTBOUND'))
})