Files
baiyue-firstleg/scripts/admin-test-env-modules/pda-complete-cross-service-recovery-contract.test.mjs
T
2026-09-20 22:18:59 +08:00

128 lines
7.8 KiB
JavaScript

import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
const read = (relativeUrl) => readFileSync(new URL(relativeUrl, import.meta.url), 'utf8')
const loadingPickService = read('../../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java')
const completionCommand = read('../../firstleg/src/main/java/com/baiyue/firstleg/entity/LoadingCompletionCommandEntity.java')
const completionWorker = read('../../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletionWorker.java')
const loadingPickTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java')
const completionWorkerTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/LoadingCompletionWorkerTest.java')
const settlementTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java')
const unifiedSql = read('../../deploy/sql/头程数据库统一增量-MySQL8.sql')
const finalizeHttp = read('phase3-loading-finalize.mjs')
const pdaPage = read('../../../apps/pda-uniapp/pages/picking/picking.vue')
const pdaCompletionState = read('../../../apps/pda-uniapp/utils/loading-completion-state.js')
const pdaCompletionTests = read('../../../apps/pda-uniapp/tests/loading-completion-state.test.mjs')
function retryDecision({ operationStatus, sameRequest, finalTaskStatus }) {
if (!sameRequest) return 'CONFLICT'
if (operationStatus === 'PROCESSING') return 'PROCESSING'
if (operationStatus === 'COMPLETED') return 'REPLAY'
if (finalTaskStatus === 'COMPLETED') return 'RECONCILE_COMPLETE'
return 'RESUME'
}
test('offline orchestration model distinguishes processing replay recovery and conflict', () => {
assert.equal(retryDecision({ operationStatus: 'PROCESSING', sameRequest: true }), 'PROCESSING')
assert.equal(retryDecision({ operationStatus: 'COMPLETED', sameRequest: true }), 'REPLAY')
assert.equal(retryDecision({ operationStatus: 'FAILED', sameRequest: true, finalTaskStatus: 'LOADING' }), 'RESUME')
assert.equal(retryDecision({ operationStatus: 'FAILED', sameRequest: false }), 'CONFLICT')
})
test('POST complete is reliable acceptance only and returns PROCESSING/COMPLETING', () => {
const start = loadingPickService.indexOf('public Task complete(')
const end = loadingPickService.indexOf('/** Called only by the completion worker', start)
const block = loadingPickService.slice(start, end)
assert.match(block, /LoadingCompletionCommandEntity/)
assert.match(block, /acceptCompletion/)
assert.match(block, /return view\(requiredTask\(taskId\), null\)/)
assert.doesNotMatch(block, /settlementService|completeAfterPdaSettlement|publishCompletedEvent/)
assert.match(loadingPickService, /"COMPLETING"\.equals\(task\.getTaskStatus\(\)\)[\s\S]{0,80}"PROCESSING"/)
})
test('complete does not hold fl_loading_pick_task FOR UPDATE across remote settlement calls', () => {
const start = loadingPickService.indexOf('public void processCompletion(')
const end = loadingPickService.indexOf('private static void requireSameCompletionCommand', start)
const block = loadingPickService.slice(start, end)
assert.notEqual(start, -1)
assert.match(block, /settlementService\.settleOwner/)
assert.doesNotMatch(block, /requiredLoadingTaskForUpdate|requiredTaskForUpdate/)
assert.match(loadingPickService, /completionCommands\.insert/)
})
test('completion operation has durable PROCESSING and terminal response state', () => {
assert.match(completionCommand, /commandStatus/)
assert.match(completionCommand, /attemptCount/)
assert.match(completionCommand, /nextRetryAt/)
assert.match(completionCommand, /completedAt/)
assert.match(completionCommand, /failureStage/)
const commandStart = unifiedSql.search(/CREATE TABLE(?: IF NOT EXISTS)? fl_loading_completion_command/)
const commandTable = unifiedSql.slice(commandStart, unifiedSql.indexOf(';', commandStart) + 1)
assert.match(commandTable, /command_status/i)
assert.match(commandTable, /next_retry_at/i)
assert.match(commandTable, /completed_at/i)
assert.match(unifiedSql, /CREATE TABLE(?: IF NOT EXISTS)? fl_loading_completion_checkpoint/)
})
test('durable worker processes inbound settlements incrementally with retry backoff', () => {
assert.match(completionWorker, /@Scheduled/)
assert.match(completionWorker, /commands\.due/)
assert.match(completionWorker, /commands\.claim/)
assert.match(completionWorker, /15L <<|nextRetryAt|retryAt/)
assert.match(completionWorker, /"FAILED"\s*:\s*"RETRYABLE"/)
assert.match(loadingPickService, /checkpointAtLeast[\s\S]{0,1800}SETTLED/)
assert.match(completionWorkerTests, /transientFailureIsReleasedForBackoffRetry/)
assert.match(completionWorkerTests, /"RETRYABLE"/)
assert.match(loadingPickTests, /FAILED[\s\S]{0,1500}(?:same operationId|同operationId|resume|恢复)/i)
})
test('same operationId concurrency returns PROCESSING promptly instead of waiting for the task row lock', () => {
assert.match(loadingPickTests, /sameOperationId[\s\S]{0,1600}PROCESSING/i)
assert.match(loadingPickTests, /(?:assertTimeout|timeout|elapsed|millis|seconds)[\s\S]{0,500}(?:CannotAcquireLock|FOR UPDATE|taskForUpdate)/i)
assert.match(loadingPickTests, /never\(\)[\s\S]{0,180}requiredLoadingTaskForUpdate|never\(\)[\s\S]{0,180}taskForUpdate/i)
})
test('a different operationId is rejected while the completion command is processing', () => {
assert.match(loadingPickTests, /(?:different operationId|不同operationId|different key|不同键)[\s\S]{0,1200}(?:A0409|CONFLICT)/i)
})
test('fifteen completed Warehouse settlements can be reconciled after Firstleg rollback', () => {
assert.match(loadingPickTests, /workerProcessesFifteenInboundOwnersInStableOrderAndFinalizesOnce/)
assert.match(loadingPickTests, /IntStream\.range\(0, 15\)/)
assert.match(settlementTests, /repeatedSettleWithExistingChildDoesNotConsumeOrTransferAgain/)
assert.match(settlementTests, /splits\.appliedBatch\(10001L, "PDA-SPLIT-1-31"\)/)
assert.match(settlementTests, /verify\(warehouse, never\(\)\)\.transferBlock/)
assert.match(settlementTests, /verify\(warehouse, never\(\)\)\.releaseBlock/)
assert.match(loadingPickTests, /workerRestartResumesFromOwnerCheckpointsWithoutRepeatingSettlements/)
})
test('recovery contract is fixture-independent and uses durable checkpoints', () => {
assert.doesNotMatch(finalizeHttp, /2092817774453780481/)
assert.match(loadingPickService, /checkpointAtLeast\(checkpoint, "SETTLED"\)/)
assert.match(loadingPickService, /checkpointAtLeast\(checkpoint, "INBOUND_COMPLETED"\)/)
assert.match(loadingPickTests, /workerRestartResumesFromOwnerCheckpointsWithoutRepeatingSettlements/)
})
test('final acceptance reconciles terminal task and one completion audit after owner checkpoints', () => {
for (const fact of ['task', 'plan', 'inbound', 'operation']) {
assert.match(finalizeHttp, new RegExp(fact, 'i'), `missing final ${fact} reconciliation`)
}
assert.match(finalizeHttp, /COMPLETE_LOADING/)
assert.match(finalizeHttp, /concurrentCompleteOperations\.length === 1/)
assert.match(finalizeHttp, /异步完成命令收敛到终态/)
assert.match(loadingPickService, /completionTransaction\.executeWithoutResult/)
assert.match(loadingPickService, /private void finalizeCompletion/)
})
test('PDA treats accepted completion as processing, returns to list, and never claims completion early', () => {
assert.match(pdaCompletionState, /PROCESSING_STATUSES = new Set\(\['COMPLETING', 'PROCESSING'\]\)/)
assert.match(pdaCompletionTests, /accepted completion is processing/)
assert.match(pdaPage, /loading-completion-state/)
assert.match(pdaPage, /(?:PROCESSING|COMPLETING)[\s\S]{0,1200}navigateBackOr/)
assert.doesNotMatch(pdaPage, /completeContainerPickingTask[\s\S]{0,700}showToast\('已确认装柜完成'/,
'accepted PROCESSING response must not show a completed success toast')
assert.match(pdaPage, /shouldReconcileCompletion/)
})