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) }) 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/) })