feat: 重构会话管理与提醒通知系统

This commit is contained in:
qzl
2026-03-31 18:26:36 +08:00
parent a8c262e9c7
commit 9a231dae9e
31 changed files with 650 additions and 223 deletions
@@ -75,7 +75,11 @@ void main() {
});
test('home route screen is wrapped with ChatBloc provider', () {
final widget = buildHomeRouteScreen();
final widget = buildHomeRouteScreen(
const AuthAuthenticated(
user: AuthUser(id: 'u1', phone: '13800138000'),
),
);
expect(widget, isA<BlocProvider<ChatBloc>>());
});
@@ -0,0 +1,63 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:social_app/app/services/session_scope_manager.dart';
import 'package:social_app/data/cache/cache_scope.dart';
import 'package:social_app/data/cache/cache_store.dart';
class _RecordingHybridCacheStore extends HybridCacheStore {
_RecordingHybridCacheStore()
: super(memory: MemoryCacheStore(), persistent: PersistentCacheStore());
final List<String> clearedPrefixes = <String>[];
@override
Future<void> clearByPrefix(String prefix) async {
clearedPrefixes.add(prefix);
}
}
void main() {
late _RecordingHybridCacheStore cacheStore;
late SessionScopeManager manager;
setUp(() {
cacheStore = _RecordingHybridCacheStore();
manager = SessionScopeManager(cacheStore: cacheStore);
CacheScope.resetProvider();
});
tearDown(() {
CacheScope.resetProvider();
});
test('activate configures stable user scope token', () async {
await manager.activate('u-1');
expect(CacheScope.token(), 'user:u-1');
expect(cacheStore.clearedPrefixes, isEmpty);
});
test('activate clears previous user cache when switching user', () async {
await manager.activate('u-1');
await manager.activate('u-2');
expect(cacheStore.clearedPrefixes, ['cache:user:u-1:']);
expect(CacheScope.token(), 'user:u-2');
});
test('activate does not clear cache when user is unchanged', () async {
await manager.activate('u-1');
await manager.activate('u-1');
expect(cacheStore.clearedPrefixes, isEmpty);
expect(CacheScope.token(), 'user:u-1');
});
test('clearActiveUserScope clears current user cache', () async {
await manager.activate('u-1');
await manager.clearActiveUserScope();
expect(cacheStore.clearedPrefixes, ['cache:user:u-1:']);
expect(() => CacheScope.token(), throwsStateError);
});
}
@@ -1,3 +1,6 @@
import 'dart:convert';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:social_app/core/notification/models/reminder_alarm.dart';
import 'package:social_app/core/notification/services/reminder_scheduler_service.dart';
@@ -63,4 +66,125 @@ void main() {
expect(alarms, isEmpty);
});
test('buildUpsertPlan skips unchanged pending reminders', () {
final alarm = ReminderAlarm(
eventId: 'evt_5',
title: 'Sync',
startAt: DateTime(2026, 4, 1, 10, 0),
endAt: DateTime(2026, 4, 1, 11, 0),
timezone: 'Asia/Shanghai',
reminderMinutes: 15,
fireAt: DateTime(2026, 4, 1, 9, 45),
fireTimeBucket: 29114145,
location: 'Room 1',
notes: 'Bring notes',
);
final id = ReminderSchedulerService.notificationIdFor(
alarm.eventId,
alarm.fireTimeBucket,
);
final pending = [
PendingNotificationRequest(
id,
alarm.title,
'开始时间 10:00 · Room 1',
jsonEncode(alarm.toJson()),
),
];
final plan = ReminderSchedulerService.buildUpsertPlan(
eventId: alarm.eventId,
pending: pending,
desired: [alarm],
);
expect(plan.idsToCancel, isEmpty);
expect(plan.alarmsToSchedule, isEmpty);
});
test('buildUpsertPlan reschedules when payload changed', () {
final desired = ReminderAlarm(
eventId: 'evt_6',
title: 'Daily',
startAt: DateTime(2026, 4, 1, 8, 0),
endAt: DateTime(2026, 4, 1, 9, 0),
timezone: 'Asia/Shanghai',
reminderMinutes: 10,
fireAt: DateTime(2026, 4, 1, 7, 50),
fireTimeBucket: 29114030,
location: 'Desk',
notes: 'Updated',
);
final stale = ReminderAlarm(
eventId: desired.eventId,
title: desired.title,
startAt: desired.startAt,
endAt: desired.endAt,
timezone: desired.timezone,
reminderMinutes: desired.reminderMinutes,
fireAt: desired.fireAt,
fireTimeBucket: desired.fireTimeBucket,
location: desired.location,
notes: 'Old notes',
);
final id = ReminderSchedulerService.notificationIdFor(
desired.eventId,
desired.fireTimeBucket,
);
final plan = ReminderSchedulerService.buildUpsertPlan(
eventId: desired.eventId,
pending: [
PendingNotificationRequest(
id,
stale.title,
'开始时间 08:00 · Desk',
jsonEncode(stale.toJson()),
),
],
desired: [desired],
);
expect(plan.idsToCancel, [id]);
expect(plan.alarmsToSchedule.length, 1);
expect(plan.alarmsToSchedule.first.notes, 'Updated');
});
test('buildUpsertPlan cancels and rebuilds when payload is invalid', () {
final desired = ReminderAlarm(
eventId: 'evt_7',
title: 'Check-in',
startAt: DateTime(2026, 4, 2, 9, 0),
endAt: DateTime(2026, 4, 2, 9, 30),
timezone: 'Asia/Shanghai',
reminderMinutes: 10,
fireAt: DateTime(2026, 4, 2, 8, 50),
fireTimeBucket: 29115530,
location: 'Room A',
notes: 'fresh',
);
final id = ReminderSchedulerService.notificationIdFor(
desired.eventId,
desired.fireTimeBucket,
);
final plan = ReminderSchedulerService.buildUpsertPlan(
eventId: desired.eventId,
pending: [
PendingNotificationRequest(
id,
desired.title,
'开始时间 09:00 · Room A',
'{"eventId":"${desired.eventId}"}',
),
],
desired: [desired],
);
expect(plan.idsToCancel, [id]);
expect(plan.alarmsToSchedule.length, 1);
expect(plan.alarmsToSchedule.first.eventId, desired.eventId);
});
}
+26
View File
@@ -1,5 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:social_app/data/cache/cache_policy.dart';
import 'package:social_app/data/cache/cache_scope.dart';
import 'package:social_app/data/cache/cached_repository.dart';
import 'package:social_app/data/cache/cache_store.dart';
@@ -29,6 +30,14 @@ class _IntCachedRepository extends CachedRepository<int> {
void main() {
group('CachedRepository', () {
setUp(() {
CacheScope.configureProvider(() => 'user:test');
});
tearDown(() {
CacheScope.resetProvider();
});
test('reads from cache after first load', () async {
final repo = _IntCachedRepository(
store: HybridCacheStore(
@@ -59,5 +68,22 @@ void main() {
expect(refreshed, 2);
expect(repo.loadCount, 2);
});
test('falls back to remote load when scope is unavailable', () async {
CacheScope.resetProvider();
final repo = _IntCachedRepository(
store: HybridCacheStore(
memory: MemoryCacheStore(),
persistent: PersistentCacheStore(),
),
);
final first = await repo.fetch();
final second = await repo.fetch();
expect(first, 1);
expect(second, 2);
expect(repo.loadCount, 2);
});
});
}
+16
View File
@@ -1,5 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:social_app/data/cache/cache_scope.dart';
import 'package:social_app/data/cache/cache_store.dart';
void main() {
@@ -24,5 +25,20 @@ void main() {
final secondRead = await hybrid.read<String>('k');
expect(secondRead, 'v');
});
test('cache invalidator no-ops without configured scope', () async {
CacheScope.resetProvider();
final invalidator = CacheInvalidator(
store: HybridCacheStore(
memory: MemoryCacheStore(),
persistent: PersistentCacheStore(),
),
);
expect(
() => invalidator.invalidate('calendar:day:2026-03-31'),
returnsNormally,
);
});
});
}
@@ -82,7 +82,7 @@ class _FakeChatApi implements ChatApi {
void main() {
setUp(() {
CacheScope.configureProvider(() => null);
CacheScope.configureProvider(() => 'user:test');
});
tearDown(() {