119 lines
9.9 KiB
JavaScript
119 lines
9.9 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { feeCents, assertDualDestinationFixture, extensionMatrix, verifyLoadingReplayRace, executeFirstLoadingRace, assertBolPages, assertConfirmedRelabelCancellation } from './dispatch-workflow-extension-acceptance.mjs'
|
|
import { parseExtensionArguments, runDispatchExtensions } from '../dispatch-workflow-extensions-test-env.mjs'
|
|
|
|
test('default plan imports without credentials/network and keeps all unexecuted gaps explicit', async () => {
|
|
const plan = await runDispatchExtensions(parseExtensionArguments([]), {})
|
|
assert.equal(plan.execute, false); assert.equal(plan.matrix.length, 5)
|
|
assert.equal(extensionMatrix.find(row => row.scenario === 'relabel').readiness, 'READY_FORMAL_THREE_BOX_RECEIPT')
|
|
})
|
|
test('execution requires fresh explicit TEST scope and declared synthetic load, never coerces a plan', () => {
|
|
const args = ['--execute', '--environment', 'TEST', '--run-id', 'EXT1', '--scenario', 'dual-destination', '--warehouse-id', '10',
|
|
'--destination-warehouse-id', '20', '--pda-env-file', 'private.env', '--synthetic-pallet-spaces', '1']
|
|
assert.equal(parseExtensionArguments(args).execute, true)
|
|
for (const [from, to] of [['TEST', 'DEV'], ['EXT1', 'plan'], ['dual-destination', 'unsupported'], ['10', ''], ['1', '0']])
|
|
assert.throws(() => parseExtensionArguments(args.map(value => value === from ? to : value)))
|
|
assert.throws(() => parseExtensionArguments([...args, '--execute']))
|
|
})
|
|
test('terminal continuation requires an explicit original report hash and is never an implicit replay', () => {
|
|
assert.throws(() => parseExtensionArguments(['--scenario', 'review-terminal']))
|
|
assert.throws(() => parseExtensionArguments(['--resume-report', 'reports/old/report.json']))
|
|
const config = parseExtensionArguments(['--execute', '--environment', 'TEST', '--run-id', 'EXTCLOSE1', '--scenario', 'review-terminal',
|
|
'--warehouse-id', '10', '--destination-warehouse-id', '20', '--pda-env-file', 'private.env', '--synthetic-pallet-spaces', '1',
|
|
'--resume-report', 'reports/current/report.json', '--resume-report-sha256', 'a'.repeat(64)])
|
|
assert.equal(config.scenario, 'review-terminal'); assert.equal(config.resumeReportSha256, 'a'.repeat(64))
|
|
})
|
|
test('dual destination proof rejects reused customer, collapsed destination and missing route stop', () => {
|
|
const fixture = { members: [{ customerId: 'A', destination: { key: 'FBA:ONE' }, receiptVerified: true },
|
|
{ customerId: 'B', destination: { key: 'FBA:TWO' }, receiptVerified: true }],
|
|
tripProof: { stops: [{}, { destinationKey: 'FBA:ONE' }, { destinationKey: 'FBA:TWO' }] } }
|
|
assert.doesNotThrow(() => assertDualDestinationFixture(fixture))
|
|
for (const mutate of [f => { f.members[1].customerId = 'A' }, f => { f.members[1].destination.key = 'FBA:ONE' },
|
|
f => { f.tripProof.stops.pop() }]) { const bad = structuredClone(fixture); mutate(bad); assert.throws(() => assertDualDestinationFixture(bad)) }
|
|
})
|
|
test('fee conservation uses integer cents and rejects unsafe, negative or overprecision amounts', () => {
|
|
assert.equal(feeCents('31.25'), 3125); assert.equal(feeCents(47.75), 4775)
|
|
for (const value of [null, false, -1, '1e2', '0.001', Number.MAX_SAFE_INTEGER]) assert.throws(() => feeCents(value))
|
|
})
|
|
|
|
for (const unknown of [false, true]) test(`bounded two-actor race ${unknown ? 'preserves unknown result and stops' : 'starts both requests before either returns and conserves inventory'}`, async () => {
|
|
const fixture = { dispatchNo: 'OWN', tripNo: 'TRIP', warehouseId: 'W', pdaToken: 'OWN_PDA_TOKEN', members: [{ customerId: 'A' }, { customerId: 'B' }] }
|
|
const context = { marker: 'CURRENT', createdResources: { dispatchWorkflowCompetition: { status: 'CHECKING' } },
|
|
checkpoint: async () => {}, assert: () => {} }
|
|
const acknowledgement = { recordVersion: '8', result: { confirmedLoad: { pallets: ['P1', 'P2'] } } }
|
|
const body = { recordVersion: '7', idempotencyKey: 'ORIGINAL', measurements: ['P1', 'P2'] }, requests = [], waiting = []
|
|
const send = async (method, path, payload, token) => {
|
|
requests.push({ method, path, payload: structuredClone(payload), token })
|
|
if (method === 'GET') return { http: 200, body: { code: '200', data: { immutable: path } } }
|
|
return new Promise((resolve, reject) => {
|
|
waiting.push({ resolve, reject, token })
|
|
if (waiting.length === 2) for (const entry of waiting) {
|
|
if (entry.token) entry.resolve({ http: 200, body: { code: '200', data: { ...acknowledgement, replayed: true } } })
|
|
else if (unknown) entry.reject(Error('Unknown write outcome; preserve original key'))
|
|
else entry.resolve({ http: 200, body: { code: 'A0403', message: '该请求标识不属于当前操作员' } })
|
|
}
|
|
})
|
|
}
|
|
const execute = () => verifyLoadingReplayRace(context, fixture, { readInventory: async scope => ({ ...scope, available: 0, picked: 1 }) }, send, body, acknowledgement)
|
|
if (unknown) await assert.rejects(execute(), /Unknown write outcome/)
|
|
else await execute()
|
|
assert.equal(waiting.length, 2)
|
|
assert.equal(requests.filter(row => row.method === 'POST').length, 2)
|
|
assert(requests.filter(row => row.method === 'POST').every(row => JSON.stringify(row.payload) === JSON.stringify(body)))
|
|
const proof = context.createdResources.dispatchWorkflowCompetition.loadingReplayRace
|
|
assert.equal(proof.status, unknown ? 'UNKNOWN_RESULT' : 'VERIFIED')
|
|
assert.equal(proof.firstExecutionRace, 'NOT_TESTED')
|
|
if (!unknown) assert.deepEqual(proof.before, proof.after)
|
|
})
|
|
|
|
test('BOL PDF evidence enforces empty/same/different appointment grouping and actual quantity conservation', () => {
|
|
const fixture = { members: [{ palletNo: 'PALLET_A', incomingNo: 'PALLET_A' }, { palletNo: 'PALLET_B', incomingNo: 'PALLET_B' }] }
|
|
const pdf = (isa, pallets, total) => `BILL OF LADING\nISA #: ${isa}\n${pallets.join('\n')}\n总箱数 / Total CTNS:\n${total}\nDriver signature`
|
|
assert.equal(assertBolPages([pdf('', ['PALLET_A', 'PALLET_B'], 2)], fixture, { appointments: [] }, 'bol-empty').totalPackages, 2)
|
|
assert.equal(assertBolPages([pdf('ISA1', ['PALLET_A', 'PALLET_B'], 2)], fixture,
|
|
{ appointments: [{ data: { confirmationNo: 'ISA1' } }] }, 'bol-same').pageCount, 1)
|
|
const appointments = { appointments: [{ data: { confirmationNo: 'ISA1' } }, { data: { confirmationNo: 'ISA2' } }] }
|
|
const pages = [pdf('ISA1', ['PALLET_A'], 1), pdf('ISA2', ['PALLET_B'], 1)]
|
|
assert.equal(assertBolPages(pages, fixture, appointments, 'bol-different').pageCount, 2)
|
|
for (const bad of [[pdf('ISA1', ['PALLET_A'], 2), pdf('ISA2', ['PALLET_B'], 2)],
|
|
[pdf('ISA1', ['PALLET_A', 'PALLET_B'], 1), pdf('ISA2', ['PALLET_B'], 1)],
|
|
[pdf('ISA1', ['PALLET_A'], 1), pdf('ISA1', ['PALLET_B'], 1)]])
|
|
assert.throws(() => assertBolPages(bad, fixture, appointments, 'bol-different'))
|
|
})
|
|
|
|
test('confirmed relabel cancellation conserves customer-confirmed results, method and full inventory while clearing only outbound association', () => {
|
|
const before = { application: { relabelNo: 'RL-OWN', customerConfirmedAt: '2026-10-20T15:00:00', status: 'READY_FOR_OUTBOUND',
|
|
deliveryMethod: 'PICKUP', fulfillmentStatus: 'OUTBOUND_CREATED', outboundOrderNo: 'OB-OWN' }, items: [{ id: 'I', fbaCartonMark: 'NEW', processedPackages: 1 }] }
|
|
const after = structuredClone(before); after.application.outboundOrderNo = null; after.application.fulfillmentStatus = 'WAIT_OUTBOUND_CREATION'
|
|
const outbound = { outbound: { outboundNo: 'OB-OWN', status: 'CANCELLED' } }, stock = { positions: [{ held: 1, available: 0 }], ledger: [{ immutable: 1 }] }
|
|
assert.doesNotThrow(() => assertConfirmedRelabelCancellation(before, after, outbound, stock, structuredClone(stock)))
|
|
for (const mutate of [a => { a.application.customerConfirmedAt = null }, a => { a.application.status = 'CANCELLED' },
|
|
a => { a.application.deliveryMethod = 'TRUCK' }, a => { a.application.outboundOrderNo = 'OB-OWN' }, a => { a.items[0].processedPackages = 0 }]) {
|
|
const bad = structuredClone(after); mutate(bad)
|
|
assert.throws(() => assertConfirmedRelabelCancellation(before, bad, outbound, stock, stock))
|
|
}
|
|
assert.throws(() => assertConfirmedRelabelCancellation(before, after, outbound, stock, { ...stock, ledger: [] }))
|
|
})
|
|
|
|
for (const winner of ['PDA', 'ADMIN']) test(`first valid loading CAS race records ${winner} winner and preserves the actual PDA receipt`, async () => {
|
|
const context = { marker: 'CURRENT', createdResources: { dispatchWorkflowCompetition: { status: 'CHECKING' } }, checkpoint: async () => {}, assert: () => {} }
|
|
const fixture = { dispatchNo: 'OWN', tripNo: 'TRIP', warehouseId: 'W', pdaToken: 'OWN_PDA', members: [{ customerId: 'A' }, { customerId: 'B' }] }
|
|
const body = { recordVersion: '7', idempotencyKey: 'ORIGINAL_PDA', manifestHash: 'ACTUAL_HASH' }
|
|
const success = { http: 200, body: { code: '200', data: { stage: 'DELIVERING', recordVersion: '8', result: { confirmedLoad: { manifestHash: 'ACTUAL_HASH' } } } } }
|
|
const rejected = { http: 200, body: { code: 'A0409' } }, pending = []; let completed = false
|
|
const send = async (method, path, payload, token) => {
|
|
if (method === 'GET') return { http: 200, body: { code: '200', data: { stage: completed ? 'DELIVERING' : 'LOADING',
|
|
recordVersion: completed ? '8' : '7', counts: { packageCount: 2, pickedPackageCount: 2 } } } }
|
|
return new Promise(resolve => {
|
|
pending.push({ resolve, token, payload })
|
|
if (pending.length === 2) { completed = true; for (const request of pending) request.resolve((request.token ? 'PDA' : 'ADMIN') === winner ? success : rejected) }
|
|
})
|
|
}
|
|
const outcome = await executeFirstLoadingRace(context, fixture, { readInventory: async scope => ({ ...scope, picked: 1 }) }, send, body)
|
|
assert.equal(pending.length, 2); assert.notEqual(pending[0].payload.idempotencyKey, pending[1].payload.idempotencyKey)
|
|
assert.equal(context.createdResources.dispatchWorkflowCompetition.firstLoadingRace.winner, winner)
|
|
assert.equal(outcome.pdaResponse, winner === 'PDA' ? success : rejected)
|
|
assert.equal(Boolean(outcome.error), winner === 'ADMIN')
|
|
})
|