Files
photo-live/apps/mobile/test/features/uploads/task_state_test.dart
T
浅念 d96e56f653 feat(mobile): drift upload task store with state machine
- upload_tasks 表(accountId 隔离 + 三个复合索引)、状态列枚举转换器、
  datetime 文本落库(UTC 归一,亚秒精度)
- 纯状态机(task_state.dart):合法迁移表 + canTransition/assertTransition,
  completed/cancelled 终态
- TaskStore 仓储:create(clientTaskId 同账号同相册应用侧唯一)、
  updateState(迁移守护+旁路字段)、账号隔离读路径(硬守护)、
  countsByState 徽章、cleanupCandidates/markCleanable(仅终态可清理——
  评审缺口 1 硬守护,未完成源文件永不清理)
- 测试 26 例(状态机表驱动全边 + 守护/隔离/筛选/保留策略);
  sqlite3 3.x build hooks 在 flutter test 宿主与 Android APK 构建均验证
  (libsqlite3.so 已打包全 ABI,运行时验证留 Task 11)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 02:12:39 +08:00

154 lines
5.2 KiB
Dart

/// 上传任务状态机表驱动测试(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<UploadTaskState, Set<UploadTaskState>> 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<StateError>()),
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);
});
}