docs: pin reminder identity and calendar permission contracts
This commit is contained in:
@@ -89,7 +89,7 @@ test('morning break before period 3 is fifteen minutes', () {
|
|||||||
- `ImportDraft({required List<Course> courses, required bool explicitlyEmpty, required CourseSource source, required String parserVersion})`;`JsonScheduleImport.parse(String text) -> ImportDraft`,只接受 schemaVersion 1、结构化字段和最大 2 MiB UTF-8 数据。根字段为 `schemaVersion`、`isDemo`、`explicitlyEmpty`、`courses`;课程字段为 `id/name/teacher/room/weekday/startPeriod/endPeriod/weeks`,weeks 是整数数组。文件来源仅可为 file 或 demo,不能冒充 school;isDemo=true 进入示例体验。
|
- `ImportDraft({required List<Course> courses, required bool explicitlyEmpty, required CourseSource source, required String parserVersion})`;`JsonScheduleImport.parse(String text) -> ImportDraft`,只接受 schemaVersion 1、结构化字段和最大 2 MiB UTF-8 数据。根字段为 `schemaVersion`、`isDemo`、`explicitlyEmpty`、`courses`;课程字段为 `id/name/teacher/room/weekday/startPeriod/endPeriod/weeks`,weeks 是整数数组。文件来源仅可为 file 或 demo,不能冒充 school;isDemo=true 进入示例体验。
|
||||||
- `NotificationService.reconcile(List<CourseOccurrence> occurrences, AppSettings settings, DateTime now) -> Future<ReminderResult>`,`requestPermissions() -> Future<void>`、`cancelOwned() -> Future<void>`;reconcile 仅检查权限,不自行弹窗。`ReminderResult` 的 status 为 off/permissionDenied/exact/inexact/error,包含 scheduledCount 与说明。
|
- `NotificationService.reconcile(List<CourseOccurrence> occurrences, AppSettings settings, DateTime now) -> Future<ReminderResult>`,`requestPermissions() -> Future<void>`、`cancelOwned() -> Future<void>`;reconcile 仅检查权限,不自行弹窗。`ReminderResult` 的 status 为 off/permissionDenied/exact/inexact/error,包含 scheduledCount 与说明。
|
||||||
- `CalendarService.requestAccess() -> Future<bool>`,`listWritableCalendars() -> Future<List<DeviceCalendar>>`,`sync(List<CourseOccurrence> occurrences, String calendarId, Map<String,String> knownEventIds) -> Future<Map<String,String>>`,`deleteOwned(Map<String,String> eventIds) -> Future<void>`;`DeviceCalendar(id,name)`。
|
- `CalendarService.requestAccess() -> Future<bool>`,`listWritableCalendars() -> Future<List<DeviceCalendar>>`,`sync(List<CourseOccurrence> occurrences, String calendarId, Map<String,String> knownEventIds) -> Future<Map<String,String>>`,`deleteOwned(Map<String,String> eventIds) -> Future<void>`;`DeviceCalendar(id,name)`。
|
||||||
- `AppController(SnapshotStore store, NotificationService notifications, CalendarService calendar, DateTime Function() clock)` extends ChangeNotifier;暴露持久化 `snapshot`、显示用 `visibleCourses/visibleSettings`、`isDemo`、`lastError`、`reminderResult`、`calendarSyncError`。`initialize()`、`showDemo({List<Course>? courses})`、`leaveDemo()`、`updateSettings(AppSettings value)`、`applyImport(ImportDraft draft,{required bool confirmedEmpty})`、`clearCourses()`、`disableCalendar({required bool deleteEvents})`、`enableRemindersFromUserAction()` 均返回 Future<void>。示例有单独临时数据和“示例”学期,以 clock 所在周为第 1 周,不改变真实快照。clearCourses 始终清理对应通知和自有日历事件;仅关闭日历的保留选择由 disableCalendar 处理。
|
- `AppController(SnapshotStore store, NotificationService notifications, CalendarService calendar, DateTime Function() clock)` extends ChangeNotifier;暴露持久化 `snapshot`、显示用 `visibleCourses/visibleSettings`、`isDemo`、`lastError`、`reminderResult`、`calendarSyncError`。`initialize()`、`showDemo({List<Course>? courses})`、`leaveDemo()`、`updateSettings(AppSettings value)`、`applyImport(ImportDraft draft,{required bool confirmedEmpty})`、`clearCourses()`、`disableCalendar({required bool deleteEvents})`、`enableCalendarFromUserAction(String calendarId)`、`enableRemindersFromUserAction()` 均返回 Future<void>。示例有单独临时数据和“示例”学期,以 clock 所在周为第 1 周,不改变真实快照。clearCourses 始终清理对应通知和自有日历事件;仅关闭日历的保留选择由 disableCalendar 处理。
|
||||||
|
|
||||||
- [ ] 写失败测试:有效 JSON 转为确定周次集合;部分记录非法使整个草稿失败;空数组且 explicitlyEmpty=false 不得覆盖;重复 ID 拒绝;schemaVersion 未知和超过 2 MiB 拒绝;isDemo=true 导入仅改变显示数据,不改真实快照、不调用平台服务。
|
- [ ] 写失败测试:有效 JSON 转为确定周次集合;部分记录非法使整个草稿失败;空数组且 explicitlyEmpty=false 不得覆盖;重复 ID 拒绝;schemaVersion 未知和超过 2 MiB 拒绝;isDemo=true 导入仅改变显示数据,不改真实快照、不调用平台服务。
|
||||||
- [ ] 写失败测试:临时目录保存、重新打开恢复所有课程/设置;损坏文件报错且不返回“空课表”;模拟保存异常后控制器仍保留旧 snapshot;showDemo 不调用通知或日历;快速并发两次更新按顺序保留最后一份设置;移除课程后只清理本 APP 的副作用。
|
- [ ] 写失败测试:临时目录保存、重新打开恢复所有课程/设置;损坏文件报错且不返回“空课表”;模拟保存异常后控制器仍保留旧 snapshot;showDemo 不调用通知或日历;快速并发两次更新按顺序保留最后一份设置;移除课程后只清理本 APP 的副作用。
|
||||||
@@ -153,15 +153,14 @@ test('lookalike auth host cannot receive credentials', () {
|
|||||||
|
|
||||||
**Files:** 创建 local_notification_service、`mobile/test/services/notification_service_test.dart`;修改 `mobile/android/app/build.gradle.kts`、`mobile/android/app/src/main/AndroidManifest.xml`、`mobile/android/app/src/main/res/drawable/ic_stat_calendar.xml`、settings_page/main。
|
**Files:** 创建 local_notification_service、`mobile/test/services/notification_service_test.dart`;修改 `mobile/android/app/build.gradle.kts`、`mobile/android/app/src/main/AndroidManifest.xml`、`mobile/android/app/src/main/res/drawable/ic_stat_calendar.xml`、settings_page/main。
|
||||||
|
|
||||||
**Interfaces:** `LocalNotificationService` implements Task 2 NotificationService;可注入 `ReminderGateway`,其 `permissions({required bool request}) -> Future<ReminderPermissions>`、`pendingIds() -> Future<Set<int>>`、`schedule(ReminderRequest value) -> Future<void>`、`cancel(int id) -> Future<void>`。`ReminderRequest(id, occurrenceKey, fireAt, title, body, exact)`;`ReminderPermissions(notifications,exactAlarm)`。`notificationId(String occurrenceKey) -> int` 使用固定算法并显式解决本次集合碰撞,不能依赖 Dart hashCode。
|
**Interfaces:** `LocalNotificationService` implements Task 2 NotificationService;可注入 `ReminderGateway`,其 `permissions({required bool request}) -> Future<ReminderPermissions>`、`pendingIds() -> Future<Set<int>>`、`schedule(ReminderRequest value) -> Future<void>`、`cancel(int id) -> Future<void>`。`ReminderRequest(id, occurrenceKey, fireAt, title, body, exact)`;`ReminderPermissions(notifications,exactAlarm)`。`notificationId(String occurrenceKey) -> int` 使用 FNV-1a 32-bit 后取正 31-bit;本次集合碰撞按排序后线性探测解决,不能依赖 Dart hashCode。ID 变化时先取消旧计划再重排,不能覆盖碰撞课程。
|
||||||
|
|
||||||
- [ ] 写失败测试:08:30 课程默认 08:15 触发;已过提醒时间不排任务;只排实际上课周、每连续课程一次;demo、无学期、提醒关闭不排;重复 reconcile 无重复;删除课程取消旧 ID;通知权限拒绝返回 permissionDenied;精确权限拒绝可用 inexact 并显示说明;撤销权限后不显示已生效。
|
- [ ] 写失败测试:08:30 课程默认 08:15 触发;已过提醒时间不排任务;只排实际上课周、每连续课程一次;demo、无学期、提醒关闭不排;重复 reconcile 无重复;删除课程取消旧 ID;通知权限拒绝返回 permissionDenied;精确权限拒绝可用 inexact 并显示说明;撤销权限后不显示已生效。
|
||||||
- [ ] 写失败测试:500 条以上未来实例时仅保留最早 400 条并显示“后续提醒在再次打开时补充”;初始化/设置更改补充未来任务,Android 重启恢复已安排的任务。用 FakeReminderGateway 检验相同实例保留稳定 ID。
|
- [ ] 写失败测试:500 条以上未来实例时仅保留最早 400 条并显示“后续提醒在再次打开时补充”;初始化/设置更改补充未来任务,Android 重启恢复已安排的任务。用 FakeReminderGateway 检验相同实例保留稳定 ID。
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
test('notification identity is stable', () {
|
test('notification identity is stable', () {
|
||||||
expect(notificationId('math/2026-09-09'), notificationId('math/2026-09-09'));
|
expect(notificationId('math/2026-09-09'), 1794407394);
|
||||||
expect(notificationId('math/2026-09-09'), isNonNegative);
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -181,12 +180,13 @@ test('notification identity is stable', () {
|
|||||||
```dart
|
```dart
|
||||||
test('denied calendar permission keeps calendar off', () async {
|
test('denied calendar permission keeps calendar off', () async {
|
||||||
// setup 中 Fake CalendarService.requestAccess 返回 false。
|
// setup 中 Fake CalendarService.requestAccess 返回 false。
|
||||||
expect(await calendar.requestAccess(), isFalse);
|
await controller.enableCalendarFromUserAction('selected-calendar');
|
||||||
expect(controller.snapshot.settings.calendarEnabled, isFalse);
|
expect(controller.snapshot.settings.calendarEnabled, isFalse);
|
||||||
|
expect(controller.calendarSyncError, isNotNull);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] 运行 `flutter test test/services/calendar_service_test.dart` 确认失败;实现 READ_CALENDAR/WRITE_CALENDAR 运行时权限,选日历后才开启设置;写入 TZ 实例对应的 UTC 毫秒与 EVENT_TIMEZONE=Asia/Shanghai。
|
- [ ] 运行 `flutter test test/services/calendar_service_test.dart` 确认失败;实现 READ_CALENDAR/WRITE_CALENDAR 运行时权限。enableCalendarFromUserAction 检查授权和可写日历,成功同步后才开启设置;写入 TZ 实例对应的 UTC 毫秒与 EVENT_TIMEZONE=Asia/Shanghai。
|
||||||
- [ ] 实现 Kotlin 桥接和 Dart 重试逻辑。系统写入与快照无法组成跨系统事务,先凭 app URI 查询去重再写;每次已完成映射及时持久化,重启后可恢复。仅操作用户选定日历,无法校验拥有标记时跳过;disableCalendar 保留时关闭后续自动同步,删除时先清理自有事件。清理失败保留待重试映射,不丢失追踪。
|
- [ ] 实现 Kotlin 桥接和 Dart 重试逻辑。系统写入与快照无法组成跨系统事务,先凭 app URI 查询去重再写;每次已完成映射及时持久化,重启后可恢复。仅操作用户选定日历,无法校验拥有标记时跳过;disableCalendar 保留时关闭后续自动同步,删除时先清理自有事件。清理失败保留待重试映射,不丢失追踪。
|
||||||
- [ ] 同一测试通过;`flutter test integration_test/calendar_bridge_test.dart -d emulator-5554` 验证创建、更新、删除和权限结果。没有可写日历时设备测试明确跳过并报告;仅使用测试专用日历,不向用户其他日历写测试数据。提交。
|
- [ ] 同一测试通过;`flutter test integration_test/calendar_bridge_test.dart -d emulator-5554` 验证创建、更新、删除和权限结果。没有可写日历时设备测试明确跳过并报告;仅使用测试专用日历,不向用户其他日历写测试数据。提交。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user