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 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') const RECOVERY_TASK_ID = '2092817774453780481' 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(loadingPickTests, /(?:transient|瞬时)[\s\S]{0,1500}(?:backoff|retry|退避)/i) 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(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) }) 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('final acceptance reconciles task allocation reservation occupancy and one completion audit', () => { for (const fact of ['task', 'allocation', 'reservation', 'occupancy']) { 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) }) 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/) })