test(order): verify platform submit and cancel command boundaries

This commit is contained in:
2026-10-02 23:37:15 +08:00
parent c55ecabbfb
commit d1a13cdc80
2 changed files with 166 additions and 1 deletions
@@ -2,7 +2,92 @@ import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { fixtureInboundCode, orderPersistenceFacts, requestedOrderDestinationType, freezeOrderCommand, verifyPlatformDraftEdit } from './orders.mjs'
import { fixtureInboundCode, orderPersistenceFacts, requestedOrderDestinationType, freezeOrderCommand, verifyPlatformDraftEdit, verifyPlatformSubmitCancel } from './orders.mjs'
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'}
+80
View File
@@ -75,6 +75,85 @@ export async function verifyPlatformDraftEdit(context, tenantId, orderId, mercha
context.assert(MODULE, 'Platform重放与拒绝不重复写入', null, orderPersistenceFacts(stable) === orderPersistenceFacts(edited), 'order/cargo/parties/history', CONTRACT_OPTIONS)
return stable
}
export async function verifyPlatformSubmitCancel(context, tenantId, payload, policy) {
const base = '/api/platform/v1/orders/first-leg'
const scope = `tenantId=${encodeURIComponent(tenantId)}`
const proofs = context.createdResources.orders.platformLifecycle = []
for (const action of ['submit', 'cancel']) {
const draftPayload = { ...structuredClone(payload), ...fixtureInboundCode(policy, `${context.marker}-PLATFORM-${action}`),
customerRemark: `${context.marker} Platform ${action} independent draft` }
const created = await context.request('POST', `${base}?${scope}`, draftPayload, context.token,
{ 'Idempotency-Key': key(context, `platform-${action}-create`) })
const orderId = id(context.apiSuccess(MODULE, `Platform ${action} 创建本轮独立草稿`, created))
if (!orderId) throw new Error(`Platform ${action} fixture order missing`)
const path = `${base}/${encodeURIComponent(orderId)}?${scope}`
const mutationPath = `${base}/${encodeURIComponent(orderId)}/${action}?${scope}`
const proof = { action, tenantId, orderId, commands: [], checks: [] }
proofs.push(proof)
const assertFact = (label, response, passed, evidence) => {
context.assert(MODULE, `Platform ${action} ${label}`, response, passed, evidence, CONTRACT_OPTIONS)
if (!passed) throw new Error(`Platform ${action} ${label}`)
}
const read = async label => {
const response = await context.request('GET', path)
const detail = context.apiSuccess(MODULE, `Platform ${action} ${label}`, response)
assertFact('详情身份与完整事实集合', response, id(detail?.order?.id) === orderId && id(detail?.order?.tenantId) === id(tenantId)
&& Number.isSafeInteger(detail.order.version) && detail.order.version >= 0
&& ['cargoes','parties','internalNotes','statusHistory'].every(field => Array.isArray(detail[field])), 'exact-customer/order/version/aggregate')
return detail
}
const same = (label, before, after) => assertFact(label, null, orderPersistenceFacts(before) === orderPersistenceFacts(after), 'state/version/cargo/parties/notes/history unchanged')
const original = await read('冻结首次草稿版本')
assertFact('取消与提交前置仅为未履约草稿', created, original.order.status === 'DRAFT', 'DRAFT-only')
// Advance through an actual edit, so stale version 0 remains valid input rather than Min(0) validation failure.
const editCommand = freezeOrderCommand(original, key(context, `platform-${action}-prepare-edit`), { draft: draftPayload })
context.apiSuccess(MODULE, `Platform ${action} 形成真实旧页面版本`,
await context.request('PUT', path, editCommand.body, context.token, editCommand.headers))
const current = await read('冻结动作首次有效请求版本')
assertFact('编辑后仍为草稿且版本增加', null, current.order.status === 'DRAFT' && current.order.version > original.order.version, 'old-page-version-created')
const extra = action === 'cancel' ? { reason: `${context.marker} independent draft cancellation` } : {}
const command = freezeOrderCommand(current, key(context, `platform-${action}-execute`), extra)
proof.originalVersion = original.order.version
proof.command = command
const send = async (label, body, headers) => {
proof.commands.push({ label, path: mutationPath, body: structuredClone(body), headers: structuredClone(headers) })
return context.request('POST', mutationPath, body, context.token, headers)
}
const reject = async (label, body, headers, code, expected, statuses = [200]) => {
const response = await send(label, body, headers)
assertFact(label, response, statuses.includes(response?.http) && String(response.body?.code) === code, `expected=${code}`)
same(`${label}后零漂移`, expected, await read(`${label}后回查`))
proof.checks.push({ label, code })
}
await reject('首次旧页面版本拒绝', { ...extra, expectedVersion: original.order.version },
{ 'Idempotency-Key': key(context, `platform-${action}-stale-first`) }, 'A0409', current)
await reject('首次缺版本拒绝', extra,
{ 'Idempotency-Key': key(context, `platform-${action}-missing-version`) }, 'A0422', current)
await reject('首次缺幂等键拒绝', command.body, {}, 'A0400', current, [200,400])
context.apiSuccess(MODULE, `Platform ${action} 原版本原键成功`, await send('success', command.body, command.headers))
const completed = await read('成功后的事实快照')
const expectedStatus = action === 'submit' ? 'SUBMITTED' : 'CANCELLED'
const oldHistoryIds = new Set(current.statusHistory.map(row => id(row.id)))
const added = completed.statusHistory.filter(row => !oldHistoryIds.has(id(row.id)))
assertFact('成功状态版本与唯一动作历史', null, completed.order.status === expectedStatus && completed.order.version > current.order.version
&& added.length === 1 && added[0].action === expectedStatus && added[0].fromStatus === 'DRAFT' && added[0].toStatus === expectedStatus
&& Boolean(added[0].operatorId && added[0].occurredAt)
&& (action !== 'cancel' || added[0].reason === extra.reason), 'one attributed lifecycle history')
assertFact('成功后货物旧历史及业务身份保留', null,
['cargoes','parties','internalNotes'].every(field => JSON.stringify(current[field]) === JSON.stringify(completed[field]))
&& JSON.stringify(current.statusHistory) === JSON.stringify(completed.statusHistory.filter(row => oldHistoryIds.has(id(row.id))))
&& ['id','tenantId','customerId','orderNo'].every(field => current.order[field] === completed.order[field]), 'no unrelated mutations')
context.apiSuccess(MODULE, `Platform ${action} 原键原版本精确重放成功`, await send('replay', command.body, command.headers))
same('成功重放后零漂移', completed, await read('原键重放后回查'))
const changed = action === 'submit' ? { expectedVersion: completed.order.version } : { ...command.body, reason: `${extra.reason}-changed` }
await reject('原键异载荷拒绝', changed, command.headers, 'A0409', completed)
await reject('新键重复动作拒绝', { ...extra, expectedVersion: completed.order.version },
{ 'Idempotency-Key': key(context, `platform-${action}-repeat-new-key`) }, 'A0409', completed)
proof.finalStatus = expectedStatus
proof.finalFactsSha256 = createHash('sha256').update(orderPersistenceFacts(completed)).digest('hex')
}
return proofs
}
export function fixtureInboundCode(policy, marker) {
if (policy?.mode === 'AUTO' && policy.manualEntryRequired === false) return {}
if (policy?.mode !== 'LEGACY_MANUAL' || policy.manualEntryRequired !== true || !marker) {
@@ -855,6 +934,7 @@ export async function run(context) {
assertCargoContract(context, draftResponse, draft?.cargoes, payload)
context.createdResources.orders.orderNo = orderNo
draft = await verifyPlatformDraftEdit(context, tenantId, orderId, merchantToken, payload)
await verifyPlatformSubmitCancel(context, tenantId, payload, policy)
const createConflictPayload = {
...payload,