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
@@ -31,6 +31,22 @@ final Map<String, dynamic> initResponseFixture = <String, dynamic>{
'credential': credentialFixture,
};
/// UploadSessionRecord 最小形状(sourceDeviceId 测试基底)。
const Map<String, dynamic> _recordFixture = <String, dynamic>{
'uploadId': 'upl0000002',
'albumId': 'cktragt000001',
'clientTaskId': 'task-uuid-0002',
'state': 'confirmed',
'expectedBytes': 8192000,
'contentType': 'image/png',
'createdAt': '2026-09-19T09:15:30.000Z',
'photo': {
'photoId': 'pho0000002',
'processingState': 'uploaded',
'visibility': 'unpublished',
},
};
void main() {
test('UploadCredential fromJson/toJson 往返(e2e 凭证形状)', () {
final model = UploadCredential.fromJson(credentialFixture);
@@ -76,6 +92,27 @@ void main() {
expect(withCategory.toJson()['categoryId'], 'ckucat0000001');
});
test('UploadInitRequest sourceDeviceId(P3 Task 3 契约增补):'
'提供时下发、缺省不下发', () {
const withDevice = UploadInitRequest(
albumId: 'cktragt000001',
clientTaskId: 'task-uuid-0003',
filename: 'IMG_20260919_0003.jpg',
bytes: 1024,
contentType: 'image/jpeg',
sourceDeviceId: '0123456789abcdef0123456789abcdef',
);
expect(withDevice.toJson()['sourceDeviceId'], isNotNull);
const withoutDevice = UploadInitRequest(
albumId: 'cktragt000001',
clientTaskId: 'task-uuid-0004',
filename: 'IMG_20260919_0004.jpg',
bytes: 1024,
contentType: 'image/jpeg',
);
expect(withoutDevice.toJson().containsKey('sourceDeviceId'), isFalse);
});
test('UploadCompleteResponse fromJson/toJson 往返', () {
final fixture = <String, dynamic>{
'photoId': 'pho0000001',
@@ -108,6 +145,19 @@ void main() {
expect(model.toJson(), equals(fixture));
});
test('UploadSessionRecord sourceDeviceId:可空回显(A19 设备来源)', () {
final withDevice = UploadSessionRecord.fromJson({
..._recordFixture,
'sourceDeviceId': '0123456789abcdef0123456789abcdef',
});
expect(withDevice.sourceDeviceId, '0123456789abcdef0123456789abcdef');
expect(withDevice.toJson()['sourceDeviceId'], isNotNull);
// init 未携带时为 null(契约 type: [string, 'null'])。
final withoutDevice = UploadSessionRecord.fromJson(_recordFixture);
expect(withoutDevice.sourceDeviceId, isNull);
expect(withoutDevice.toJson().containsKey('sourceDeviceId'), isFalse);
});
test('UploadSessionState 全枚举解析(pending/confirmed/cancelled/expired)',
() {
for (final value in const ['pending', 'confirmed', 'cancelled', 'expired']) {