Files

263 lines
15 KiB
JavaScript
Raw Permalink Normal View History

2026-09-14 18:34:37 +08:00
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'
2026-09-15 14:18:18 +08:00
import { receivingProgressMatches, outboundCandidateGuards, probePartialPick } from './dropship-outbound-edge-cases.mjs'
test('loading confirmation assigns the authenticated PDA operator from the target warehouse', () => {
const source = readFileSync(new URL('./dropship-pallet-matrix.mjs', import.meta.url), 'utf8')
assert.match(source, /domestic-pda-operators\?warehouseId=\$\{summary\.warehouseId\}/)
assert.match(source, /selectSessionPdaOperator\(operators, context\.pdaToken\)/)
assert.match(source, /assigneeId: String\(operator\.userId\)/)
})
2026-09-15 14:18:18 +08:00
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))
})
}
2026-09-15 10:37:34 +08:00
2026-09-15 13:36:58 +08:00
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/)
})
2026-09-15 10:37:34 +08:00
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) },
request: async () => ({ body: { code: 'A0409' } }) },
2026-09-15 10:37:34 +08:00
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)
}
})
2026-09-14 18:34:37 +08:00
const source = readFileSync(new URL('./dropship-pallet-matrix.mjs', import.meta.url), 'utf8')
2026-09-15 10:37:34 +08:00
const edges = readFileSync(new URL('./dropship-outbound-edge-cases.mjs', import.meta.url), 'utf8')
2026-09-15 14:18:18 +08:00
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"))
})
2026-09-15 10:37:34 +08:00
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/)
})
2026-09-14 18:34:37 +08:00
2026-09-14 20:20:57 +08:00
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/)
})
2026-09-14 18:34:37 +08:00
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'))
})
2026-09-16 21:22:18 +08:00
test('physical-carton chain covers mixed SKU cartons without multiplying carton totals', () => {
const modules = normalizeModuleOrder(['dropship-physical-carton-chain'])
assert.deepEqual(modules, ['merchant-qualification-fixture', 'dropship-physical-carton-chain'])
assert.match(source, /prepare\('PHYSICAL-20X2', 2[\s\S]*totalPackageCount: 20, skuCount: 2/)
assert.match(source, /prepare\('PHYSICAL-5X3', 1[\s\S]*totalPackageCount: 5, skuCount: 3/)
assert.match(source, /prepare\('PHYSICAL-7X2-MERGED', 2, \{ mergeToFirstPallet: true \}/)
assert.match(source, /carton\.skuLines\.length === \(record\.expectedSkuCount \|\| 1\)/)
assert.match(source, /packagesByPhysicalUnit/)
assert.match(source, /new Set\(actual\.map\(\(item\) => item\.skuCode\)/)
assert.match(source, /PHYSICAL_CARTON_OUTBOUND_COMPLETED/)
})
2026-09-14 18:34:37 +08:00
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/)
2026-09-16 21:22:18 +08:00
assert.match(source, /packagesByPhysicalUnit\.values\(\)[\s\S]*?record.expectedCartonMarks.length/)
2026-09-14 18:34:37 +08:00
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, /装柜前保存柜级预计到仓时间/)
assert.match(source, /capacityCbm: 68, maxWeightKg: 19500, estimatedWarehouseArrivalAt/)
assert.ok(source.indexOf('装柜前保存柜级预计到仓时间') < source.indexOf('读取自动生成装柜计划'))
2026-09-14 18:34:37 +08:00
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\)/)
})
2026-09-14 20:03:22 +08:00
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/)
2026-09-17 10:21:46 +08:00
assert.match(source, /stage = 'PHYSICAL_CARTON_OUTBOUND_COMPLETED'[\s\S]*?fullFlowVerified = true/)
2026-09-14 20:03:22 +08:00
})