test(fleet): close pool cancellation capacity and isolation matrices
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { parsePoolArguments, poolPlan, runPoolAcceptance, assertPoolRemaining, assertSingleCommercial,
|
||||
shrinkingVehicleRequest, expiryWaitMs, poolLocalDateTime, poolPickScope, pickPoolDispatch, POOL_COVERAGE, POOL_REMAINING } from './fleet-capacity-pool-test-env.mjs'
|
||||
shrinkingVehicleRequest, expiryWaitMs, poolLocalDateTime, poolPickScope, pickPoolDispatch, POOL_COVERAGE, POOL_REMAINING,
|
||||
poolInquiryWrite, poolReadIsDenied, assertPoolBoundCapacity, verifyPoolConfirmedCancellation, verifyPoolBindingRevalidation } from './fleet-capacity-pool-test-env.mjs'
|
||||
|
||||
test('default and explicit plans make no network requests or invoke the mutating runner', async () => {
|
||||
const original = globalThis.fetch
|
||||
@@ -158,3 +159,104 @@ test('failed helper result stops the pool stage immediately instead of appending
|
||||
const context = { marker: 'R', steps: [] }
|
||||
await assert.rejects(pickPoolDispatch(context, 'test', { customerId: '9', dispatchNo: 'DS-A' }, async () => false), /PDA_PICK_FAILED:DS-A/)
|
||||
})
|
||||
|
||||
test('foreign read rejection requires actual access/not-found code and no leaked business payload', () => {
|
||||
for (const response of [{ http: 403, body: { code: 'A0403' } }, { http: 404, body: { code: 'A0404', data: null } },
|
||||
{ http: 200, body: { code: 'A0403' } }, { http: 200, body: { code: 'A0404' } }]) assert.equal(poolReadIsDenied(response), true)
|
||||
for (const response of [{ http: 200, body: { code: '200' } }, { http: 401 }, { http: 409 }, { http: 500 },
|
||||
{ http: 200, body: { code: 'A0409' } }, { http: 403, body: { data: { orderNo: 'PRIVATE' } } }]) assert.equal(poolReadIsDenied(response), false)
|
||||
})
|
||||
|
||||
test('owner rejection sends a valid current edit rather than malformed input', () => {
|
||||
const source = { tripRecordVersion: 5, offerRecordVersion: 7, recordVersion: 9, customerId: '42', businessType: 'US_TRUCK',
|
||||
deliveryMode: 'SELF_DELIVERY', estimatedArrivalAt: '2030-01-01T00:00:00Z', expectedPalletCount: 1,
|
||||
cargo: { destinationKey: 'FBA:1', packageCount: 1, palletSpaces: 4 }, bookingNo: 'FB-1', actions: { canEdit: false } }
|
||||
const body = poolInquiryWrite(source, 'CURRENT-KEY')
|
||||
assert.equal(body.idempotencyKey, 'CURRENT-KEY')
|
||||
assert.equal(body.tripRecordVersion, 5)
|
||||
assert.equal(body.offerRecordVersion, 7)
|
||||
assert.equal(body.recordVersion, 9)
|
||||
assert.deepEqual(body.cargo, source.cargo)
|
||||
assert.equal(body.customerId, '42')
|
||||
assert.equal(body.estimatedArrivalAt, source.estimatedArrivalAt)
|
||||
assert.equal('actions' in body, false)
|
||||
assert.equal('bookingNo' in body, false)
|
||||
})
|
||||
|
||||
test('BOUND accounting detects stale reservation, omitted actual pallet and wrong destination in every dimension', () => {
|
||||
const planned = { palletSpaces: 3, weightKg: 300, volumeCbm: 3 }, reserved = { palletSpaces: 4, weightKg: 100, volumeCbm: 1 }
|
||||
const capacity = { palletSpaces: 30, weightKg: 18000, volumeCbm: 90 }
|
||||
const remaining = { palletSpaces: 23, weightKg: 17600, volumeCbm: 86 }
|
||||
const offer = { status: 'OPEN', capacityKnown: true, canConfirm: true,
|
||||
segments: [{ fromIndex: 0, toIndex: 1, planned, reserved, remaining }], destinations: [{ destinationKey: 'FBA:1', remaining }] }
|
||||
assert.doesNotThrow(() => assertPoolBoundCapacity(offer, 'FBA:1', planned, reserved, capacity))
|
||||
for (const dimension of ['palletSpaces', 'weightKg', 'volumeCbm']) {
|
||||
for (const section of ['planned', 'reserved', 'remaining']) {
|
||||
const altered = structuredClone(offer); altered.segments[0][section][dimension]++
|
||||
assert.throws(() => assertPoolBoundCapacity(altered, 'FBA:1', planned, reserved, capacity), /DOUBLE_COUNT_OR_MISSING/)
|
||||
}
|
||||
}
|
||||
assert.throws(() => assertPoolBoundCapacity({ ...offer, canConfirm: false }, 'FBA:1', planned, reserved, capacity), /OPEN_KNOWN/)
|
||||
assert.throws(() => assertPoolBoundCapacity(offer, 'FBA:OTHER', planned, reserved, capacity), /DESTINATION/)
|
||||
})
|
||||
|
||||
test('binding check requires OPEN invalidation, replan still blocked, then explicit resume in that order', async () => {
|
||||
const events = [], check = (label, condition) => assert.ok(condition, label)
|
||||
let stage = 'INVALID'
|
||||
const snapshots = {
|
||||
INVALID: { status: 'OPEN', capacityKnown: false, canConfirm: false, blockReason: '计划容量待调度核对' },
|
||||
REPLANNED: { status: 'OPEN', capacityKnown: false, canConfirm: false, blockReason: '发布承诺已变化,请调度重新核对并恢复接单' },
|
||||
RESUMED: { status: 'OPEN', capacityKnown: true, canConfirm: true },
|
||||
}
|
||||
const result = await verifyPoolBindingRevalidation({ check,
|
||||
offerDetail: async () => { events.push(`GET-${stage}`); return { offer: snapshots[stage] } },
|
||||
blockedConfirm: async reason => { events.push(reason) },
|
||||
replan: async () => { events.push('REPLAN'); stage = 'REPLANNED' },
|
||||
resume: async current => { assert.equal(current, snapshots.REPLANNED); events.push('RESUME'); stage = 'RESUMED'; return snapshots.RESUMED },
|
||||
})
|
||||
assert.equal(result.canConfirm, true)
|
||||
assert.deepEqual(events, ['GET-INVALID', 'BIND-INVALIDATED', 'REPLAN', 'GET-REPLANNED', 'REPLANNED-NOT-RESUMED', 'RESUME'])
|
||||
})
|
||||
|
||||
test('closed publication or automatic resumption cannot masquerade as OPEN binding revalidation', async () => {
|
||||
const check = (label, condition) => assert.ok(condition, label)
|
||||
await assert.rejects(verifyPoolBindingRevalidation({ check, offerDetail: async () => ({ offer: { status: 'CLOSED', canConfirm: false } }),
|
||||
replan: () => assert.fail('must stop before replan') }), /remains OPEN/)
|
||||
let reads = 0
|
||||
await assert.rejects(verifyPoolBindingRevalidation({ check,
|
||||
offerDetail: async () => ({ offer: ++reads === 1 ? { status: 'OPEN', capacityKnown: false, canConfirm: false, blockReason: '计划容量待调度核对' }
|
||||
: { status: 'OPEN', capacityKnown: true, canConfirm: true } }), replan: async () => {}, blockedConfirm: async () => {},
|
||||
resume: () => assert.fail('must stop before resume') }), /cannot silently resume/)
|
||||
})
|
||||
|
||||
test('confirmed cancellation verifies both commercial types and reuses exact successful cancellation request', async () => {
|
||||
for (const businessType of ['LOCAL_LOOSEBOARD', 'US_TRUCK']) {
|
||||
let current = { bookingNo: 'FB-1', offerNo: 'FO-1', customerId: '9', businessType, recordVersion: 1, status: 'DRAFT' }
|
||||
let cancellationBody, cancellationCount = 0, commercialStatus = 'WAITING_INBOUND'
|
||||
const offer = count => ({ capacityKnown: true, destinations: [{ destinationKey: 'FBA:1', remaining: { palletSpaces: count } }] })
|
||||
const request = async (method, path, body) => {
|
||||
if (method === 'POST' && path.endsWith('/confirm')) {
|
||||
current = { ...current, status: 'CONFIRMED', businessOrderNo: 'BUSINESS-1', recordVersion: 2 }
|
||||
return { inquiry: { ...current }, offer: offer(11) }
|
||||
}
|
||||
if (method === 'POST' && path.endsWith('/cancel')) {
|
||||
cancellationCount++
|
||||
if (cancellationBody) assert.equal(body, cancellationBody)
|
||||
else cancellationBody = body
|
||||
commercialStatus = 'CANCELLED'; current = { ...current, status: 'CANCELLED', recordVersion: 3 }
|
||||
return { inquiry: { ...current }, offer: offer(12), replayed: cancellationCount > 1 }
|
||||
}
|
||||
if (path === '/commercial') return { summary: { status: commercialStatus, fleetArrangement: { bookingNo: 'FB-1' } } }
|
||||
if (path.endsWith('/FO-1')) return { offer: offer(12) }
|
||||
assert.fail(`unexpected ${method} ${path}`)
|
||||
}
|
||||
const result = await verifyPoolConfirmedCancellation({ inquiry: { ...current }, destinationKey: 'FBA:1', request,
|
||||
check: (label, condition) => assert.ok(condition, label), latest: async () => ({ ...current }),
|
||||
command: (row, label) => ({ recordVersion: row.recordVersion, idempotencyKey: label }), businessPath: () => '/commercial',
|
||||
dispatcherToken: 'offline', label: businessType })
|
||||
assert.equal(result.businessType, businessType)
|
||||
assert.equal(result.status, 'CANCELLED')
|
||||
assert.equal(cancellationCount, 2)
|
||||
assert.equal(result.remainingPalletSpaces, 12)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user