import 'package:adhd_scheduler_core/scheduler_core.dart'; import 'package:test/test.dart'; void main() { group('Persistence edge-case regressions', () { final createdAt = DateTime.utc(2026, 6, 22, 8); test('task ids remain stable across copy and update operations', () { final original = scheduledTask( id: 'task-1', createdAt: createdAt, ); final updated = original.copyWith( title: 'Updated title', status: TaskStatus.active, updatedAt: createdAt.add(const Duration(minutes: 5)), ); expect(updated.id, original.id); expect(updated.title, 'Updated title'); expect(updated.status, TaskStatus.active); }); test('project ids remain stable across copy and update operations', () { final original = ProjectProfile( id: 'project-1', name: 'Project', colorKey: 'project-blue', ); final updated = original.copyWith( name: 'Renamed project', colorKey: 'project-green', ); expect(updated.id, original.id); expect(updated.name, 'Renamed project'); expect(updated.colorKey, 'project-green'); }); test('locked block ids remain stable across copy and update operations', () { final original = LockedBlock( id: 'locked-1', name: 'Work', startTime: ClockTime(hour: 9, minute: 0), endTime: ClockTime(hour: 17, minute: 0), recurrence: LockedBlockRecurrence.weekly( weekdays: {LockedWeekday.monday}, ), createdAt: createdAt, updatedAt: createdAt, ); final updated = original.copyWith( name: 'Adjusted work', hiddenByDefault: false, updatedAt: createdAt.add(const Duration(minutes: 10)), ); expect(updated.id, original.id); expect(updated.name, 'Adjusted work'); expect(updated.hiddenByDefault, isFalse); }); test('DateTime persistence uses UTC ISO-8601 strings and UTC comparison', () { final pacificTime = DateTime.parse('2026-06-22T09:00:00-07:00'); final sameInstantUtc = DateTime.utc(2026, 6, 22, 16); final stored = PersistenceDateTimeConvention.toStoredString(pacificTime); final restored = PersistenceDateTimeConvention.fromStoredString(stored); expect(PersistenceDateTimeConvention.description, contains('UTC')); expect(stored, '2026-06-22T16:00:00.000Z'); expect(restored, sameInstantUtc); expect( PersistenceDateTimeConvention.compare(pacificTime, sameInstantUtc), 0, ); }); test('nullable task fields are preserved unless explicitly cleared', () { final original = scheduledTask( id: 'nullable-task', createdAt: createdAt, ).copyWith( priority: PriorityLevel.high, parentTaskId: 'parent', ); final preserved = original.copyWith(title: 'Updated'); final clearedSchedule = original.copyWith(clearSchedule: true); expect(preserved.priority, PriorityLevel.high); expect(preserved.durationMinutes, original.durationMinutes); expect(preserved.scheduledStart, original.scheduledStart); expect(preserved.scheduledEnd, original.scheduledEnd); expect(preserved.parentTaskId, 'parent'); expect(clearedSchedule.scheduledStart, isNull); expect(clearedSchedule.scheduledEnd, isNull); expect(clearedSchedule.id, original.id); }); test('enum persistence names stay stable and distinguish not set values', () { expect(TaskType.flexible.persistenceName, 'flexible'); expect(TaskStatus.backlog.persistenceName, 'backlog'); expect(PriorityLevel.veryHigh.persistenceName, 'veryHigh'); expect(RewardLevel.notSet.persistenceName, 'notSet'); expect(RewardLevel.veryLow.persistenceName, 'veryLow'); expect(DifficultyLevel.veryHard.persistenceName, 'veryHard'); expect(ReminderProfile.gentle.persistenceName, 'gentle'); expect(BacklogTag.wishlist.persistenceName, 'wishlist'); expect(LockedWeekday.monday.persistenceName, 'monday'); expect(LockedBlockOverrideType.replace.persistenceName, 'replace'); expect( RewardLevel.notSet.persistenceName, isNot(RewardLevel.veryLow.persistenceName), ); }); test('backlog age behavior uses task creation timestamp', () { final oldButRecentlyUpdated = Task.quickCapture( id: 'old', title: 'Old backlog idea', createdAt: DateTime.utc(2026, 5, 1), updatedAt: DateTime.utc(2026, 6, 21), ); final view = BacklogView( tasks: [oldButRecentlyUpdated], now: DateTime.utc(2026, 6, 22), ); expect(view.filter(BacklogFilter.stale), [oldButRecentlyUpdated]); expect( view.stalenessMarkerFor(oldButRecentlyUpdated), BacklogStalenessMarker.purple, ); }); test('in-memory repositories round-trip scheduling fields', () async { final task = scheduledTask( id: 'scheduled', createdAt: createdAt, ); final repository = InMemoryTaskRepository(); await repository.save(task); final saved = await repository.findById(task.id); expect(saved, isNotNull); expect(saved!.id, task.id); expect(saved.scheduledStart, task.scheduledStart); expect(saved.scheduledEnd, task.scheduledEnd); expect(saved.durationMinutes, task.durationMinutes); expect(saved.projectId, task.projectId); expect(saved.reward, task.reward); expect(saved.difficulty, task.difficulty); }); test('repository read operations do not expose mutable collections', () async { final original = scheduledTask( id: 'immutable', createdAt: createdAt, ); final repository = InMemoryTaskRepository([original]); final savedList = await repository.findAll(); expect(() => savedList.add(original), throwsUnsupportedError); expect((await repository.findAll()).map((task) => task.id), [ 'immutable', ]); expect(original.scheduledStart, DateTime.utc(2026, 6, 22, 9)); }); test('document field sets preserve all task and statistics fields', () { expect(TaskDocumentFields.all, { '_id', 'title', 'projectId', 'type', 'status', 'priority', 'reward', 'difficulty', 'durationMinutes', 'scheduledStart', 'scheduledEnd', 'actualStart', 'actualEnd', 'parentTaskId', 'backlogTags', 'reminderOverride', 'createdAt', 'updatedAt', 'stats', }); expect(TaskStatisticsDocumentFields.all, { 'skippedDuringBurnoutCount', 'manuallyPushedCount', 'autoPushedCount', 'movedToBacklogCount', 'restoredFromBacklogCount', 'missedCount', 'cancelledCount', 'completedLateCount', 'completedDuringLockedHoursCount', 'completedDuringLockedHoursMinutes', }); }); test('document field sets cover project locked and snapshot documents', () { expect(ProjectDocumentFields.all, containsAll(['_id', 'colorKey'])); expect( LockedBlockDocumentFields.all, containsAll(['_id', 'startTime', 'recurrence', 'hiddenByDefault']), ); expect( LockedBlockOverrideDocumentFields.all, containsAll(['_id', 'lockedBlockId', 'type']), ); expect( SchedulingSnapshotDocumentFields.all, containsAll([ '_id', 'capturedAt', 'window', 'tasks', 'lockedIntervals', 'requiredVisibleIntervals', 'notices', 'changes', 'overlaps', 'operationName', ]), ); }); test('document field names are stable and migration-friendly', () { for (final fieldSet in PersistenceDocumentFieldSets.all) { expect(fieldSet, isNotEmpty); for (final fieldName in fieldSet) { expect(fieldName, isNotEmpty); expect(fieldName.trim(), fieldName); expect(fieldName.contains('.'), isFalse, reason: fieldName); expect(fieldName.startsWith(r'$'), isFalse, reason: fieldName); } } expect(DocumentFields.id, '_id'); expect(TaskDocumentFields.projectId, 'projectId'); expect(TaskDocumentFields.scheduledStart, 'scheduledStart'); expect(TaskDocumentFields.scheduledEnd, 'scheduledEnd'); }); }); } Task scheduledTask({ required String id, required DateTime createdAt, }) { return Task( id: id, title: 'Scheduled task', projectId: 'project-1', type: TaskType.flexible, status: TaskStatus.planned, priority: PriorityLevel.medium, reward: RewardLevel.high, difficulty: DifficultyLevel.medium, durationMinutes: 30, scheduledStart: DateTime.utc(2026, 6, 22, 9), scheduledEnd: DateTime.utc(2026, 6, 22, 9, 30), createdAt: createdAt, updatedAt: createdAt, ); }