Files
baiyue-firstleg/scripts/client-test-env-acceptance-contract.test.mjs
T

318 lines
23 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import vm from 'node:vm'
import { createHash } from 'node:crypto'
import { inflateRawSync, deflateRawSync } from 'node:zlib'
const source = await readFile(new URL('./client-test-env-acceptance.mjs', import.meta.url), 'utf8')
test('client registration distinguishes incomplete-registration replay from credential conflicts', () => {
assert.match(source, /same credentials replay the incomplete registration/)
assert.match(source, /\['accountId', 'tenantId', 'membershipId'\]/)
assert.match(source, /String\(json\.data\[field\]\), String\(registration\.json\.data\[field\]\)/)
assert.match(source, /duplicate username with different credentials is rejected/)
assert.match(source, /body: \{ \.\.\.registrationBody, password: `Different/)
})
const helperSource = source.slice(source.indexOf('// BEGIN ORDER ACCEPTANCE HELPERS'), source.indexOf('// END ORDER ACCEPTANCE HELPERS'))
const workflowSource = source.slice(source.indexOf('async function ownedOrderSnapshot('), source.indexOf('async function apiStep('))
function runtime(extra = {}) {
const context = vm.createContext({ Buffer, createHash, inflateRawSync, structuredClone, Blob, FormData,
assertEqual: (actual, expected, label) => assert.equal(actual, expected, label),
assertTrue: (value, label) => assert.ok(value, label),
assertPresent: (value, label) => assert.ok(value !== null && value !== undefined && value !== '', label),
requireSuccess: json => assert.equal(String(json?.code), '200'), ...extra })
vm.runInContext(`${helperSource}\n${workflowSource}`, context)
return context
}
const plain = value => JSON.parse(JSON.stringify(value))
test('frozen edit command preserves original version, key and nested payload through retries', () => {
const r = runtime(), snapshot = { order: { version: 7 } }, payload = { draft: { cargoes: [{ skuNo: 'ORIGINAL' }] } }
const command = r.orderMutationCommand(snapshot, payload, 'original-key')
snapshot.order.version = 99; payload.draft.cargoes[0].skuNo = 'CHANGED'
assert.deepEqual(plain(command), { headers: { 'Idempotency-Key': 'original-key' }, body: { expectedVersion: 7, draft: { cargoes: [{ skuNo: 'ORIGINAL' }] } } })
assert.ok(Object.isFrozen(command.body.draft.cargoes[0]))
assert.throws(() => r.orderMutationCommand({ order: {} }, payload, 'key'), /version/)
assert.throws(() => r.orderMutationCommand(snapshot, payload, ''), /key/)
})
test('submit command carries only frozen expectedVersion with the same original replay key',()=>{
const r=runtime(),detail={order:{version:4}}
const command=r.orderMutationCommand(detail,{},'submit-original')
detail.order.version=9
assert.deepEqual(plain(command),{headers:{'Idempotency-Key':'submit-original'},body:{expectedVersion:4}})
assert.match(source,/const submitCommand = orderMutationCommand\(submitBefore, \{\}, submitKey\)/)
assert.match(source,/submit replay keeps version and history unchanged/)
assert.match(source,/submit same key different version conflicts/)
assert.match(source,/submit new key cannot submit already submitted order/)
for(const name of ['pre-approval-submit','incomplete-submit','cancelled-submit','cancelled-submit-proof']) {
assert.match(source,new RegExp(`orderMutationCommand\\([^\\n]+(?:\\n[^\\n]+)?newIdempotencyKey\\('${name}'\\)`))
}
})
test('zero-drift validator detects changes in every aggregate and preserves array ordering', () => {
const r = runtime(), before = { order: { version: 2 }, cargoes: [{ id: '1', amount: 2 }], parties: [], internalNotes: [], statusHistory: [{ id: 'h1' }, { id: 'h2' }] }
r.assertOrderUnchanged(before, structuredClone(before))
for (const field of Object.keys(before)) {
const changed = structuredClone(before)
if (field === 'order') changed.order.version++
else changed[field].push({ id: 'new' })
assert.throws(() => r.assertOrderUnchanged(before, changed), /unchanged/)
}
assert.throws(() => r.assertOrderUnchanged(before, { ...before, statusHistory: [...before.statusHistory].reverse() }), /unchanged/)
r.assertOrderUnchanged({ b: 2, a: 1 }, { a: 1, b: 2 })
})
test('partial import validates physical row, exact counts, failure details and identities', () => {
const r = runtime(), data = { mode: 'PARTIAL', batchNo: 'batch', sourceFileId: 'f', errorReportFileId: 'e', result: {
total: 2, succeeded: 1, failed: 1, items: [
{ index: 0, clientRowId: 'excel-row-3', success: false, orderId: null, errorCode: 'A0422', errorMessage: 'cargo required' },
{ index: 1, clientRowId: 'excel-row-4', success: true, orderId: '99', orderNo: 'FL99' },
],
} }
r.assertImportResult(data, 'PARTIAL', [false, true])
for (const mutate of [
value => value.result.succeeded++, value => value.result.items[0].clientRowId = 'excel-row-2',
value => value.result.items[0].orderId = 'unexpected', value => value.result.items[0].errorCode = 'S0500',
value => value.result.items[1].orderNo = '', value => value.errorReportFileId = null,
]) {
const changed = structuredClone(data); mutate(changed)
assert.throws(() => r.assertImportResult(changed, 'PARTIAL', [false, true]))
}
})
test('PARTIAL wire response accepts only null or omitted failed-row identifiers', () => {
const r = runtime()
// Shape captured from r1: Jackson omits all null fields on both failed and successful rows.
const wire = JSON.parse('{"mode":"PARTIAL","batchNo":"OBI-contract","sourceFileId":"101","errorReportFileId":"102","result":{"total":2,"succeeded":1,"failed":1,"items":[{"index":0,"clientRowId":"excel-row-3","success":false,"errorCode":"A0422","errorMessage":"货物名称不能为空"},{"index":1,"clientRowId":"excel-row-4","success":true,"orderId":"2106016937331458049","orderNo":"FL2106016937331458049"}]}}')
r.assertImportResult(wire, 'PARTIAL', [false, true])
for (const field of ['orderId', 'orderNo', 'inboundCode']) {
const explicitNull = structuredClone(wire); explicitNull.result.items[0][field] = null
r.assertImportResult(explicitNull, 'PARTIAL', [false, true])
for (const value of ['2106016937331458049', 'FL-example', 'NYEXX-example', '', 0, false, NaN, [], {}]) {
const invalid = structuredClone(wire); invalid.result.items[0][field] = value
assert.throws(() => r.assertImportResult(invalid, 'PARTIAL', [false, true]), new RegExp(`no ${field}`))
}
}
for (const field of ['orderId', 'orderNo']) {
const missingSuccess = structuredClone(wire); delete missingSuccess.result.items[1][field]
assert.throws(() => r.assertImportResult(missingSuccess, 'PARTIAL', [false, true]), /created order/)
}
})
function templateFixture(r) {
return new Map([
['xl/workbook.xml', '<workbook><sheets><sheet name="订单导入"/><sheet name="选项数据"/></sheets></workbook>'],
['xl/sharedStrings.xml', '<sst><si><t>运输方式 *</t></si><si><t>客户备注</t></si><si><t>仓库|CODE</t></si></sst>'],
['xl/worksheets/sheet1.xml', '<worksheet><sheetData><row r="1"><c r="A1" t="inlineStr"><is><t>订单基础</t></is></c></row><row r="2"><c r="A2" t="s"><v>0</v></c><c r="B2"/><c r="AE2" t="s"><v>1</v></c></row><row r="3"><c r="A3" t="inlineStr"><is><t>old data</t></is></c></row></sheetData><dataValidations count="1"/></worksheet>'],
['xl/worksheets/sheet2.xml', '<worksheet><sheetData><row r="1"><c r="A1" t="s"><v>2</v></c></row></sheetData></worksheet>'],
])
}
test('XLSX mutation preserves downloaded headers/options and writes 31 columns past Z', () => {
const r = runtime(), template = templateFixture(r), row = Array.from({ length: 31 }, (_, i) => `column-${i}`)
row[30] = '备注<&"\'中文'
const bytes = r.orderWorkbookRows(template, [row]), parsed = r.orderZipRead(bytes), rows = r.orderSheetRows(parsed)
assert.equal(parsed.get('xl/worksheets/sheet2.xml'), template.get('xl/worksheets/sheet2.xml'))
assert.match(parsed.get('xl/worksheets/sheet1.xml'), /dataValidations/)
assert.deepEqual(plain(rows.find(item => item.number === 2).cells.map(cell => cell.text)), ['运输方式 *', '', '客户备注'])
assert.equal(rows[2].cells[30].ref, 'AE'); assert.equal(rows[2].cells[30].text, row[30])
assert.doesNotMatch(parsed.get('xl/worksheets/sheet1.xml'), /old data/)
assert.deepEqual(plain(r.orderSheetRows(parsed, 'xl/worksheets/sheet2.xml')[0].cells), [{ ref: 'A', text: '仓库|CODE' }])
const corrupted = Buffer.from(bytes); corrupted[80] ^= 1
assert.throws(() => r.orderZipRead(corrupted))
assert.throws(() => r.orderZipRead(Buffer.from('not zip')))
})
test('ZIP reader accepts independently compressed entries and rejects damaged checksums', () => {
const r = runtime(), data = Buffer.from('<worksheet>中文</worksheet>'), name = Buffer.from('xl/worksheets/sheet1.xml')
const compressed = deflateRawSync(data)
let checksum = 0xffffffff
for (const byte of data) { checksum ^= byte; for (let i = 0; i < 8; i++) checksum = checksum & 1 ? 0xedb88320 ^ (checksum >>> 1) : checksum >>> 1 }
checksum = (checksum ^ 0xffffffff) >>> 0
const local = Buffer.alloc(30); local.writeUInt32LE(0x04034b50); local.writeUInt16LE(8, 8); local.writeUInt16LE(name.length, 26)
const central = Buffer.alloc(46); central.writeUInt32LE(0x02014b50); central.writeUInt16LE(8, 10)
central.writeUInt32LE(checksum, 16); central.writeUInt32LE(compressed.length, 20); central.writeUInt32LE(data.length, 24); central.writeUInt16LE(name.length, 28)
const end = Buffer.alloc(22); end.writeUInt32LE(0x06054b50); end.writeUInt16LE(1, 10); end.writeUInt32LE(local.length + name.length + compressed.length, 16)
const archive = Buffer.concat([local, name, compressed, central, name, end])
assert.equal(r.orderZipRead(archive).get(name.toString()), data.toString())
central.writeUInt32LE((checksum ^ 1) >>> 0, 16)
assert.throws(() => r.orderZipRead(Buffer.concat([local, name, compressed, central, name, end])), /checksum/)
})
test('rollback plan changes only row 3 validity and row 4 hash without moving physical rows', () => {
const r = runtime(), first = Array.from({ length: 31 }, (_, index) => `first-${index}`), second = Array.from({ length: 31 }, (_, index) => `second-${index}`)
const plan = r.importRollbackRows(first, second)
assert.equal(plan.seed[0][10], ''); assert.deepEqual(plain(plan.seed[1]), second)
assert.deepEqual(plain(plan.conflict[0]), first)
assert.deepEqual(plain(plan.conflict[1].slice(0, 30)), second.slice(0, 30))
assert.notEqual(plan.conflict[1][30], second[30]); assert.deepEqual(plain(plan.retry), [first, second])
const rows = r.orderSheetRows(r.orderZipRead(r.orderWorkbookRows(templateFixture(r), plan.conflict)))
assert.deepEqual(plain(rows.slice(2).map(row => row.number)), [3, 4])
})
test('actual Java path derives row keys without mode and joins both writes to the atomic transaction', async () => {
const service = await readFile(new URL('../order/src/main/java/com/baiyue/order/service/MerchantBatchDraftService.java', import.meta.url), 'utf8')
const item = await readFile(new URL('../order/src/main/java/com/baiyue/order/service/MerchantBatchDraftItemService.java', import.meta.url), 'utf8')
const orders = await readFile(new URL('../order/src/main/java/com/baiyue/order/service/OrderApplicationService.java', import.meta.url), 'utf8')
assert.match(service, /batchKey\.trim\(\) \+ "\\n" \+ rowId/)
assert.match(service, /@Transactional\s+public MerchantBatchDraftDtos.Response createAtomic/)
assert.match(service, /itemService.createJoined\(tenantId, userId, username, item.draft\(\),\s*derivedKey\(batchKey, item.clientRowId\(\)\)\)/)
assert.match(item, /@Transactional\(propagation = Propagation.MANDATORY\)/)
assert.match(orders, /requestHash\("CREATE_DRAFT", request\)/)
})
function mutationHarness(fault = '') {
let nextId = 100, nextKey = 0, nextHistory = 0
const draft = { customerRemark: 'original', cargoes: [{ skuNo: 'SKU', packageCount: 2, model: 'M', productImageFileId: 'img' }] }
const make = (id, status, body) => ({ order: { id, orderNo: `FL${id}`, inboundCode: null, tenantId: 'tenant', customerId: 'customer', version: 0, status, customerRemark: body.customerRemark },
cargoes: structuredClone(body.cargoes), parties: [], internalNotes: [], statusHistory: [] })
const orders = new Map([['submitted', make('submitted', 'SUBMITTED', draft)]]), commands = new Map(), requests = [], report = { resources: { orderIds: [] } }
const r = runtime({ merchantToken: 'owner', existingClientToken: 'other', marker: 'local-contract', report,
orderAcceptanceFixture: { payload: draft, orderId: 'submitted' }, newIdempotencyKey: () => `key-${++nextKey}`,
apiStep: async (module, name, request, validate) => {
requests.push({ name, request: structuredClone(request) })
const id = request.path.split('/')[6], current = orders.get(id)
let code = '200', data = null
if (!request.method || request.method === 'GET') data = structuredClone(current)
else if (request.path.endsWith('/first-leg')) {
data = String(nextId++); orders.set(data, make(data, 'DRAFT', request.body))
} else if (request.token === 'other') code = 'A0404'
else if (request.path.endsWith('/submit')) code = 'A0409'
else {
const key = request.headers?.['Idempotency-Key'], payload = request.body
const signature = JSON.stringify(payload), stored = commands.get(key)
if (!key) code = 'A0400'
else if (payload.expectedVersion == null || (request.method === 'POST' && !payload.reason?.trim()) || payload.draft?.cargoes[0].productImageFileId === -1) code = 'A0422'
else if (stored) {
code = stored === signature ? '200' : 'A0409'
if (fault === 'replay-write' && code === '200') current.order.version++
} else if (payload.expectedVersion !== current.order.version || current.order.status !== 'DRAFT') code = 'A0409'
else {
commands.set(key, signature); current.order.version++
const cancelled = request.method === 'POST'
if (cancelled) current.order.status = 'CANCELLED'
else { current.order.customerRemark = payload.draft.customerRemark; current.cargoes = structuredClone(payload.draft.cargoes) }
current.statusHistory.push({ id: `h${++nextHistory}`, action: cancelled ? 'CANCELLED' : 'UPDATE', fromStatus: 'DRAFT', toStatus: current.order.status,
reason: payload.reason ?? null, operatorId: 'owner', occurredAt: '2026-10-02T12:00:00' })
}
}
if (fault === 'rejection-drift' && code !== '200' && current) current.parties.push({ id: 'unexpected' })
const response = { status: 200, json: { code, data } }; validate(response); return response
},
})
return { r, requests, report, orders }
}
test('F06 actual orchestration covers frozen success replay, stale versions, missing protocol and isolation', async () => {
const { r, requests, report } = mutationHarness()
await r.orderMutationEvidenceModule()
const firstEdit = requests.find(item => item.name === 'versioned draft edit succeeds').request
const editReplay = requests.find(item => item.name === 'original edit key and frozen payload replay successfully').request
assert.deepEqual(editReplay, firstEdit)
const firstCancel = requests.find(item => item.name === 'cancel owned draft with reason').request
const cancelReplay = requests.find(item => item.name === 'original cancellation key payload and version replay successfully').request
assert.deepEqual(cancelReplay, firstCancel)
const newCancel = requests.find(item => item.name === 'repeat cancellation cannot add history').request
assert.notEqual(newCancel.headers['Idempotency-Key'], firstCancel.headers['Idempotency-Key'])
assert.ok(newCancel.body.expectedVersion > firstCancel.body.expectedVersion)
assert.ok(report.orderMutationRequests.every(item => !('token' in item)))
assert.ok(requests.some(item => item.name === 'edit missing version is rejected'))
assert.ok(requests.some(item => item.name === 'cancel missing key is rejected'))
})
for (const fault of ['replay-write', 'rejection-drift']) test(`F06 refuses false success when ${fault}`, async () => {
const { r } = mutationHarness(fault)
await assert.rejects(() => r.orderMutationEvidenceModule(), /unchanged/)
})
test('full order collection detects leaked rows, duplicated pages and truncated results', async () => {
for (const records of [[{ orderId: 'expected' }, { orderId: 'leaked' }], [{ orderId: 'expected' }, { orderId: 'expected' }]]) {
const r = runtime({ merchantToken: 'owner', apiStep: async (_module, _name, _request, validate) => {
validate({ json: { code: '200', data: { page: 1, total: 2, records } } })
} })
await assert.rejects(() => r.importOrderSet('rollback evidence', ['expected']))
}
const r = runtime({ merchantToken: 'owner', apiStep: async (_module, _name, _request, validate) => {
validate({ json: { code: '200', data: { page: 1, total: 201, records: [{ orderId: 'only-one' }] } } })
} })
await assert.rejects(() => r.importOrderSet('complete pagination'), /intermediate/)
})
function importHarness(fault = '') {
const report = { resources: { orderIds: [], tenantId: 'tenant' } }, orders = new Map(), requests = []
const masterData = { receiving: { id: 'domestic', warehouseName: '国内仓', warehouseCode: 'CN' },
destination: { id: 'fba', warehouseName: '目的仓', warehouseCode: 'US' }, channel: { channelName: '海运渠道', channelCode: 'SEA' } }
let nextKey = 0, r
const resultItem = (index, success, orderId) => Object.fromEntries(Object.entries({ index, clientRowId: `excel-row-${index + 3}`, success,
orderId: success ? orderId : null, orderNo: success ? `FL${orderId}` : null,
errorCode: success ? null : 'A0422', errorMessage: success ? null : 'cargo required' }).filter(([, value]) => value !== null))
const responseData = (mode, items) => ({ mode, batchNo: 'batch', sourceFileId: 'source-file', errorReportFileId: 'error-file',
result: { total: items.length, succeeded: items.filter(item => item.success).length, failed: items.filter(item => !item.success).length, items } })
const addDraft = (id, cells) => orders.set(id, { order: { id, orderNo: `FL${id}`, version: 0, status: 'DRAFT', tenantId: 'tenant',
customerRemark: cells[30], referenceId: cells[27], receivingWarehouseId: 'domestic', destinationWarehouseId: 'fba', channelCode: 'SEA' },
cargoes: [{ skuNo: cells[18], packageCount: 2 }], parties: [], internalNotes: [], statusHistory: [] })
const cells = (values, row) => values.map((value, index) => `<c r="${r.orderColumn(index)}${row}" t="inlineStr"><is><t>${r.orderXmlEncode(value)}</t></is></c>`).join('')
r = runtime({ report, marker: 'contract', merchantToken: 'owner', orderAcceptanceFixture: { masterData, payload: { pricingTaxMode: 'TAX_INCLUDED' } },
newIdempotencyKey: () => `key-${++nextKey}`,
apiStep: async (_module, name, request, validate) => {
requests.push({ name, path: request.path, key: request.headers?.['Idempotency-Key'] })
let code = '200', data = null, bytes = null
if (request.path.endsWith('/template')) {
const template = templateFixture(r)
const headers = Array.from({ length: 31 }, (_, index) => index === 0 ? '运输方式 *' : index === 30 ? '客户备注' : `字段${index}`)
template.set('xl/worksheets/sheet1.xml', `<worksheet><sheetData><row r="1">${cells(['订单基础'], 1)}</row><row r="2">${cells(headers, 2)}</row></sheetData></worksheet>`)
template.set('xl/worksheets/sheet2.xml', `<worksheet><sheetData><row r="1">${cells(['国内仓|CN', '目的仓|US', '海运渠道|SEA'], 1)}</row></sheetData></worksheet>`)
bytes = r.orderZipWrite(template)
} else if (request.path.includes('/error-reports/')) {
const physicalRow = fault === 'wrong-error-row' ? '4.0' : '3.0'
bytes = r.orderZipWrite(new Map([['xl/worksheets/sheet1.xml', `<worksheet><sheetData><row r="1">${cells(['Excel行号', '错误代码', '错误说明'], 1)}</row><row r="2">${cells(['3', 'A0422', 'cargo required'], 2).replace(/<c r="A2"[\s\S]*?<\/c>/, `<c r="A2" t="n"><v>${physicalRow}</v></c>`)}</row></sheetData></worksheet>`]]))
} else if (request.method === 'POST') {
const file = request.body.get('file'), body = Buffer.from(await file.arrayBuffer())
if (name.startsWith('PARTIAL')) {
const parsed = r.orderSheetRows(r.orderZipRead(body)).slice(2).map(row => row.cells.map(cell => cell.text))
if (!orders.has('second')) addDraft('second', parsed[1])
data = responseData('PARTIAL', [resultItem(0, false), resultItem(1, true, 'second')])
} else if (name.startsWith('ATOMIC retry')) {
const parsed = r.orderSheetRows(r.orderZipRead(body)).slice(2).map(row => row.cells.map(cell => cell.text))
addDraft('first', parsed[0]); data = responseData('ATOMIC', [resultItem(0, true, 'first'), resultItem(1, true, 'second')])
} else if (name.startsWith('ATOMIC later')) {
code = 'A0409'
if (fault === 'leaked-first-row') {
const parsed = r.orderSheetRows(r.orderZipRead(body)).slice(2).map(row => row.cells.map(cell => cell.text))
addDraft('leaked-first', parsed[0])
}
if (fault === 'changed-existing-row') orders.get('second').order.customerRemark = 'incorrectly-overwritten'
} else code = name === 'reject over five MiB' ? 'A0400' : 'A0422'
} else if (request.path.includes('?page=')) {
data = { page: 1, total: orders.size, records: [...orders.keys()].map(orderId => ({ orderId })) }
} else data = structuredClone(orders.get(request.path.split('/').at(-1)))
const response = { status: 200, headers: new Map([['content-type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']]), bytes, json: { code, data, message: code === 'A0400' ? '上传文件超过服务允许大小' : undefined } }
validate(response); return response
},
})
return { r, report, requests }
}
test('F04 runs actual import orchestration with the same seed/conflict/retry key and records limitations', async () => {
const { r, report, requests } = importHarness()
await r.orderExcelImportModule()
const keys = requests.filter(item => item.key && /PARTIAL preserves|PARTIAL same key|ATOMIC later|ATOMIC retry/.test(item.name)).map(item => item.key)
assert.equal(keys.length, 4); assert.equal(new Set(keys).size, 1)
assert.deepEqual(report.resources.orderIds, ['second', 'first'])
assert.ok(report.orderImportLimitations.some(item => item.includes('remote source uploads')))
assert.ok(requests.some(item => item.name === 'forged option adds no orders: complete order page 1'))
})
for (const fault of ['leaked-first-row', 'changed-existing-row']) test(`F04 rejects apparent ATOMIC failure with ${fault}`, async () => {
const { r } = importHarness(fault)
await assert.rejects(() => r.orderExcelImportModule(), /unchanged/)
})
test('F04 still rejects a numeric error row pointing to the successful row', async () => {
const { r } = importHarness('wrong-error-row')
await assert.rejects(() => r.orderExcelImportModule(), /actual Excel error row/)
})