128 lines
7.8 KiB
JavaScript
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 commandTable = unifiedSql.slice(unifiedSql.indexOf('CREATE TABLE IF NOT EXISTS fl_loading_completion_command'),
|
|
unifiedSql.indexOf(';', unifiedSql.indexOf('CREATE TABLE IF NOT EXISTS fl_loading_completion_command')) + 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/)
|
|
})
|