d96e56f653
- 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>
154 lines
5.2 KiB
Dart
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);
|
|
});
|
|
}
|