240 lines
13 KiB
JavaScript
240 lines
13 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs'
|
|
import { sameOutboundTask } from './dropship-pallet-matrix.mjs'
|
|
import { receivingProgressMatches, outboundCandidateGuards, probePartialPick } from './dropship-outbound-edge-cases.mjs'
|
|
|
|
test('receiving verifier follows three independent inbound codes through interleaved progress', () => {
|
|
const expected = [{ inboundCode: 'FBA', total: 5, received: 0 },
|
|
{ inboundCode: 'DROPSHIP', total: 4, received: 0 }, { inboundCode: 'OVERSEAS', total: 3, received: 0 }]
|
|
for (const [index, count] of [[0, 2], [1, 1], [2, 3], [0, 3], [1, 3]]) {
|
|
expected[index].received += count
|
|
const snapshot = { totalPackages: 12, receivedPackages: expected.reduce((sum, x) => sum + x.received, 0),
|
|
pendingPackages: expected.reduce((sum, x) => sum + x.total - x.received, 0),
|
|
inbounds: expected.map(x => ({ inboundCode: x.inboundCode, totalPackages: x.total,
|
|
receivedPackages: x.received, pendingPackages: x.total - x.received })) }
|
|
assert.equal(receivingProgressMatches(snapshot, expected), true)
|
|
const stale = structuredClone(snapshot)
|
|
stale.inbounds[index].pendingPackages += 1
|
|
assert.equal(receivingProgressMatches(stale, expected), false)
|
|
const swapped = structuredClone(snapshot)
|
|
swapped.inbounds[0].receivedPackages += 1
|
|
swapped.inbounds[1].receivedPackages -= 1
|
|
assert.equal(receivingProgressMatches(swapped, expected), false, 'correct container total cannot conceal wrong ownership')
|
|
const duplicate = structuredClone(snapshot)
|
|
duplicate.inbounds[2] = duplicate.inbounds[0]
|
|
assert.equal(receivingProgressMatches(duplicate, expected), false)
|
|
}
|
|
assert.equal(receivingProgressMatches({ inbounds: [] }, []), false)
|
|
})
|
|
|
|
test('receiving verifier does not interpret absent or malformed progress as zero', () => {
|
|
const expected = [{ inboundCode: 'IN', total: 4, received: 0 }]
|
|
const good = { totalPackages: 4, receivedPackages: 0, pendingPackages: 4,
|
|
inbounds: [{ inboundCode: 'IN', totalPackages: 4, receivedPackages: 0, pendingPackages: 4 }] }
|
|
for (const value of [null, undefined, '', false, -1, 0.5]) {
|
|
const snapshot = structuredClone(good)
|
|
snapshot.inbounds[0].receivedPackages = value
|
|
assert.equal(receivingProgressMatches(snapshot, expected), false)
|
|
}
|
|
assert.equal(receivingProgressMatches(good, expected), true)
|
|
})
|
|
|
|
function candidateProbe(transform = rows => rows) {
|
|
const rows = ['P1', 'P2'].map((palletNo, index) => ({ palletNo, inventoryPositionId: index + 1,
|
|
inboundCode: 'IN-1', skuCode: 'SKU', availablePackages: 2, customerId: 'C1', overseasWarehouseId: 'W1' }))
|
|
const paths = []
|
|
return { paths, edge: { label: 'MULTI', customerId: 'C1', warehouseId: 'W1',
|
|
group: [{ label: 'MULTI', source: { inboundCode: 'IN-1' },
|
|
task: { pallets: [{ palletNo: 'P1' }, { palletNo: 'P2' }] }, expectedCartonMarks: ['a', 'b', 'c', 'd'] }],
|
|
check: (name, _response, passed) => assert.ok(passed, name),
|
|
call: async (_name, method, path) => {
|
|
assert.equal(method, 'GET'); paths.push(path)
|
|
return transform(structuredClone(rows), path)
|
|
},
|
|
} }
|
|
}
|
|
|
|
test('candidate probe sends the actual platform query without customer and checks both pallets', async () => {
|
|
const { edge, paths } = candidateProbe()
|
|
await outboundCandidateGuards(edge)
|
|
assert.equal(new URL(paths[0], 'https://example.invalid').searchParams.has('customerId'), false)
|
|
assert.equal(new URL(paths[1], 'https://example.invalid').searchParams.get('customerId'), 'C1')
|
|
})
|
|
|
|
for (const [name, transform] of [
|
|
['missing second pallet', rows => rows.slice(0, 1)],
|
|
['wrong warehouse', rows => rows.map(row => ({ ...row, overseasWarehouseId: 'W2' }))],
|
|
['wrong customer', rows => rows.map(row => ({ ...row, customerId: 'C2' }))],
|
|
['bad request response', () => ({ code: 'A0400', message: 'Bad request' })],
|
|
['candidate identity drift after filtering', (rows, path) => path.includes('customerId=')
|
|
? rows.map(row => ({ ...row, inventoryPositionId: row.inventoryPositionId + 10 })) : rows],
|
|
]) {
|
|
test(`candidate probe fails closed on ${name}`, async () => {
|
|
await assert.rejects(outboundCandidateGuards(candidateProbe(transform).edge))
|
|
})
|
|
}
|
|
|
|
test('browser fixture is explicit and stops before overseas receipt or outbound', () => {
|
|
assert.deepEqual(normalizeModuleOrder(['dropship-browser-fixture']), ['merchant-qualification-fixture', 'dropship-browser-fixture'])
|
|
const source = readFileSync(new URL('dropship-pallet-matrix.mjs', import.meta.url), 'utf8')
|
|
const branch = source.slice(source.indexOf('if (stopBeforeOverseasPda)'), source.indexOf("await verify(await prepare('SINGLE-MULTI'"))
|
|
assert.match(branch, /prepare\('BROWSER-MULTI', 2\)/)
|
|
assert.match(branch, /convertOverseas\(\[record\]\)/)
|
|
assert.match(branch, /READY_FOR_BROWSER_RECEIVING'[\s\S]*return/)
|
|
assert.doesNotMatch(branch, /receiveOverseas|outboundOverseas|createPicker/)
|
|
})
|
|
|
|
test('partial-pick rejection stays failed; a changed snapshot aborts further picking', async () => {
|
|
const failures = []
|
|
const snapshot = { pallets: [{ pickedPackageCount: 0 }] }
|
|
const edge = {
|
|
label: 'SINGLE-SINGLE', task: { outboundNo: 'OD', taskNo: 'PDA' },
|
|
pick: { packages: 4, cartonMarks: ['A', 'B', 'C', 'D'], idempotencyKey: 'pick' },
|
|
context: { assert: (_module, name, _response, passed) => { if (!passed) failures.push(name) },
|
|
requestOverseas: async () => ({ body: { code: 'A0409' } }) },
|
|
check: (_name, _response, passed) => assert.ok(passed),
|
|
call: async () => structuredClone(snapshot),
|
|
}
|
|
assert.equal(await probePartialPick(edge), 0)
|
|
assert.equal(failures.length, 1)
|
|
assert.match(failures[0], /四箱任务先拣一箱成功/)
|
|
let calls = 0
|
|
edge.call = async () => ({ pallets: [{ pickedPackageCount: calls++ }] })
|
|
await assert.rejects(probePartialPick(edge))
|
|
})
|
|
|
|
test('outbound replay preserves business identity and database millisecond timestamps', () => {
|
|
const task = { id: '1', taskNo: 'PDA1', outboundNo: 'OD1', status: 'PENDING', pushedAt: '2026-09-14T20:24:45.920278604' }
|
|
assert.equal(sameOutboundTask(task, { ...task, pushedAt: '2026-09-14T20:24:45.92' }), true)
|
|
assert.equal(sameOutboundTask({ ...task, pushedAt: '2026-09-14T20:24:45.9999' },
|
|
{ ...task, pushedAt: '2026-09-14T20:24:46' }), true)
|
|
for (const key of ['id', 'taskNo', 'outboundNo', 'status', 'pushedAt']) {
|
|
assert.equal(sameOutboundTask(task, { ...task, [key]: 'changed' }), false)
|
|
}
|
|
})
|
|
|
|
const source = readFileSync(new URL('./dropship-pallet-matrix.mjs', import.meta.url), 'utf8')
|
|
const edges = readFileSync(new URL('./dropship-outbound-edge-cases.mjs', import.meta.url), 'utf8')
|
|
|
|
test('matrix runs candidate probes before selecting inventory or creating outbound', () => {
|
|
const probe = source.indexOf('await outboundCandidateGuards(')
|
|
assert.ok(probe > 0)
|
|
assert.ok(probe < source.indexOf("const all = await call('读取专用出库可用库存'"))
|
|
})
|
|
|
|
test('each remaining pallet tests normal completion rejection and zero drift before receiving', () => {
|
|
const loop = source.slice(source.indexOf('for (const [index, sourcePalletNo] of sourceNos.entries())'),
|
|
source.indexOf("check(`${label}整柜实收总箱数守恒`"))
|
|
assert.match(loop, /allowDifference: false/)
|
|
assert.match(loop, /premature.body\?\.code === 'A0409'/)
|
|
assert.match(loop, /JSON.stringify\(unchanged\) === JSON.stringify\(before\)/)
|
|
assert.ok(loop.indexOf('JSON.stringify(unchanged)') < loop.indexOf("const received = await call"))
|
|
})
|
|
|
|
test('outbound edge probes preserve failures and require zero drift before continuing', () => {
|
|
assert.match(edges, /deploymentBlocker: true/)
|
|
assert.match(edges, /四箱任务先拣一箱成功/)
|
|
assert.match(edges, /分批拣货失败后无库存明细变化/)
|
|
assert.match(edges, /JSON.stringify\(before\) === JSON.stringify\(after\)/)
|
|
assert.match(source, /await probePartialPick/)
|
|
assert.match(source, /pick.packages -= count/)
|
|
assert.match(source, /pick.cartonMarks = pick.cartonMarks.slice\(count\)/)
|
|
})
|
|
|
|
test('outbound creation probes cover concurrent idempotency, invalid cartons and cancellation', () => {
|
|
assert.match(edges, /Promise.all\(\[create\(\), create\(\)\]\)/)
|
|
for (const scenario of ['超量创建', '错误SKU创建', '不存在箱唛创建', '重复箱唛创建',
|
|
'箱数与箱唛不符创建', '出库创建同键异载荷返回A0409', '未拣齐禁止完成出库', '部分已拣禁止取消']) {
|
|
assert.ok(edges.includes(scenario))
|
|
}
|
|
assert.match(edges, /取消后逐票SKU可用箱数恢复/)
|
|
assert.match(source, /await outboundCreationGuards\(edge\)/)
|
|
assert.match(source, /await outboundTaskGuards\(\{ ...edge, task \}\)/)
|
|
})
|
|
|
|
test('two inbound orders explicitly share a SKU without losing their separate identities', () => {
|
|
assert.match(source, /label.startsWith\('MULTI-MERGE-'\)/)
|
|
assert.match(source, /skuCodes = \[`SKU-SHARED-\$\{marker\}`/)
|
|
assert.match(source, /row.inboundCode === candidate.inboundCode/)
|
|
})
|
|
|
|
test('dedicated outbound uses exact source cartons and verifies completion and stock deduction', () => {
|
|
assert.match(source, /await outboundOverseas\(group, picker.token\)/)
|
|
assert.match(source, /one-click-dropship\?customerId=/)
|
|
assert.match(source, /position.inboundCode === candidate.inboundCode/)
|
|
assert.match(source, /cartonMarks: inputs\[index\].cartonMarks/)
|
|
assert.match(source, /outbound.outbound\?\.status === 'OUTBOUNDED'/)
|
|
assert.match(source, /!remaining.some/)
|
|
})
|
|
|
|
test('dropship matrix has a real customer prerequisite without generic dispatch fixtures', () => {
|
|
const modules = normalizeModuleOrder(['dropship-pallet-matrix'])
|
|
assert.ok(modules.includes('merchant-qualification-fixture'))
|
|
assert.ok(modules.includes('dropship-pallet-matrix'))
|
|
assert.ok(!modules.includes('phase5-overseas-fulfillment'))
|
|
})
|
|
|
|
test('all four physical pallet shapes are explicitly produced', () => {
|
|
assert.match(source, /prepare\('SINGLE-MULTI', 2\)/)
|
|
assert.match(source, /prepare\('SINGLE-SINGLE', 1\)/)
|
|
assert.match(source, /prepare\('SINGLE-MERGE', 2, \{ mergeToFirstPallet: true \}\)/)
|
|
assert.match(source, /prepare\('MULTI-MERGE-A', 1, \{ deferSeal: true \}\)/)
|
|
assert.match(source, /prepare\('MULTI-MERGE-B', 1, \{ targetPalletNo \}\)/)
|
|
assert.match(source, /await verify\(first, 1\)/)
|
|
assert.match(source, /await verify\(second, 1\)/)
|
|
})
|
|
|
|
test('allocation checks both physical summaries and uncollapsed per-inbound quantities', () => {
|
|
assert.match(source, /first-leg\/loading-plans\/\$\{plan.id\}/)
|
|
assert.match(source, /const actual = detail.items.filter/)
|
|
assert.match(source, /plan.items\?\.length === pallets.length/)
|
|
assert.match(source, /actual.reduce\([\s\S]*?record.expectedCartonMarks.length/)
|
|
assert.match(source, /await conserve\('手工加入后'\)/)
|
|
assert.match(source, /await conserve\('智能推荐后'\)/)
|
|
assert.match(source, /fullFlowVerified: false/)
|
|
})
|
|
|
|
test('each scenario reaches physical loading through released booking and asynchronous completion', () => {
|
|
assert.match(source, /ocean-booking-link/)
|
|
assert.match(source, /readiness\?\.canConfirm === true/)
|
|
assert.match(source, /action: 'PICK_PALLET'/)
|
|
assert.match(source, /inventorySnapshotToken: inventory.snapshotToken/)
|
|
for (const type of ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL']) {
|
|
assert.ok(source.includes(`'${type}'`))
|
|
}
|
|
assert.match(source, /await waitForCompletion\(context/)
|
|
assert.match(source, /finalTask\?\.taskStatus === 'COMPLETED'/)
|
|
assert.match(source, /inbound.summary\?\.processStatus === 'LOADED'/)
|
|
})
|
|
|
|
test('shipping uses the current group and requires real departure and pickup readiness', () => {
|
|
assert.match(source, /for \(const group of groups\) await progressShipping\(group\)/)
|
|
assert.match(source, /containerBookingId: String\(record.loading.cabinet.id\)/)
|
|
assert.match(source, /\['customs', completeCustoms\]/)
|
|
assert.match(source, /\['departure', confirmDomesticDeparture\]/)
|
|
assert.match(source, /\['pickup-ready', runUntilPickupReady\]/)
|
|
assert.match(source, /confirmedStatus === 'WAITING_FOR_LOADING'/)
|
|
assert.match(source, /phase4CarrierPickupReady\?\.ready === true/)
|
|
})
|
|
|
|
test('overseas plans preserve per-inbound ownership and shared physical identity', () => {
|
|
assert.match(source, /inbound-plans\/convert-container/)
|
|
assert.match(source, /matching.length === 1/)
|
|
assert.match(source, /JSON.stringify\(expectedNos\) === JSON.stringify\(actualNos\)/)
|
|
assert.match(source, /byPallet.get\(pallet.palletNo\) === identity/)
|
|
assert.match(source, /JSON.stringify\(replay\) === JSON.stringify\(plans\)/)
|
|
})
|
|
|
|
test('overseas receipt uses scoped PDA identity and scans each physical pallet once', () => {
|
|
assert.match(source, /await createPicker\(context/)
|
|
assert.match(source, /await disablePicker\(context/)
|
|
assert.match(source, /const sourceNos = \[\.\.\.new Set\(group.flatMap/)
|
|
assert.match(source, /current\/receipts\/pallet/)
|
|
assert.match(source, /duplicate.result === 'ALREADY_RECEIVED'/)
|
|
assert.match(source, /Number\(inbound.receivedPackages\) === item.expectedCartonMarks.length/)
|
|
assert.match(source, /completed.status === 'COMPLETED'/)
|
|
assert.match(source, /fullFlowVerified: false/)
|
|
})
|