Files
baiyue-firstleg/scripts/admin-test-env-modules/return-boundary-scenarios-contract.test.mjs
T

146 lines
9.6 KiB
JavaScript
Raw Normal View History

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { rejected, classifyRace, stableState, verifyNoMutation, exactReceiptLedger } from './return-boundary-scenarios.mjs'
import { run as createMerchant } from './merchant-qualification-fixture.mjs'
const response = (code = '200', http = 200) => ({ http, body: { code }, requestId: 'request', traceId: 'trace' })
test('拒绝断言只接受明确业务错误,不把网络故障、越权、成功当成参数拒绝', () => {
for (const code of ['A0400', 'A0422']) assert.equal(rejected(response(code), [400, 422]), true)
for (const code of ['200', 'A0403', 'A0409', 'S0500', 'S0503']) assert.equal(rejected(response(code), [400, 422]), false)
for (const http of [0, 401, 403, 500, 502, 503]) assert.equal(rejected(response('error', http), [400, 422]), false)
assert.equal(rejected({ ...response('A0409'), traceId: '' }, [409]), false)
})
test('并发判定区分双成功、双失败、服务故障与唯一成功', () => {
assert.deepEqual(classifyRace([response(), response('A0409')]), { successes: 1, conflicts: 1, unexpected: 0 })
assert.deepEqual(classifyRace([response(), response()]), { successes: 2, conflicts: 0, unexpected: 0 })
assert.deepEqual(classifyRace([response('A0409'), response('A0409')]), { successes: 0, conflicts: 2, unexpected: 0 })
assert.deepEqual(classifyRace([response(), response('S0500', 500)]), { successes: 1, conflicts: 0, unexpected: 1 })
})
test('库存快照保留订单、批次、SKU、来源与状态变化,仅忽略排序及展示时间', () => {
const baseline = [{ returnNo: 'R1', receiptBatchNo: 'B1', quantity: 2, status: 'AVAILABLE', skuTypes: 1, items: [{ skuCode: 'A', quantity: 2 }], updatedAt: 'old' }]
assert.deepEqual(stableState(baseline), stableState([{ ...baseline[0], updatedAt: 'new' }]))
for (const change of [{ returnNo: 'R2' }, { receiptBatchNo: 'B2' }, { quantity: 1 }, { status: 'MERGED' }, { skuTypes: 0 }, { items: [] }]) {
assert.notDeepEqual(stableState(baseline), stableState([{ ...baseline[0], ...change }]))
}
})
test('每个失败请求都立即前后回查,即使接口错误类型不符也检查落账', async () => {
for (const code of ['A0409', '200', 'S0500']) {
let quantity = 2
const events = [], checks = []
await verifyNoMutation({ name: '拒绝操作', snapshot: async () => { events.push('snapshot'); return { quantity } },
action: async () => { events.push('action'); quantity -= 1; return response(code) },
expected: result => rejected(result, [409]), check: (name, result, passed) => checks.push(passed) })
assert.deepEqual(events, ['snapshot', 'action', 'snapshot'])
assert.deepEqual(checks, [code === 'A0409', false])
}
})
test('缺少快照不得执行危险动作或伪造未落账通过', async () => {
let called = false
await assert.rejects(verifyNoMutation({ snapshot: async () => { throw new Error('snapshot unavailable') },
action: async () => { called = true }, check: () => {}, expected: () => true }), /snapshot unavailable/)
assert.equal(called, false)
})
test('扩展由退货主模块调用且不进入换标出库,保留各并发响应诊断及清理', async () => {
const [source, main] = await Promise.all(['return-boundary-scenarios.mjs', 'phase4-overseas-return-receipt.mjs']
.map(file => readFile(new URL(file, import.meta.url), 'utf8')))
assert.match(main, /await runReturnBoundaries\(context, \{ merchantToken, warehouse, picker, access \}\)/)
for (const name of ['跨客户隔离', '跨仓隔离', '并发签收', '并发合箱', '多单全量合箱与来源追溯', '多候选与输入边界']) assert.ok(source.includes(`scenario('${name}'`))
assert.match(source, /recordRace\('并发签收'/)
assert.match(source, /recordRace\('并发合箱'/)
assert.match(source, /recordRace\('并发部分合箱'/)
assert.match(source, /concurrencyRounds: 3/)
assert.match(source, /publishEnvironment: false/)
assert.match(source, /finally[\s\S]*disablePicker/)
assert.match(source, /executed: false, passed: false/)
assert.doesNotMatch(source, /\bfetch\s*\(|process\.env|relabel-applications|outbound-orders|outbound-tasks|\b(?:INSERT|UPDATE|SELECT)\s/i)
})
test('第二客户准备运行真实fixture编排但不替换主客户内存与环境凭据', async () => {
const names = ['MERCHANT_TEST_USERNAME', 'MERCHANT_TEST_PASSWORD']
const saved = names.map(name => process.env[name])
try {
process.env.MERCHANT_TEST_USERNAME = 'primary-user'
process.env.MERCHANT_TEST_PASSWORD = 'primary-placeholder'
const primary = { context: { merchantQualificationFixture: { tenantId: 'A' } }, createdResources: {} }
const secondary = Object.create(primary)
Object.assign(secondary, { marker: 'OFFLINE-20260915-B', options: {}, context: { ...primary.context }, createdResources: {},
findAvailablePhone: async () => '00000000000',
assert: (module, name, result, condition) => assert.ok(condition, name),
apiSuccess: (module, name, result) => { assert.equal(result.body.code, '200', name); return result.body.data },
requestWithBody: async () => ({ body: { code: '200', data: {} } }),
request: async (method, path) => {
let data
if (path.endsWith('/iam/tenants')) data = { id: 'B', administratorUsername: 'secondary-user', temporaryPassword: 'secondary-placeholder' }
else if (path.endsWith('/submit-review')) data = { status: 'PENDING' }
else if (path.endsWith('/iam/reviews')) data = [{ id: 'review-B', tenantId: 'B' }]
else if (path.endsWith('/dimension-initialization')) data = { status: 'READY' }
else if (path.endsWith('/approve')) data = {}
else if (path.endsWith('/qualifications')) data = ['BUSINESS_LICENSE', 'LEGAL_PERSON_ID'].map(qualificationType => ({ qualificationType, status: 'APPROVED' }))
else throw new Error(`Unexpected fixture request: ${method} ${path}`)
return { http: 200, body: { code: '200', data } }
} })
await createMerchant(secondary, { publishEnvironment: false })
assert.equal(primary.context.merchantQualificationFixture.tenantId, 'A')
assert.equal(secondary.context.merchantQualificationFixture.tenantId, 'B')
assert.equal(process.env.MERCHANT_TEST_USERNAME, 'primary-user')
assert.equal(process.env.MERCHANT_TEST_PASSWORD, 'primary-placeholder')
assert.ok(!JSON.stringify(secondary.createdResources).includes('secondary-placeholder'))
await createMerchant(secondary)
assert.equal(process.env.MERCHANT_TEST_USERNAME, 'secondary-user')
} finally {
names.forEach((name, index) => { if (saved[index] === undefined) delete process.env[name]; else process.env[name] = saved[index] })
}
})
test('流水断言逐行核对归属和业务引用,不能仅凭合计相等通过', () => {
const a = { customerId: 'A', overseasWarehouseId: 'W1', skuCode: 'SKU', movementType: 'RECEIPT', referenceNo: 'RR-1', totalDelta: 2, availableDelta: 2 }
const b = { ...a, referenceNo: 'RR-2', totalDelta: 1, availableDelta: 1 }
assert.equal(exactReceiptLedger({ total: 2, records: [b, a] }, [a, b]), true)
for (const change of [{ customerId: 'B' }, { overseasWarehouseId: 'W2' }, { skuCode: 'OTHER' },
{ referenceNo: 'RR-1' }, { movementType: 'MERGE' }, { totalDelta: 2 }, { availableDelta: 0 }]) {
assert.equal(exactReceiptLedger({ total: 2, records: [a, { ...b, ...change }] }, [a, b]), false)
}
assert.equal(exactReceiptLedger({ total: 3, records: [a, b] }, [a, b]), false)
assert.equal(exactReceiptLedger({ total: 0, records: [] }, []), true)
assert.equal(exactReceiptLedger(undefined, []), false)
})
2026-09-15 18:17:06 +08:00
test('真实退货验收检查五类完整编号且保留同键重放检查', async () => {
const source = await readFile(new URL('return-boundary-scenarios.mjs', import.meta.url), 'utf8')
for (const prefix of ['RT', 'RETU', 'RR', 'RB', 'RC']) {
assert.ok(source.includes(`/^${prefix}-[0-9A-Z]{25}$/`), prefix)
}
assert.ok(source.includes('同键并发合箱后重试仍返回原箱'))
assert.ok(source.includes('签收重放不重复产生流水'))
})
test('高价值扩展覆盖混合回滚、同键合箱、同码隔离、权限和流水且仍逐次回查', async () => {
const source = await readFile(new URL('return-boundary-scenarios.mjs', import.meta.url), 'utf8')
for (const name of ['混合明细整笔回滚', '同键并发合箱', '相同SKU跨客户跨仓隔离', '身份入口权限', '签收流水与余额核对']) {
assert.ok(source.includes(`scenario('${name}'`), name)
}
for (const evidence of ['后项超收', '后项未预报', '后项超库存', '后项不存在', 'additionalSummaries',
'同键并发合箱复用同一目标箱', '同码A签收不改变B客户或异仓数据', '无登录', '超收失败不留下流水']) assert.ok(source.includes(evidence), evidence)
assert.match(source, /snapshot: withLedger[\s\S]*签收重放不重复产生流水/)
assert.match(source, /sameKeyResponses/)
})
2026-09-20 18:51:10 +08:00
test('边界fixture遵守审核、单SKU托盘和完工照片新契约', async () => {
const source = await readFile(new URL('return-boundary-scenarios.mjs', import.meta.url), 'utf8')
assert.match(source, /customerByToken\.set\(tokenB, String\(fixtureB\.tenantId\)\)/)
assert.match(source, /'X-Customer-Id': customerId/)
assert.match(source, /status !== 'PENDING_REVIEW'/)
assert.match(source, /palletCode: order\.palletCode/)
assert.match(source, /requestOverseasMultipart\('POST',[^\n]+form, operator\.token\)/)
assert.match(source, /两个合法SKU同批签收仍整笔拒绝/)
assert.match(source, /palletCode: nextPallet\.palletCode/)
assert.doesNotMatch(source, /\['receiptNo', 'receiptBatchNo', 'cartonMark'\]/)
})