import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:patbond_flutter/analytics/analytics_event_store.dart'; import 'package:shared_preferences/shared_preferences.dart'; Map event(int seq) => { 'eventId': 'id-$seq', 'eventName': 'auth_login_succeeded', 'seq': seq, }; void main() { group('AnalyticsEventStore', () { test('事件持久化后新实例冷启动可恢复(顺序保持旧 → 新)', () async { SharedPreferences.setMockInitialValues({}); final store = AnalyticsEventStore(); await store.restore(); for (var i = 0; i < 45; i++) { await store.add(event(i)); } final reborn = AnalyticsEventStore(); await reborn.restore(); expect(reborn.length, 45); expect(reborn.events.first['seq'], 0); expect(reborn.events.last['seq'], 44); // 45 条按容量 20 分段应为 3 段 final prefs = await SharedPreferences.getInstance(); final index = jsonDecode(prefs.getString(AnalyticsEventStore.segIndexKey)!) as List; expect(index.length, 3); }); test('超过 500 条总上限时丢最旧整段', () async { SharedPreferences.setMockInitialValues({}); final store = AnalyticsEventStore(); await store.restore(); for (var i = 0; i < 520; i++) { await store.add(event(i)); } // 第 501 条触发淘汰最旧一段(20 条),此后回落到上限内 expect(store.length, 500); expect(store.events.first['seq'], 20); expect(store.events.last['seq'], 519); expect(store.droppedCount, 20); }); test('损坏段丢弃不崩溃,其余段正常恢复且索引被清理', () async { SharedPreferences.setMockInitialValues({ AnalyticsEventStore.segIndexKey: jsonEncode(['bad', 'good']), '${AnalyticsEventStore.segKeyPrefix}bad': '{not-valid-json[[', '${AnalyticsEventStore.segKeyPrefix}good': jsonEncode([event(7)]), }); final store = AnalyticsEventStore(); await store.restore(); expect(store.length, 1); expect(store.events.single['seq'], 7); expect(store.droppedCount, 1); final prefs = await SharedPreferences.getInstance(); expect(prefs.getString('${AnalyticsEventStore.segKeyPrefix}bad'), isNull); expect(jsonDecode(prefs.getString(AnalyticsEventStore.segIndexKey)!), [ 'good', ]); }); test('段索引本身损坏时清扫孤儿段、从空队列重建', () async { SharedPreferences.setMockInitialValues({ AnalyticsEventStore.segIndexKey: 'garbage-not-json', '${AnalyticsEventStore.segKeyPrefix}orphan': jsonEncode([event(1)]), }); final store = AnalyticsEventStore(); await store.restore(); expect(store.length, 0); final prefs = await SharedPreferences.getInstance(); expect( prefs.getString('${AnalyticsEventStore.segKeyPrefix}orphan'), isNull, ); // 可继续正常写入 await store.add(event(2)); expect(store.length, 1); }); test('takeBatch 封段:在途批次不受新事件影响,删段后新事件保留', () async { SharedPreferences.setMockInitialValues({}); final store = AnalyticsEventStore(); await store.restore(); for (var i = 0; i < 5; i++) { await store.add(event(i)); } final batch = store.takeBatch(50); // 模拟上传在途期间进入的新事件 await store.add(event(99)); expect(batch.events.length, 5); await store.removeSegments(batch.segmentIds); expect(store.length, 1); expect(store.events.single['seq'], 99); }); test('takeBatch 按段拼批不超过单批上限', () async { SharedPreferences.setMockInitialValues({}); final store = AnalyticsEventStore(); await store.restore(); for (var i = 0; i < 60; i++) { await store.add(event(i)); } // 段容量 20、单批上限 50 → 每批最多 2 整段(40 条) final first = store.takeBatch(50); expect(first.events.length, 40); await store.removeSegments(first.segmentIds); final second = store.takeBatch(50); expect(second.events.length, 20); }); test('上传成功删段后 shared_preferences 无残留', () async { SharedPreferences.setMockInitialValues({}); final store = AnalyticsEventStore(); await store.restore(); for (var i = 0; i < 20; i++) { await store.add(event(i)); } final batch = store.takeBatch(50); await store.removeSegments(batch.segmentIds); expect(store.length, 0); final prefs = await SharedPreferences.getInstance(); expect( prefs.getKeys().where( (key) => key.startsWith(AnalyticsEventStore.segKeyPrefix), ), isEmpty, ); expect( jsonDecode(prefs.getString(AnalyticsEventStore.segIndexKey)!), isEmpty, ); }); test('未调用 restore(持久化不可用)时降级为纯内存队列', () async { final store = AnalyticsEventStore(); await store.add(event(1)); expect(store.length, 1); final batch = store.takeBatch(50); await store.removeSegments(batch.segmentIds); expect(store.length, 0); }); }); }