Files
baiyue-firstleg/scripts/admin-test-env-modules/orders-lifecycle-idempotency-contract.test.mjs
T

239 lines
15 KiB
JavaScript
Raw Normal View History

import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { fixtureInboundCode, orderPersistenceFacts, requestedOrderDestinationType, freezeOrderCommand, verifyPlatformDraftEdit, verifyPlatformSubmitCancel,
buildOrderDependencyFaultPlan, assertOrderDependencyFaultObservation } from './orders.mjs'
function dependencyFixture() {
const detail = id => ({order:{id,orderNo:`FL${id}`,tenantId:'20',status:'DRAFT',version:4},
cargoes:[{skuNo:'current'}],parties:[],internalNotes:[],statusHistory:[]})
return {marker:'CURRENT-RUN',customerId:'20',registeredCustomerId:'20',merchant:detail('101'),platform:detail('102')}
}
function dependencyObservation(plan, fixture, dependency, role) {
const phase=plan.phases.find(row=>row.dependency===dependency),command=plan.commands[role]
const snapshot={detail:structuredClone(fixture[role]),task:null,inbounds:[],inboundPageComplete:true}
return {request:{method:command.method,path:command.path,body:structuredClone(command.body),headers:{...command.headers,'X-Request-Id':`${dependency}-${role}`}},
response:{http:200,body:{code:'S0503',message:phase.expectedMessages[0]}},before:snapshot,after:structuredClone(snapshot),
proxyRecords:[{requestId:`${dependency}-${role}`,method:'GET',path:phase.targetPath,matched:true,responseStatus:503,forwarded:false,internalTokenPresent:true}]}
}
test('dependency plan binds two new-customer drafts and only named Order upstream properties; it performs no runtime actions',()=>{
const fixture=dependencyFixture(),plan=buildOrderDependencyFaultPlan(fixture)
assert.equal(plan.runtimeChangesExecuted,false)
assert.deepEqual(plan.phases.map(row=>row.urlProperty),['baiyue.clients.iam.url','baiyue.clients.finance.url'])
assert.equal(plan.commands.merchant.body.expectedVersion,4)
fixture.merchant.order.version=9
assert.equal(plan.commands.merchant.body.expectedVersion,4)
assert.ok(Object.isFrozen(plan.commands.merchant.body))
for(const change of [f=>f.customerId='0',f=>f.registeredCustomerId='21',f=>f.platform.order.id='101',f=>f.merchant.order.status='SUBMITTED',f=>delete f.merchant.cargoes]) {
const value=dependencyFixture();change(value);assert.throws(()=>buildOrderDependencyFaultPlan(value))
}
})
test('all four dependency-role observations require matching authenticated 503 injection and actual Order S0503 with zero drift',()=>{
const fixture=dependencyFixture(),plan=buildOrderDependencyFaultPlan(fixture)
for(const dependency of ['IAM','FINANCE'])for(const role of ['merchant','platform']) {
const evidence=dependencyObservation(plan,fixture,dependency,role)
assert.equal(assertOrderDependencyFaultObservation(plan,dependency,role,evidence).verified,true)
const wrapped=structuredClone(evidence)
wrapped.response.body.message=plan.phases.find(row=>row.dependency===dependency).expectedMessages[1]
assert.equal(assertOrderDependencyFaultObservation(plan,dependency,role,wrapped).verified,true)
for(const mutate of [
value=>value.response.http=503,value=>value.response.body.code='A0403',value=>value.response.body.message='资格未审核',
value=>value.request.body.expectedVersion=5,value=>value.request.headers['Idempotency-Key']='new-key',
value=>value.proxyRecords=[],value=>value.proxyRecords[0].internalTokenPresent=false,value=>value.proxyRecords[0].forwarded=true,
value=>value.proxyRecords[0].path+='/other',value=>value.proxyRecords[0].requestId='other-request',
value=>value.proxyRecords.push({...value.proxyRecords[0],path:'/internal/iam/tenants/21/order-eligibility'}),
value=>value.after.detail.order.version++,value=>value.after.detail.statusHistory.push({id:'unexpected'}),
value=>value.after.task={orderId:plan.commands[role].orderId,tenantId:'20',id:'unexpected'},
value=>value.after.inbounds.push({tenantId:'20',orderNo:plan.commands[role].orderNo,id:'unexpected'}),
value=>value.after.inboundPageComplete=false,
]) { const changed=structuredClone(evidence);mutate(changed);assert.throws(()=>assertOrderDependencyFaultObservation(plan,dependency,role,changed)) }
}
})
function platformLifecycleFixture(fault = '', faultAction = 'submit') {
const drafts = new Map(), memo = new Map(), calls = []
let nextId = 10, historyId = 0
const context = { marker:'CURRENT-RUN', token:'platform', createdResources:{orders:{}},
apiSuccess(_module,name,response){assert.equal(response?.body?.code,'200',name);return response.body.data},
assert(_module,name,_response,passed){assert.ok(passed,name)},
async request(method,path,body,token,headers={}) {
const url = new URL(path,'https://offline.invalid')
assert.equal(url.searchParams.get('tenantId'),'20')
if (method !== 'GET') assert.equal(token,'platform')
calls.push(structuredClone({method,path,body,headers}))
const match = url.pathname.match(/\/first-leg\/(\d+)(?:\/(submit|cancel))?$/)
if (method === 'POST' && !match) {
const id = String(nextId++)
drafts.set(id,{order:{id,tenantId:'20',customerId:'20',orderNo:`FL${id}`,status:'DRAFT',version:0,customerRemark:body.customerRemark},
cargoes:structuredClone(body.cargoes),parties:[],internalNotes:[],statusHistory:[]})
return {http:200,body:{code:'200',data:id}}
}
assert.ok(match,path)
const [,id,action] = match, detail = drafts.get(id)
if (method === 'GET') return {http:200,body:{code:'200',data:structuredClone(detail)}}
if (method === 'PUT') {
assert.equal(body.expectedVersion,detail.order.version);assert.ok(headers['Idempotency-Key'])
detail.order.version++
detail.statusHistory.push({id:`h${++historyId}`,action:'UPDATE',fromStatus:'DRAFT',toStatus:'DRAFT',operatorId:'admin',occurredAt:'2026-10-02T00:00:00'})
return {http:200,body:{code:'200'}}
}
assert.ok(['submit','cancel'].includes(action))
const activeFault = action === faultAction ? fault : ''
const key = headers['Idempotency-Key'], cacheKey = `${id}/${action}/${key}`, fingerprint = JSON.stringify(body)
let code = '200'
if (!key) code = 'A0400'
else if (body.expectedVersion === undefined) code = 'A0422'
else if (memo.has(cacheKey)) {
code = memo.get(cacheKey) === fingerprint ? '200' : 'A0409'
if (activeFault === 'replay-write' && code === '200') detail.order.version++
} else if (body.expectedVersion !== detail.order.version) code = activeFault === 'stale-masked-by-validation' ? 'A0422' : 'A0409'
else if (detail.order.status !== 'DRAFT') code = 'A0409'
else {
memo.set(cacheKey,fingerprint)
detail.order.version++;detail.order.status = action === 'submit' ? 'SUBMITTED' : 'CANCELLED'
if (activeFault !== 'missing-success-history') detail.statusHistory.push({id:`h${++historyId}`,action:detail.order.status,
fromStatus:'DRAFT',toStatus:detail.order.status,reason:body.reason,operatorId:'admin',occurredAt:'2026-10-02T00:00:00'})
}
if (code !== '200' && activeFault === 'rejection-drift') detail.cargoes[0].packageCount++
return {http:200,body:{code}}
},
}
return {context,calls,drafts}
}
test('Platform submit/cancel use separate fresh drafts and freeze first valid requests through replay',async()=>{
const {context,calls,drafts} = platformLifecycleFixture()
const proofs = await verifyPlatformSubmitCancel(context,'20',{customerRemark:'main',cargoes:[{skuNo:'sku',packageCount:2}]},
{mode:'AUTO',manualEntryRequired:false})
assert.equal(drafts.size,2)
assert.deepEqual(proofs.map(proof=>proof.finalStatus),['SUBMITTED','CANCELLED'])
assert.notEqual(proofs[0].orderId,proofs[1].orderId)
for (const proof of proofs) {
const commands = proof.commands
assert.equal(commands.length,7)
assert.equal(commands[0].body.expectedVersion,0) // valid old page; not an invalid negative version
assert.equal(commands[1].body.expectedVersion,undefined)
assert.equal(commands[2].headers['Idempotency-Key'],undefined)
assert.equal(commands[2].body.expectedVersion,1)
assert.equal(commands[3].label,'success');assert.equal(commands[4].label,'replay')
assert.deepEqual(commands[4].body,commands[3].body)
assert.deepEqual(commands[4].headers,commands[3].headers)
assert.notDeepEqual(commands[5].body,commands[3].body)
assert.deepEqual(commands[5].headers,commands[3].headers)
assert.notEqual(commands[6].headers['Idempotency-Key'],commands[3].headers['Idempotency-Key'])
assert.equal(commands[6].body.expectedVersion,2)
assert.equal(proof.checks.length,5)
}
assert.ok(calls.every(call=>!call.path.includes('/approve')&&!call.path.includes('/start')))
})
for (const action of ['submit','cancel']) for (const fault of ['replay-write','rejection-drift','stale-masked-by-validation','missing-success-history']) {
test(`Platform ${action} evidence rejects ${fault}`,async()=>{
const {context} = platformLifecycleFixture(fault, action)
await assert.rejects(verifyPlatformSubmitCancel(context,'20',{cargoes:[{packageCount:2}]},
{mode:'AUTO',manualEntryRequired:false}),/零漂移|首次旧页面版本拒绝|唯一动作历史/)
})
}
test('order command freezes original detail version/key and never accepts an absent version',()=>{
const detail={order:{version:3}},draft={customerRemark:'original'}
const command=freezeOrderCommand(detail,'key',{draft})
detail.order.version=8;draft.customerRemark='changed'
assert.deepEqual(command,{body:{draft:{customerRemark:'original'},expectedVersion:3},headers:{'Idempotency-Key':'key'}})
assert.ok(Object.isFrozen(command.body.draft))
for(const version of [undefined,null,'3',-1])assert.throws(()=>freezeOrderCommand({order:{version}},'key'))
})
test('Platform edit uses exact customer and admin identity; replay/stale/foreign writes preserve full facts',async()=>{
let detail={order:{id:'10',tenantId:'20',status:'DRAFT',version:3,customerRemark:'marker'},cargoes:[],parties:[],internalNotes:[],statusHistory:[]}
let saved
const writes=[]
const context={marker:'marker',token:'admin',createdResources:{orders:{}},apiSuccess(_m,_n,r){assert.equal(r.body.code,'200');return r.body.data},
assert(_m,name,_r,ok){assert.ok(ok,name)},async request(method,path,body,token,headers){
assert.equal(path,'/api/platform/v1/orders/first-leg/10?tenantId=20')
if(method==='GET')return {http:200,body:{code:'200',data:structuredClone(detail)}}
writes.push({body,token,headers})
let code='200'
if(token!=='admin')code='A0403'
else if(saved)code=saved.key===headers['Idempotency-Key']&&saved.payload===JSON.stringify(body)?'200':'A0409'
else {assert.equal(body.expectedVersion,3);saved={key:headers['Idempotency-Key'],payload:JSON.stringify(body)};detail.order.version++;detail.order.customerRemark=body.draft.customerRemark;detail.statusHistory.push({action:'UPDATE'})}
return {http:200,body:{code}}
}}
const result=await verifyPlatformDraftEdit(context,'20','10','merchant',{customerRemark:'marker'})
assert.equal(result.order.version,4);assert.equal(writes.length,5)
assert.deepEqual(writes[1],writes[2]);assert.equal(writes[0].token,'merchant')
})
const source = readFileSync(new URL('orders.mjs', import.meta.url), 'utf8')
test('fresh fixture obeys the authoritative inbound-code policy without changing global settings', () => {
assert.deepEqual(fixtureInboundCode({ mode: 'AUTO', manualEntryRequired: false }, 'batch-A'), {})
const policy = { mode: 'LEGACY_MANUAL', manualEntryRequired: true }
const first = fixtureInboundCode(policy, 'batch-A')
assert.deepEqual(first, fixtureInboundCode(policy, 'batch-A'))
assert.notEqual(first.inboundCode, fixtureInboundCode(policy, 'batch-B').inboundCode)
assert.match(first.inboundCode, /^AT[A-F0-9]{20}$/)
assert.ok(`${first.inboundCode}#9999-9999`.length <= 34)
for (const invalid of [null, {}, { mode: 'AUTO', manualEntryRequired: true }, { mode: 'UNKNOWN', manualEntryRequired: false }]) {
assert.throws(() => fixtureInboundCode(invalid, 'batch-A'), /入仓码策略/)
}
assert.match(source, /GET', `\$\{createPath\}\/inbound-code-policy`/)
})
test('order demo payload covers multi-box multi-SKU images and both destination families', () => {
for (const boxType of ['SMALL', 'MEDIUM', 'LARGE', 'EXTRA_LARGE']) {
assert.match(source, new RegExp(`boxType: '${boxType}'`))
}
assert.match(source, /cargoes: selectedBoxes\.map/)
assert.match(source, /productImageFileId/)
assert.equal(requestedOrderDestinationType({ context: { orderDestinationType: 'FBA' } }), 'FBA')
assert.equal(requestedOrderDestinationType({ context: { orderDestinationType: 'OVERSEAS' } }), 'OVERSEAS')
})
test('create and submit lifecycle covers replay payload conflict and zero drift', () => {
assert.match(source, /Client 创建订单幂等重放/)
assert.match(source, /Client 创建订单同键异载荷返回A0409/)
assert.match(source, /创建同键异载荷失败后订单全事实零漂移/)
assert.match(source, /Client 提交订单同键同载荷精确重放/)
assert.match(source, /已提交订单使用新键重复提交返回A0409/)
assert.match(source, /重复提交失败后订单状态货物与历史零漂移/)
assert.match(source, /orderPersistenceFacts\(draftAfterConflict\) === orderPersistenceFacts\(draft\)/)
assert.match(source, /orderPersistenceFacts\(submittedAfterFailure\) === orderPersistenceFacts\(submitted\)/)
})
2026-09-14 17:29:03 +08:00
test('admin internal notes use the current versioned collaborative remarks contract', () => {
assert.match(source, /context\.request\('PUT',[\s\S]*\/remarks\?tenantId=/)
assert.match(source, /expectedVersion:\s*Number\(firstLegDetail\?\.order\?\.version\)/)
assert.match(source, /internalRemark:\s*`\$\{context\.marker\} Admin 订单链路验收`/)
assert.doesNotMatch(source, /\/internal-notes/)
})
test('persistence fact comparison includes order cargo parties notes and history', () => {
const detail = {
order: { id: '1', status: 'DRAFT' }, cargoes: [{ skuNo: 'SKU-1' }],
parties: [{ role: 'SENDER' }], internalNotes: [{ content: 'note' }],
statusHistory: [{ toStatus: 'DRAFT' }],
}
const baseline = orderPersistenceFacts(detail)
assert.equal(orderPersistenceFacts(structuredClone(detail)), baseline)
assert.notEqual(orderPersistenceFacts({ ...detail, order: { ...detail.order, status: 'SUBMITTED' } }), baseline)
assert.notEqual(orderPersistenceFacts({ ...detail, cargoes: [{ skuNo: 'SKU-2' }] }), baseline)
assert.notEqual(orderPersistenceFacts({ ...detail, statusHistory: [...detail.statusHistory, { toStatus: 'SUBMITTED' }] }), baseline)
})
test('approval convergence is a hard WAIT_MEASURE prerequisite without coverage downgrade', () => {
assert.match(source, /审核后入库单与履约任务异步收敛/)
assert.match(source, /MEASURABLE_STATUSES/)
assert.match(source, /throw new Error\('订单审核后的入库单与履约任务未在30秒内收敛/)
assert.doesNotMatch(source, /审核后入库单与履约任务异步收敛[\s\S]{0,300}addCoverageGap/)
})
2026-09-03 16:09:22 +08:00
test('post-approval declaration update waits for authoritative fulfilment convergence', () => {
const approval = source.indexOf("Admin 审核通过本轮订单")
const declaration = source.indexOf("Admin 修改审核后客户申报资料")
const convergence = source.indexOf('const inboundResult = await waitForInbound')
2026-09-03 16:09:22 +08:00
assert.ok(approval >= 0 && convergence > approval && declaration > convergence)
assert.match(source, /APPROVED declaration edits fail closed against the authoritative Firstleg projection/)
})