This commit is contained in:
2026-08-26 15:58:54 +08:00
parent 57a2f6ef28
commit fd1a81594a
21 changed files with 406 additions and 20 deletions
@@ -20,8 +20,8 @@ const MARKER = 'T-PHASE4'
function ok(data, requestId = 'req-ok') {
return { http: 200, body: { code: '200', data, requestId }, requestId, traceId: 'trace-ok' }
}
function conflict(requestId) {
return { http: 200, body: { code: 'A0409', message: 'conflict', requestId }, requestId, traceId: 'trace-conflict' }
function conflict(requestId, message = 'conflict') {
return { http: 200, body: { code: 'A0409', message, requestId }, requestId, traceId: 'trace-conflict' }
}
function clone(value) { return structuredClone(value) }
@@ -121,7 +121,7 @@ test('missing phase3 outbound query is an explicit deployment blocker and perfor
function markerOwnedContext() {
let nextId = 9007199254740994000n
const calls = []; const steps = []; const idempotency = new Map()
const calls = []; const steps = []; const assertions = []; const idempotency = new Map()
const task = {
id: QUERY_ID, queryNo: `CSQ${QUERY_ID}`, customerId: CUSTOMER_ID, oceanBookingId: OCEAN_ID,
containerBookingId: CONTAINER_ID, carrierCode: 'ZIM', trackingType: 'CONTAINER_NO',
@@ -179,6 +179,11 @@ function markerOwnedContext() {
: conflict(`req-hash-${key}`)
}
if (Number(body.expectedVersion) !== Number(task.version)) return conflict(`req-version-${key}`)
if (body.actualAt != null
&& NODE_SEQUENCES[body.standardNode] <= NODE_SEQUENCES[task.latestActualNode]) {
return conflict(`req-monotonic-${key}`,
`人工实际节点必须严格晚于当前最新实际节点:${task.latestActualNode}`)
}
const providerEventId = `manual-${String(nextId)}`
nodes.push({
id: String(nextId++), providerCode: 'MANUAL', providerEventId, providerNodeCode: body.standardNode,
@@ -203,13 +208,16 @@ function markerOwnedContext() {
apiSuccess: (_module, _name, response) => {
assert.equal(response.http, 200); assert.equal(response.body.code, '200'); return response.body.data
},
assert: (_module, name, _response, condition, evidence) => assert.ok(condition, `${name}: ${evidence}`),
assert: (_module, name, response, condition, evidence, options) => {
assertions.push({ name, passed: Boolean(condition), evidence, options, requestId: response?.requestId || null })
assert.ok(condition, `${name}: ${evidence}`)
},
addStep: (_module, name, response, passed, evidence, options) => {
steps.push({ name, passed, evidence, options, requestId: response?.requestId || null })
assert.ok(passed, `${name}: ${evidence}`)
},
}
return { context, task, nodes, exceptions, histories, steps }
return { context, task, nodes, exceptions, histories, steps, assertions }
}
test('marker-owned chain covers idempotency conflicts, all actual nodes, exception DTO and empty-return stop', async () => {
@@ -218,11 +226,15 @@ test('marker-owned chain covers idempotency conflicts, all actual nodes, excepti
await run(fixture.context)
const posts = fixture.context.calls.filter((item) => item.method === 'POST')
assert.equal(posts.length, 14)
assert.equal(posts.length, 17)
assert.ok(posts.every((item) => item.path === `${BASE}/${QUERY_ID}/manual-nodes`))
assert.ok(posts.every((item) => item.headers['Idempotency-Key']))
assert.ok(posts.every((item) => Object.keys(item.body).length === 8))
assert.equal(fixture.steps.filter((item) => /A0409/.test(item.name)).length, 2)
assert.equal(fixture.steps.filter((item) => /A0409/.test(item.name)).length, 6)
assert.ok(fixture.assertions.some((item) => /重复当前实际节点A0409后task、node、history零漂移/.test(item.name)))
assert.ok(fixture.assertions.some((item) => /更早实际节点A0409后task、node、history零漂移/.test(item.name)))
assert.ok(fixture.steps.some((item) => /同级actual失败请求同键重试.*operation claim零残留/.test(item.name)))
assert.ok(fixture.steps.some((item) => /更早actual失败请求同键重试.*operation claim零残留/.test(item.name)))
assert.equal(fixture.task.status, 'STOPPED')
assert.equal(fixture.task.latestActualNode, 'EMPTY_RETURNED')
assert.equal(fixture.task.stopReason, '已还柜')