/// 上传任务状态机表驱动测试(P3 计划 Task 5)。 /// /// 覆盖:合法迁移表与方案 §5.5 逐边对齐、全量非法对拒绝、自迁移拒绝、 /// 终态无出边、wire 落库取值往返。 library; import 'package:flutter_test/flutter_test.dart'; import 'package:photo_live/features/uploads/task_state.dart'; /// 方案 §5.5 / P3 计划 Task 5/6 的合法迁移表(人工誊写,作为对照基准)。 final Map> expectedTransitions = { UploadTaskState.preparing: { UploadTaskState.queued, UploadTaskState.preparationFailed, UploadTaskState.cancelled, }, UploadTaskState.preparationFailed: { UploadTaskState.preparing, UploadTaskState.cancelled, }, UploadTaskState.queued: { UploadTaskState.uploading, UploadTaskState.waitingNetwork, UploadTaskState.cancelled, }, UploadTaskState.uploading: { UploadTaskState.uploadedConfirming, UploadTaskState.waitingNetwork, UploadTaskState.retryableFailed, UploadTaskState.cancelled, // 尽力取消 }, UploadTaskState.uploadedConfirming: { UploadTaskState.completed, UploadTaskState.retryableFailed, UploadTaskState.waitingNetwork, }, UploadTaskState.waitingNetwork: { UploadTaskState.queued, UploadTaskState.uploading, // 网络恢复直接续传 UploadTaskState.cancelled, }, UploadTaskState.retryableFailed: { UploadTaskState.queued, UploadTaskState.cancelled, }, UploadTaskState.completed: {}, UploadTaskState.cancelled: {}, }; void main() { test('合法迁移表与方案基准逐状态一致(多边/少边都失败)', () { for (final entry in expectedTransitions.entries) { final actual = kUploadTaskTransitions[entry.key]!; expect(actual, equals(entry.value), reason: '${entry.key} 的合法目标集合与基准不一致'); } expect( kUploadTaskTransitions.keys, equals(expectedTransitions.keys.toSet()), reason: '迁移表的键必须覆盖全部状态', ); }); test('全部合法边:canTransition=true 且 assertTransition 不抛', () { var legalEdgeCount = 0; for (final from in UploadTaskState.values) { for (final to in kUploadTaskTransitions[from]!) { legalEdgeCount++; expect(canTransition(from, to), isTrue, reason: '$from → $to 应合法'); expect(() => assertTransition(from, to), returnsNormally, reason: '$from → $to 断言不应抛'); } } expect(legalEdgeCount, 20, reason: '合法边总数(防意外增删边)'); }); test('全量非法对(9x9 减合法边):canTransition=false 且 assertTransition 抛 StateError', () { for (final from in UploadTaskState.values) { for (final to in UploadTaskState.values) { if (kUploadTaskTransitions[from]!.contains(to)) continue; expect(canTransition(from, to), isFalse, reason: '$from → $to 应非法'); expect(() => assertTransition(from, to), throwsA(isA()), reason: '$from → $to 断言应抛 StateError'); } } }); test('自迁移一律非法(状态未变不该写库)', () { for (final state in UploadTaskState.values) { expect(canTransition(state, state), isFalse, reason: '$state → 自身'); } }); test('终态(completed/cancelled)无出边,且 isTerminal 仅为这两个', () { for (final state in UploadTaskState.values) { if (state == UploadTaskState.completed || state == UploadTaskState.cancelled) { expect(state.isTerminal, isTrue); expect(kUploadTaskTransitions[state]!, isEmpty, reason: '$state 是终态不应有出边'); } else { expect(state.isTerminal, isFalse, reason: '$state 不是终态'); } } }); test('典型非法路径被拒绝(状态机硬守护)', () { // 不能跳过准备直接传。 expect(canTransition(UploadTaskState.preparing, UploadTaskState.uploading), isFalse); // 不能从 queued 直接完成。 expect( canTransition(UploadTaskState.queued, UploadTaskState.completed), isFalse); // completed 是终态:不能复活。 expect( canTransition(UploadTaskState.completed, UploadTaskState.queued), isFalse); // cancelled 是终态。 expect( canTransition(UploadTaskState.cancelled, UploadTaskState.preparing), isFalse); // preparationFailed 只能回 preparing 或取消,不能直接入队。 expect( canTransition( UploadTaskState.preparationFailed, UploadTaskState.queued), isFalse); }); test('wire 落库取值全枚举往返;未知值 fail-fast', () { const wires = [ 'preparing', 'preparation_failed', 'queued', 'uploading', 'uploaded_confirming', 'completed', 'waiting_network', 'retryable_failed', 'cancelled', ]; expect( UploadTaskState.values.map((s) => s.wire).toSet(), equals(wires.toSet()), ); for (final wire in wires) { expect(UploadTaskState.fromWire(wire).wire, wire); } expect(() => UploadTaskState.fromWire('failed'), throwsFormatException); expect(() => UploadTaskState.fromWire('UPLOADING'), throwsFormatException); }); }