Files
baiyue-firstleg/scripts/admin-test-env-modules/dropship-outbound-edge-cases.mjs
T

171 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Internal probes invoked only by the registered dropship matrix and its fresh fixtures.
// A failed expectation remains a deployment blocker. Continue other probes only when
// authoritative task/inventory snapshots prove the failed request changed no state.
const MODULE = 'dropship-pallet-matrix'
const OPTIONS = { scenario: 'dropship-outbound-edge-cases', deploymentBlocker: true }
const BASE = '/api/platform/v1/last-leg/outbound-orders'
// Expected progress comes from submitted fixture operations, never the server response.
export function receivingProgressMatches(snapshot, expected) {
const count = value => (typeof value === 'number' || typeof value === 'string' && /^\d+$/.test(value))
&& Number.isSafeInteger(Number(value)) && Number(value) >= 0 ? Number(value) : NaN
if (!Array.isArray(snapshot?.inbounds) || !Array.isArray(expected) || !expected.length) return false
if (new Set(expected.map(row => row.inboundCode)).size !== expected.length
|| snapshot.inbounds.length !== expected.length
|| new Set(snapshot.inbounds.map(row => row.inboundCode)).size !== expected.length) return false
let total = 0
let received = 0
for (const row of expected) {
if (!Number.isInteger(row.total) || !Number.isInteger(row.received)
|| row.total <= 0 || row.received < 0 || row.received > row.total) return false
const actual = snapshot.inbounds.find(item => item.inboundCode === row.inboundCode)
if (!actual || count(actual.totalPackages) !== row.total
|| count(actual.receivedPackages) !== row.received
|| count(actual.pendingPackages) !== row.total - row.received) return false
total += row.total
received += row.received
}
return count(snapshot.totalPackages) === total && count(snapshot.receivedPackages) === received
&& count(snapshot.pendingPackages) === total - received
}
function record(edge, name, response, passed, evidence = '') {
edge.context.assert(MODULE, `${edge.label}${name}`, response, passed, evidence, OPTIONS)
}
async function request(edge, method, path, body, pda = false) {
return edge.context.request(method, path, body, pda ? edge.token : edge.context.token, {})
}
async function candidates(edge) {
const rows = await edge.call('边界测试回查本轮库存候选', 'GET', edge.candidatePath)
return rows.filter(row => edge.group.some(x => x.source.inboundCode === row.inboundCode))
.map(row => ({ position: row.inventoryPositionId, sku: row.skuCode,
inbound: row.inboundCode, packages: row.availablePackages }))
.sort((a, b) => `${a.position}:${a.sku}`.localeCompare(`${b.position}:${b.sku}`))
}
async function taskSnapshot(edge) {
return edge.call('边界测试回查出库冻结事实', 'GET', `${BASE}/${edge.task.outboundNo}`)
}
export async function outboundCandidateGuards(edge) {
const warehouse = encodeURIComponent(edge.warehouseId)
const customer = encodeURIComponent(edge.customerId)
const paths = [
['平台首次查询不传客户', `${BASE}/one-click-dropship/candidates?warehouseId=${warehouse}`],
['平台指定客户查询', `${BASE}/one-click-dropship/candidates?customerId=${customer}&warehouseId=${warehouse}`],
]
const snapshots = []
for (const [name, path] of paths) {
const rows = await edge.call(name, 'GET', path)
edge.check(`${edge.label}${name}返回候选数组`, null, Array.isArray(rows))
edge.check(`${edge.label}${name}仓库隔离`, null,
rows.every(row => String(row.overseasWarehouseId) === String(edge.warehouseId)))
if (name === '平台指定客户查询') {
edge.check(`${edge.label}指定客户不返回其他客户库存`, null,
rows.every(row => String(row.customerId) === String(edge.customerId)))
}
const own = rows.filter(row => edge.group.some(x => x.source.inboundCode === row.inboundCode))
for (const record of edge.group) {
const selected = own.filter(row => row.inboundCode === record.source.inboundCode)
const expectedPallets = [...new Set(record.task.pallets.map(p => p.palletNo))].sort()
const actualPallets = [...new Set(selected.map(row => row.palletNo))].sort()
edge.check(`${record.label}${name}全部托盘与箱数完整`, null,
JSON.stringify(expectedPallets) === JSON.stringify(actualPallets)
&& selected.reduce((sum, row) => sum + Number(row.availablePackages), 0) === record.expectedCartonMarks.length
&& selected.every(row => String(row.customerId) === String(edge.customerId)))
}
snapshots.push(own.map(row => ({ inbound: row.inboundCode, pallet: row.palletNo,
position: String(row.inventoryPositionId), sku: row.skuCode, packages: Number(row.availablePackages) }))
.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))))
}
edge.check(`${edge.label}客户筛选前后本轮库存事实一致`, null,
JSON.stringify(snapshots[0]) === JSON.stringify(snapshots[1]))
}
export async function outboundCreationGuards(edge) {
const before = await candidates(edge)
const first = edge.body.items[0]
const cases = [
['超量创建', { ...first, packages: first.packages + 100, cartonMarks: [] }],
['错误SKU创建', { ...first, sku: `WRONG-${edge.context.marker}` }],
['不存在箱唛创建', { ...first, packages: 1, cartonMarks: [`MISSING-${edge.context.marker}`] }],
['重复箱唛创建', { ...first, packages: 2, cartonMarks: [first.cartonMarks[0], first.cartonMarks[0]] }],
['箱数与箱唛不符创建', { ...first, packages: first.packages + 1 }],
]
for (const [index, [name, input]] of cases.entries()) {
const response = await request(edge, 'POST', `${BASE}/one-click-dropship?customerId=${edge.customerId}`,
{ ...edge.body, items: [input], idempotencyKey: `${edge.body.idempotencyKey}-invalid-${index}` })
record(edge, `${name}返回A0409`, response, response.body?.code === 'A0409')
const after = await candidates(edge)
edge.check(`${edge.label}${name}后库存候选零漂移`, null, JSON.stringify(before) === JSON.stringify(after))
}
// Two real concurrent requests, same fixture and same key; no parallel fixture workers.
const body = { ...edge.body, idempotencyKey: `${edge.body.idempotencyKey}-race-cancel` }
const create = () => request(edge, 'POST', `${BASE}/one-click-dropship?customerId=${edge.customerId}`, body)
const responses = await Promise.all([create(), create()])
const tasks = responses.filter(r => r.body?.code === '200').map(r => r.body.data)
record(edge, '并发同键创建均成功且只生成一个任务', responses[1],
tasks.length === 2 && tasks[0].taskNo === tasks[1].taskNo && tasks[0].outboundNo === tasks[1].outboundNo)
edge.check(`${edge.label}并发创建至少一个可回查结果`, null, tasks.length > 0)
for (const outboundNo of new Set(tasks.map(t => t.outboundNo))) {
for (let replay = 0; replay < 2; replay++) {
const cancelled = await request(edge, 'POST', `${BASE}/${outboundNo}/cancel`, { reason: '本轮未拣取消及重放验收' })
edge.check(`${edge.label}未拣取消及重复取消成功`, cancelled, cancelled.body?.code === '200')
}
const detail = await edge.call('回查取消任务状态', 'GET', `${BASE}/${outboundNo}`)
edge.check(`${edge.label}取消后明细全部取消`, null, detail.outbound?.status === 'CANCELLED'
&& detail.pallets.every(p => p.status === 'CANCELLED' && Number(p.pickedPackageCount) === 0))
}
edge.check(`${edge.label}取消后逐票SKU可用箱数恢复`, null,
JSON.stringify(before) === JSON.stringify(await candidates(edge)))
}
export async function outboundTaskGuards(edge) {
const before = await taskSnapshot(edge)
const first = edge.body.items[0]
const changedBodies = [
['收件人', { ...edge.body, recipient: `${edge.body.recipient}-changed` }],
['备注', { ...edge.body, remark: `${edge.body.remark || ''}-changed` }],
['明细', { ...edge.body, items: [{ ...first, packages: first.packages + 1 }, ...edge.body.items.slice(1)] }],
['箱唛', { ...edge.body, items: [{ ...first, cartonMarks: [`CHANGED-${edge.context.marker}`, ...first.cartonMarks.slice(1)] }, ...edge.body.items.slice(1)] }],
]
for (const [field, body] of changedBodies) {
const changed = await request(edge, 'POST', `${BASE}/one-click-dropship?customerId=${edge.customerId}`, body)
record(edge, `出库创建同键异载荷返回A0409:${field}`, changed, changed.body?.code === 'A0409')
edge.check(`${edge.label}同键修改${field}后出库事实零漂移`, null,
JSON.stringify(before) === JSON.stringify(await taskSnapshot(edge)))
}
const path = `/api/pda/v1/last-leg/outbound-tasks/${edge.task.taskNo}`
const incomplete = await request(edge, 'POST', `${path}/complete`,
{ idempotencyKey: `${edge.body.idempotencyKey}-incomplete` }, true)
record(edge, '未拣齐禁止完成出库', incomplete, incomplete.body?.code === 'A0409')
edge.check(`${edge.label}未拣齐完成失败后零漂移`, null,
JSON.stringify(before) === JSON.stringify(await taskSnapshot(edge)))
}
export async function probePartialPick(edge) {
const before = await taskSnapshot(edge)
const path = `/api/pda/v1/last-leg/outbound-tasks/${edge.task.taskNo}/pick`
const partial = { ...edge.pick, packages: 1, cartonMarks: edge.pick.cartonMarks.slice(0, 1),
idempotencyKey: `${edge.pick.idempotencyKey}-partial` }
const result = await request(edge, 'POST', path, partial, true)
record(edge, '四箱任务先拣一箱成功', result, result.body?.code === '200')
const after = await taskSnapshot(edge)
if (result.body?.code !== '200') {
edge.check(`${edge.label}分批拣货失败后无库存明细变化`, null, JSON.stringify(before) === JSON.stringify(after))
// Keep the failed assertion; whole-item picking below only continues later coverage.
return 0
}
edge.check(`${edge.label}分批首箱只累计一箱`, null,
after.pallets.reduce((sum, p) => sum + Number(p.pickedPackageCount), 0) === 1)
const replay = await request(edge, 'POST', path, partial, true)
record(edge, '分批首箱同键重放成功', replay, replay.body?.code === '200')
edge.check(`${edge.label}分批重放不重复扣减`, null, JSON.stringify(after) === JSON.stringify(await taskSnapshot(edge)))
const cancel = await request(edge, 'POST', `${BASE}/${edge.task.outboundNo}/cancel`, { reason: '已拣取消拦截验收' })
record(edge, '部分已拣禁止取消', cancel, cancel.body?.code === 'A0409')
edge.check(`${edge.label}已拣取消失败后零漂移`, null, JSON.stringify(after) === JSON.stringify(await taskSnapshot(edge)))
return 1
}