test(acceptance): recover completed-plan rejection probe after verified connection loss

This commit is contained in:
2026-10-23 05:36:21 +08:00
parent 2b54e709f8
commit 3616e9db04
3 changed files with 86 additions and 5 deletions
@@ -4,6 +4,41 @@ const OPTIONS = { scenario: 'phase3-loading-hard-gate-finalize', deploymentBlock
function id(value) { return value == null ? '' : String(value) }
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
/** 只对已完成计划的拒绝探针核实未知结果;原体原键最多重放一次,不用于正常业务写入。 */
export async function completedPlanAssignmentProbe(context, { planPath, body, baseline }, send = request) {
if (baseline?.status !== 'COMPLETED' || String(baseline.version) !== String(body.expectedVersion)
|| !body.operationId?.startsWith(context.marker) || !body.inboundId || !body.inboundDetailId || body.packageCount !== 1)
throw Error('已完成计划拒绝探针缺少原状态、版本及不可变命令')
const signature = value => JSON.stringify(value, (_, row) => row && typeof row === 'object' && !Array.isArray(row)
? Object.fromEntries(Object.keys(row).sort().map(key => [key, row[key]])) : row)
const original = structuredClone(body)
const evidence = { path: `${planPath}/assignments`, body: original, status: 'PREPARED', attempts: [] }
;(context.createdResources.completedPlanAssignmentProbes ??= []).push(evidence)
await context.checkpoint?.()
const attempt = async () => {
evidence.status = 'SENDING'; await context.checkpoint?.()
const response = await send(context, 'POST', evidence.path, original)
evidence.attempts.push(response); evidence.status = response.http === 0 ? 'UNKNOWN_RESULT' : 'RESPONDED'
await context.checkpoint?.(); return response
}
const first = await attempt()
if (first.http !== 0) return first
const current = await context.request('GET', planPath)
evidence.readback = current
if (current.http !== 200 || codeOf(current) !== '200' || current.body?.data?.status !== 'COMPLETED'
|| signature(current.body.data) !== signature(baseline)) {
evidence.status = 'UNKNOWN_RESULT_STATE_CHANGED'; await context.checkpoint?.()
throw Error('拒绝探针连接中断后原计划状态或明细变化,禁止重放及后续修改')
}
const replay = await attempt()
const after = await context.request('GET', planPath); evidence.after = after
const unchanged = after.http === 200 && codeOf(after) === '200' && signature(after.body.data) === signature(baseline)
evidence.status = replay.http === 200 && codeOf(replay) === 'A0409' && unchanged ? 'VERIFIED_REJECTION_AFTER_TRANSPORT_RECOVERY' : 'UNVERIFIED'
await context.checkpoint?.()
if (!unchanged) throw Error('原键拒绝探针重放后计划不一致,停止后续修改')
return replay
}
export async function replayCompletedCommand(send, recordTransportFailure) {
const first = await send()
if (first.http !== 0) return first
@@ -84,7 +119,8 @@ async function request(context, method, path, body, headers = {}) {
requestId: payload?.requestId || response.headers.get('x-request-id'),
traceId: payload?.traceId || response.headers.get('x-trace-id') }
} catch (error) {
return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null }
return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null,
transportError: { name: error?.name, code: error?.cause?.code || error?.code, cause: error?.cause?.message } }
}
}
@@ -520,13 +556,12 @@ export async function run(context) {
`HTTP=${immutableTransport.http}, code=${codeOf(immutableTransport)}, requestId=${immutableTransport.requestId || 'missing'}, traceId=${immutableTransport.traceId || 'missing'}`, OPTIONS)
const completedPlanItem = immutablePlan?.items?.[0]
const assignmentAfterCompleted = await request(context, 'POST',
`/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(loadingPlanId)}/assignments`, {
const assignmentAfterCompleted = await completedPlanAssignmentProbe(context, { planPath, baseline: immutablePlan, body: {
inboundId: fixture.incrementalInboundId || inboundId,
inboundDetailId: id(completedPlanItem?.inboundDetailId), packageCount: 1,
expectedVersion: Number(immutablePlan?.version),
operationId: `${context.marker}-assign-after-completed`.slice(0, 128),
})
} })
context.addStep(MODULE, 'COMPLETED 后新增计划明细被拒绝', assignmentAfterCompleted,
assignmentAfterCompleted.http === 200 && codeOf(assignmentAfterCompleted) === 'A0409',
`HTTP=${assignmentAfterCompleted.http}, code=${codeOf(assignmentAfterCompleted)}`, OPTIONS)