import 'dart:convert'; import 'dart:io'; import 'package:adhd_scheduler_core/scheduler_core.dart'; import 'package:test/test.dart'; void main() { group('V0 to V1 document migrations', () { final migratedAt = DateTime.utc(2026, 6, 25, 12); const ownerId = 'owner-1'; final service = V1DocumentMigrationService( ownerId: ownerId, migratedAt: migratedAt, ); test('legacy minimum task migrates with backlog provenance', () { final legacy = fixture('task_v0_minimum.json'); final result = service.migrateTask(legacy); final document = result.document!; final task = TaskDocumentMapping.fromDocument(document); expect(result.isSuccess, isTrue); expect(result.shouldWrite, isTrue); expect(result.report.status, DocumentMigrationStatus.migrated); expect(result.report.fromSchemaVersion, 0); expect(result.report.documentId, 'task-v0-minimum'); expect(result.report.notes.single.code, DocumentMigrationNoteCode.approximatedBacklogEnteredAt); expect(legacy.containsKey(DocumentFields.schemaVersion), isFalse); expect(document, containsPair(DocumentFields.schemaVersion, 1)); expect(document, containsPair(DocumentFields.ownerId, ownerId)); expect(document, containsPair(DocumentFields.revision, 1)); expect(document, containsPair(TaskDocumentFields.reward, 'not_set')); expect(document, containsPair(TaskDocumentFields.difficulty, 'not_set')); expect( document, containsPair( TaskDocumentFields.backlogEnteredAt, '2026-06-20T09:15:00.000Z', ), ); expect( document, containsPair( TaskDocumentFields.backlogEnteredAtProvenance, DocumentMigrationProvenance.approximatedFromCreatedAt, ), ); expect(task.id, 'task-v0-minimum'); expect(task.status, TaskStatus.backlog); expect(task.stats.manuallyPushedCount, 0); }); test('legacy full task migrates enum names to stable codes', () { final legacy = fixture('task_v0_full.json'); final result = service.migrateTask(legacy); final document = result.document!; final task = TaskDocumentMapping.fromDocument(document); expect(result.isSuccess, isTrue); expect(document[TaskDocumentFields.type], 'free_slot'); expect(document[TaskDocumentFields.status], 'no_longer_relevant'); expect(document[TaskDocumentFields.priority], 'very_high'); expect(document[TaskDocumentFields.reward], 'very_low'); expect(document[TaskDocumentFields.difficulty], 'very_hard'); expect(document[TaskDocumentFields.backlogTags], ['wishlist']); expect(document[TaskDocumentFields.backlogEnteredAt], isNull); expect(document[TaskDocumentFields.backlogEnteredAtProvenance], isNull); expect(task.type, TaskType.freeSlot); expect(task.status, TaskStatus.noLongerRelevant); expect(task.priority, PriorityLevel.veryHigh); expect(task.reward, RewardLevel.veryLow); expect(task.difficulty, DifficultyLevel.veryHard); expect(task.stats.totalPushesBeforeCompletion, 13); }); test('legacy project and locked documents migrate to V1 shapes', () { final projectResult = service.migrateProject(fixture('project_v0.json')); final blockResult = service.migrateLockedBlock(fixture('locked_block_v0.json')); final overrideResult = service.migrateLockedBlockOverride( fixture('locked_override_v0.json'), ); expect(projectResult.isSuccess, isTrue); expect( projectResult.report.notes.single.code, DocumentMigrationNoteCode.synthesizedMetadataTimestamp, ); expect( projectResult.document![ProjectDocumentFields.defaultPriority], 'very_high', ); expect( projectResult.document![ProjectDocumentFields.defaultReward], 'not_set', ); expect( projectResult.document![ProjectDocumentFields.defaultDifficulty], 'very_easy', ); expect( projectResult.document![DocumentFields.createdAt], '2026-06-25T12:00:00.000Z', ); expect( ProjectDocumentMapping.fromDocument(projectResult.document!).id, 'project-v0-home', ); final recurrence = blockResult.document![LockedBlockDocumentFields.recurrence] as Map; expect(blockResult.isSuccess, isTrue); expect( blockResult.document![LockedBlockDocumentFields.startTime], '09:00'); expect(blockResult.document![LockedBlockDocumentFields.endTime], '17:00'); expect( recurrence[LockedBlockRecurrenceDocumentFields.type], 'weekly', ); expect( recurrence[LockedBlockRecurrenceDocumentFields.weekdays], ['monday', 'thursday'], ); expect( LockedBlockDocumentMapping.fromDocument(blockResult.document!) .recurrence! .weekdays, {LockedWeekday.monday, LockedWeekday.thursday}, ); expect(overrideResult.isSuccess, isTrue); expect( overrideResult.document![LockedBlockOverrideDocumentFields.date], '2026-06-25', ); expect( overrideResult.document![LockedBlockOverrideDocumentFields.startTime], '10:00', ); expect( overrideResult.document![LockedBlockOverrideDocumentFields.endTime], '15:00', ); expect( LockedBlockOverrideDocumentMapping.fromDocument( overrideResult.document!, ).date, CivilDate(2026, 6, 25), ); }); test('already-current V1 documents are validated without rewrite', () { final migrated = service .migrateTask( fixture('task_v0_minimum.json'), ) .document!; final result = service.migrateTask(migrated); expect(result.isSuccess, isTrue); expect(result.shouldWrite, isFalse); expect(result.report.status, DocumentMigrationStatus.alreadyCurrent); expect(result.report.fromSchemaVersion, 1); expect(result.document, migrated); }); test('repeated migration is idempotent', () { final first = service.migrateTask(fixture('task_v0_full.json')); final second = service.migrateTask(first.document!); expect(first.isSuccess, isTrue); expect(second.isSuccess, isTrue); expect(second.report.status, DocumentMigrationStatus.alreadyCurrent); expect(second.document, first.document); }); test('partial corruption produces a typed failure report before writes', () { final legacy = fixture('task_v0_malformed_missing_title.json'); final result = service.migrateTask(legacy); expect(result.isSuccess, isFalse); expect(result.shouldWrite, isFalse); expect(result.document, isNull); expect(result.report.status, DocumentMigrationStatus.failed); expect( result.report.failureCode, DocumentMigrationFailureCode.legacyDocumentFailure, ); expect(result.report.fieldName, TaskDocumentFields.title); expect(result.report.detailCode, 'missingField'); expect(legacy.containsKey(DocumentFields.schemaVersion), isFalse); }); test('ambiguous legacy date-times fail instead of guessing a zone', () { final result = service.migrateTask( fixture('task_v0_ambiguous_datetime.json'), ); expect(result.isSuccess, isFalse); expect(result.shouldWrite, isFalse); expect(result.document, isNull); expect( result.report.failureCode, DocumentMigrationFailureCode.legacyDocumentFailure, ); expect(result.report.fieldName, TaskDocumentFields.createdAt); expect(result.report.detailCode, 'ambiguousDateTime'); }); test('unsupported future schema versions fail closed', () { final result = service.migrateTask(fixture('task_future_schema.json')); expect(result.isSuccess, isFalse); expect(result.shouldWrite, isFalse); expect(result.document, isNull); expect( result.report.failureCode, DocumentMigrationFailureCode.unsupportedSchemaVersion, ); expect(result.report.fromSchemaVersion, 99); expect(result.report.fieldName, DocumentFields.schemaVersion); }); }); } Map fixture(String name) { final file = File('test/fixtures/migration/$name'); final decoded = jsonDecode(file.readAsStringSync()) as Map; return Map.from(decoded); }