feat(mobile): upload engine with preparation, batching states and retries

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-19 02:50:40 +08:00
parent d96e56f653
commit 3ddb0ea2d4
23 changed files with 3232 additions and 12 deletions
@@ -251,6 +251,59 @@ void main() {
throwsA(isA<StateError>()),
);
});
test('patch 未提供的列保持不变(文档契约,引擎重试依赖)', () async {
// 场景(Task 6 引擎):失败写入 serverUploadId/attempt/lastError 后,
// 后续不带这些字段的迁移不得清空它们。
await store.create(draft());
await store.updateState('task-0001', UploadTaskState.queued);
await store.updateState(
'task-0001',
UploadTaskState.uploading,
patch: const UploadTaskPatch(
serverUploadId: 'upl-1',
serverPhotoId: 'pho-1',
),
);
await store.updateState(
'task-0001',
UploadTaskState.waitingNetwork,
patch: const UploadTaskPatch(
attempt: 2,
lastError: 'NETWORK_ERROR',
),
);
final kept = await store.updateState(
'task-0001',
UploadTaskState.uploading,
now: DateTime.utc(2026, 9, 19, 10),
);
expect(kept.serverUploadId, 'upl-1', reason: '未提供不得清空');
expect(kept.serverPhotoId, 'pho-1');
expect(kept.attempt, 2);
expect(kept.lastError, 'NETWORK_ERROR');
});
test('updateMeta:旁路回填不迁移状态(引擎 init 后回填会话 ID)', () async {
await store.create(draft());
await store.updateState('task-0001', UploadTaskState.queued);
await store.updateState('task-0001', UploadTaskState.uploading);
final at = DateTime.utc(2026, 9, 19, 10);
final updated = await store.updateMeta(
'task-0001',
serverUploadId: 'upl-9',
serverPhotoId: 'pho-9',
now: at,
);
expect(updated.state, UploadTaskState.uploading, reason: '状态不变');
expect(updated.serverUploadId, 'upl-9');
expect(updated.serverPhotoId, 'pho-9');
expect(updated.updatedAt, at);
// 未提供的字段保持不变(二次回填只更新一个)。
final again = await store.updateMeta('task-0001', serverUploadId: 'upl-x');
expect(again.serverUploadId, 'upl-x');
expect(again.serverPhotoId, 'pho-9');
});
});
group('账号隔离(硬守护)', () {