test(fleet): isolate pick commands per customer dispatch
This commit is contained in:
@@ -58,6 +58,22 @@ export function assertSingleCommercial(before, after) {
|
||||
&& before.customerId === after.customerId, 'COMMERCIAL_REPLAY_IDENTITY_CHANGED')
|
||||
}
|
||||
|
||||
export function poolPickScope(context, member) {
|
||||
requireThat(context?.marker && /^[1-9]\d*$/.test(id(member?.customerId)) && member?.dispatchNo,
|
||||
'EXACT_CUSTOMER_DISPATCH_PICK_SCOPE_REQUIRED')
|
||||
const scoped = Object.create(context)
|
||||
// pickAll keys only include its context marker and item/position indexes; the PDA key scope is customer-wide.
|
||||
scoped.marker = `POOL-PICK-${createHash('sha256').update(JSON.stringify([context.marker, id(member.customerId), member.dispatchNo])).digest('hex').slice(0, 32)}`
|
||||
return scoped
|
||||
}
|
||||
|
||||
export async function pickPoolDispatch(context, token, member, pick = pickAll) {
|
||||
const scoped = poolPickScope(context, member)
|
||||
const passed = await pick(scoped, token, member.dispatchNo)
|
||||
requireThat(passed === true, `PDA_PICK_FAILED:${member.dispatchNo}`)
|
||||
return scoped.marker
|
||||
}
|
||||
|
||||
export function shrinkingVehicleRequest(vehicle) {
|
||||
requireThat(vehicle?.id && Number.isInteger(Number(vehicle.recordVersion)) && Number(vehicle.maxPalletSpaces) > 1
|
||||
&& Number(vehicle.maxLoadKg) > 0 && Number(vehicle.maxVolumeCbm) > 0, 'COMPLETE_OWNED_VEHICLE_SNAPSHOT_REQUIRED')
|
||||
@@ -167,16 +183,16 @@ async function poolScenario(context, fixture, { execute = false, readInventory,
|
||||
const tripProof = context.createdResources.fleetTrips.find(row => row.tripNo === fixture.tripNo)
|
||||
const tripPath = `${API}/fleet/trips/${encode(fixture.tripNo)}`
|
||||
const key = label => createHash('sha256').update(`${context.marker}:POOL:${label}`).digest('hex')
|
||||
const check = (label, ok) => { context.assert(MODULE, label, null, Boolean(ok), '', { scenario: MODULE, deploymentBlocker: true }); requireThat(ok, label) }
|
||||
const check = (label, ok, response = null) => { context.assert(MODULE, label, response, Boolean(ok), '', { scenario: MODULE, deploymentBlocker: true }); requireThat(ok, label) }
|
||||
const raw = (method, path, body, token = context.token) => context.request(method, path, body, token)
|
||||
const request = async (method, path, body, token = context.token) => {
|
||||
const response = await raw(method, path, body, token)
|
||||
check(`${method} ${path}`, success(response) && response.requestId && response.traceId)
|
||||
check(`${method} ${path}`, success(response) && response.requestId && response.traceId, response)
|
||||
return response.body.data
|
||||
}
|
||||
const reject = async (label, method, path, body, token) => {
|
||||
const response = await raw(method, path, body, token)
|
||||
check(label, denied(response) && response.requestId && response.traceId)
|
||||
check(label, denied(response) && response.requestId && response.traceId, response)
|
||||
return response
|
||||
}
|
||||
const permissions = await request('GET', `${IAM}/permissions`)
|
||||
@@ -303,8 +319,10 @@ async function poolScenario(context, fixture, { execute = false, readInventory,
|
||||
detail = await request('GET', tripPath)
|
||||
for (const binding of detail.dispatches) detail = await request('POST', `${tripPath}/dispatches/${encode(binding.dispatchNo)}/review`, { recordVersion: detail.trip.recordVersion, decision: 'APPROVED' })
|
||||
const members = [...fixture.members, ...poolMembers]
|
||||
proof.pickScopes = []
|
||||
for (const member of members) {
|
||||
await pickAll(context, fixture.pdaToken, member.dispatchNo)
|
||||
const namespace = await pickPoolDispatch(context, fixture.pdaToken, member)
|
||||
proof.pickScopes.push({ customerId: member.customerId, dispatchNo: member.dispatchNo, namespace })
|
||||
const task = await request('GET', `/api/pda/v1/last-leg/dispatches/${encode(member.dispatchNo)}`, undefined, fixture.pdaToken)
|
||||
check('Every owned and external pallet was actually picked', task.items.every(row => Number(row.pickedPackages) === Number(row.scheduledPackages)))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { parsePoolArguments, poolPlan, runPoolAcceptance, assertPoolRemaining, assertSingleCommercial,
|
||||
shrinkingVehicleRequest, expiryWaitMs, poolLocalDateTime, POOL_COVERAGE, POOL_REMAINING } from './fleet-capacity-pool-test-env.mjs'
|
||||
shrinkingVehicleRequest, expiryWaitMs, poolLocalDateTime, poolPickScope, pickPoolDispatch, POOL_COVERAGE, POOL_REMAINING } 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
|
||||
@@ -106,3 +106,55 @@ test('LocalDateTime transport time keeps actual local wall clock while Instant d
|
||||
assert.equal(new LosAngelesDate(instant).toISOString(), instant)
|
||||
assert.throws(() => poolLocalDateTime(new Date('invalid')), /VALID_LOCAL_DATE_REQUIRED/)
|
||||
})
|
||||
|
||||
test('PDA namespace belongs to the exact run, customer and dispatch while parent evidence stays shared', () => {
|
||||
const context = { marker: 'ORIGINAL-RUN', steps: [], createdResources: {} }, first = { customerId: '9', dispatchNo: 'DS-A' }
|
||||
const a = poolPickScope(context, first), replay = poolPickScope(context, first)
|
||||
assert.equal(a.marker, replay.marker)
|
||||
assert.notEqual(a.marker, poolPickScope(context, { ...first, dispatchNo: 'DS-B' }).marker)
|
||||
assert.notEqual(a.marker, poolPickScope(context, { ...first, customerId: '10' }).marker)
|
||||
assert.notEqual(a.marker, poolPickScope({ ...context, marker: 'OTHER-RUN' }, first).marker)
|
||||
assert.equal(a.steps, context.steps)
|
||||
assert.equal(a.createdResources, context.createdResources)
|
||||
assert.equal(context.marker, 'ORIGINAL-RUN')
|
||||
assert.throws(() => poolPickScope(context, { dispatchNo: 'DS-A' }), /PICK_SCOPE_REQUIRED/)
|
||||
})
|
||||
|
||||
test('real pickAll helper handles two same-customer dispatches with separate pick/complete keys and exact replays', async () => {
|
||||
const operations = new Map(), requests = [], steps = [], picked = new Map()
|
||||
const dispatches = ['DS-A', 'DS-B']
|
||||
const context = {
|
||||
marker: 'SAME-CUSTOMER-RUN', steps, createdResources: {},
|
||||
addStep(_module, _name, _response, passed) { steps.push(passed) },
|
||||
assert(_module, _name, _response, passed) { assert.equal(Boolean(passed), true) },
|
||||
addCoverageGap() { assert.fail('unexpected coverage gap') },
|
||||
async request(method, path, body) {
|
||||
const dispatchNo = path.split('/dispatches/')[1].split('/')[0], index = dispatches.indexOf(dispatchNo)
|
||||
assert.ok(index >= 0)
|
||||
const response = data => ({ http: 200, body: { code: '200', data }, requestId: 'test', traceId: 'test' })
|
||||
if (method === 'GET') return response({ items: [{ dispatchItemId: `${index + 1}`, scheduledPackages: 1,
|
||||
pickedPackages: picked.get(dispatchNo) || 0, positions: [{ id: `${index + 100}`, availablePackages: 1 }] }] })
|
||||
requests.push({ method, path, body: structuredClone(body) })
|
||||
const prior = operations.get(body.idempotencyKey), payload = JSON.stringify([path, body])
|
||||
if (prior) {
|
||||
if (prior.payload !== payload) return { http: 200, body: { code: 'A0409', message: 'Idempotency key was reused with a different request' } }
|
||||
return response({ ...prior.result, replayed: true })
|
||||
}
|
||||
let result
|
||||
if (path.endsWith('/pick')) { picked.set(dispatchNo, 1); result = { pickedPackages: 1 } }
|
||||
else result = { status: 'READY_TO_DEPART', plannedPackages: 1, pickedPackages: 1, remainingPackages: 0, replayed: false }
|
||||
operations.set(body.idempotencyKey, { payload, result })
|
||||
return response(result)
|
||||
},
|
||||
}
|
||||
for (const dispatchNo of dispatches) await pickPoolDispatch(context, 'test-pda', { customerId: '9', dispatchNo })
|
||||
assert.equal(operations.size, 4)
|
||||
assert.equal(requests.length, 8)
|
||||
assert.ok(steps.every(Boolean))
|
||||
for (let index = 0; index < requests.length; index += 2) assert.deepEqual(requests[index], requests[index + 1])
|
||||
})
|
||||
|
||||
test('failed helper result stops the pool stage immediately instead of appending a misleading generic failure', async () => {
|
||||
const context = { marker: 'R', steps: [] }
|
||||
await assert.rejects(pickPoolDispatch(context, 'test', { customerId: '9', dispatchNo: 'DS-A' }, async () => false), /PDA_PICK_FAILED:DS-A/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user