import 'dart:async'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:pj_supermarket_cloud/common/index.dart'; import 'package:pj_supermarket_cloud/common/utils/sync/face_sync_plan.dart'; import 'package:pj_supermarket_cloud/common/utils/sync/face_image_download_queue.dart'; import 'package:pj_supermarket_cloud/common/services/database/type_converter/face_timestamp_converter.dart'; ServiceFaceSyncRecord record(String id, {DateTime? version, String? name}) => ServiceFaceSyncRecord( id: id, sampleId: id, userId: '20', deptId: '10', faceImg: 'photo', sourceUpdatedAt: version, username: name); FaceEntity local(String id, {DateTime? version, String? deptName}) => FaceEntity( sampleId: id, userId: '20', userName: '', deptId: '10', deptName: deptName, imagePath: 'photo', path: '/$id.jpg', faceModel: [1], updateTime: version); void main() { test('重复同步与空部门名直接跳过,缺失版本只需校验一次照片', () { final remote = [record('1'), record('2', version: DateTime(2026))]; final first = FaceSyncPlan.build( remote: remote, local: [local('1'), local('2')], existingPaths: {'/1.jpg', '/2.jpg'}); expect(first.changes.map((x) => x.kind), [FaceSyncChangeKind.unchanged, FaceSyncChangeKind.image]); final second = FaceSyncPlan.build( remote: remote, local: [local('1'), local('2', version: DateTime(2026))], existingPaths: {'/1.jpg', '/2.jpg'}); expect(second.changes.every((x) => x.kind == FaceSyncChangeKind.unchanged), isTrue); }); test('新增、照片版本变化和文件缺失只处理对应样本', () { final plan = FaceSyncPlan.build( remote: [record('1'), record('2', version: DateTime(2026)), record('3')], local: [local('1'), local('2', version: DateTime(2025))], existingPaths: {'/2.jpg'}, ); expect( plan.changes.every((x) => x.kind == FaceSyncChangeKind.image), isTrue); }); test('空部门权威清单可删除最后一张照片,并保留调试导入的外部门样本', () { final foreign = local('2')..deptId = '99'; final plan = FaceSyncPlan.build( remote: [], local: [local('1'), foreign], existingPaths: {}, scopeDeptIds: {'10', '11'}); expect(plan.orphans.map((x) => x.sampleId), ['1']); }); test('冲突编号被保护,本地和远端重复清单会拒绝执行', () { final plan = FaceSyncPlan.build( remote: [], local: [local('1'), local('2')], existingPaths: {}, protectedSampleIds: {'1'}); expect(plan.orphans.map((x) => x.sampleId), ['2']); expect( () => FaceSyncPlan.build( remote: [record('1'), record('1')], local: [], existingPaths: {}), throwsA(isA())); expect( () => FaceSyncPlan.build( remote: [], local: [local('1'), local('1')], existingPaths: {}), throwsA(isA())); }); test('时间版本保留微秒和时区,旧秒级格式仍可读取', () { final converter = FaceTimestampConverter(); final version = DateTime.parse('2026-10-22T08:00:01.123456Z'); expect(converter.decode(converter.encode(version)), version); expect(converter.decode('2026-10-22 08:00:01'), DateTime(2026, 10, 22, 8, 0, 1)); final plan = FaceSyncPlan.build( remote: [record('1', version: version)], local: [local('1', version: version.toLocal())], existingPaths: {'/1.jpg'}); expect(plan.changes.single.kind, FaceSyncChangeKind.unchanged); }); test('消费未完成的预下载会等待同一任务,预取窗口始终有界', () async { final tasks = >{}; final calls = []; final discarded = []; final queue = FaceImageDownloadQueue( records: List.generate(30, (i) => record('${i + 1}')), download: (face) { calls.add(face.sampleId); return (tasks[face.sampleId] = Completer()).future; }, discard: (file) async => discarded.add(file.path), ); expect(calls, ['1', '2', '3']); final take = queue.take('1'); expect(calls.length, 3); tasks['1']!.complete(File('/1')); expect((await take)!.path, '/1'); expect(calls, ['1', '2', '3', '4']); final closing = queue.dispose(); for (final id in ['2', '3', '4']) { tasks[id]!.complete(File('/$id')); } await closing; expect(calls.length, 4); expect(discarded.toSet(), {'/2', '/3', '/4'}); expect(queue.consumedCount, 1); }); test('下载异常被消费方接管,不产生未处理异步异常', () async { final queue = FaceImageDownloadQueue( records: [record('1')], download: (_) async => throw const SocketException('offline'), discard: (_) async {}); expect(await queue.take('1'), isNull); await queue.dispose(); expect(queue.downloadCount, 1); }); }