fix(firstleg): atomically finalize PDA completion
This commit is contained in:
+20
-17
@@ -8,6 +8,7 @@ const loadingPickService = read('../../firstleg/src/main/java/com/baiyue/firstle
|
||||
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')
|
||||
@@ -15,8 +16,6 @@ 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')
|
||||
|
||||
const RECOVERY_TASK_ID = '2092817774453780481'
|
||||
|
||||
function retryDecision({ operationStatus, sameRequest, finalTaskStatus }) {
|
||||
if (!sameRequest) return 'CONFLICT'
|
||||
if (operationStatus === 'PROCESSING') return 'PROCESSING'
|
||||
@@ -74,7 +73,8 @@ test('durable worker processes inbound settlements incrementally with retry back
|
||||
assert.match(completionWorker, /15L <<|nextRetryAt|retryAt/)
|
||||
assert.match(completionWorker, /"FAILED"\s*:\s*"RETRYABLE"/)
|
||||
assert.match(loadingPickService, /checkpointAtLeast[\s\S]{0,1800}SETTLED/)
|
||||
assert.match(loadingPickTests, /(?:transient|瞬时)[\s\S]{0,1500}(?:backoff|retry|退避)/i)
|
||||
assert.match(completionWorkerTests, /transientFailureIsReleasedForBackoffRetry/)
|
||||
assert.match(completionWorkerTests, /"RETRYABLE"/)
|
||||
assert.match(loadingPickTests, /FAILED[\s\S]{0,1500}(?:same operationId|同operationId|resume|恢复)/i)
|
||||
})
|
||||
|
||||
@@ -89,27 +89,30 @@ test('a different operationId is rejected while the completion command is proces
|
||||
})
|
||||
|
||||
test('fifteen completed Warehouse settlements can be reconciled after Firstleg rollback', () => {
|
||||
assert.match(settlementTests, /15|fifteen/i)
|
||||
assert.match(settlementTests, /(?:partial success|部分成功|local rollback|本地回滚)/i)
|
||||
assert.match(settlementTests, /(?:settlementTarget|COMPLETED|replaySettlement)/)
|
||||
assert.match(loadingPickTests, /(?:resume|recover|reconcile|恢复|核平)[\s\S]{0,2000}COMPLETE_LOADING/i)
|
||||
assert.match(loadingPickTests, /workerProcessesFifteenInboundOwnersInStableOrderAndFinalizesOnce/)
|
||||
assert.match(loadingPickTests, /IntStream\.range\(0, 15\)/)
|
||||
assert.match(settlementTests, /reusesWarehouseSettlementTargetWhenRetryingAfterLocalRollback/)
|
||||
assert.match(settlementTests, /warehouse\.settlementTarget\("internal-token", 31L, "PDA-SETTLE-1-31"\)/)
|
||||
assert.match(settlementTests, /targetBusinessId\(\) == 999L/)
|
||||
assert.match(loadingPickTests, /workerRestartResumesFromOwnerCheckpointsWithoutRepeatingSettlements/)
|
||||
})
|
||||
|
||||
test('recovery fixture preserves the observed fifteen-settlement partial success', () => {
|
||||
assert.equal(RECOVERY_TASK_ID, '2092817774453780481')
|
||||
assert.match(finalizeHttp, new RegExp(RECOVERY_TASK_ID))
|
||||
assert.match(finalizeHttp, /15[\s\S]{0,600}(?:Warehouse|settlement)/i)
|
||||
assert.match(finalizeHttp, /(?:LOADING|v1)[\s\S]{0,600}(?:恢复|recovery|resume)/i)
|
||||
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 task allocation reservation occupancy and one completion audit', () => {
|
||||
for (const fact of ['task', 'allocation', 'reservation', 'occupancy']) {
|
||||
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, /(?:length === 1|恰有一次|只写一条)/)
|
||||
assert.match(finalizeHttp, /(?:PROCESSING|处理中)/)
|
||||
assert.match(finalizeHttp, /(?:elapsed|latency|耗时|毫秒|ms)/i)
|
||||
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', () => {
|
||||
|
||||
Reference in New Issue
Block a user