import 'package:scheduler_core/scheduler_core.dart' as core; import 'package:scheduler_persistence/persistence.dart'; import 'package:test/test.dart'; /// Factory set used by adapter packages to run the shared repository contract. final class RepositoryComplianceFactories { const RepositoryComplianceFactories({ required this.taskRepository, required this.projectRepository, required this.lockedBlockRepository, required this.settingsRepository, required this.scheduleSnapshotRepository, }); final TaskRepository Function() taskRepository; final ProjectRepository Function() projectRepository; final LockedBlockRepository Function() lockedBlockRepository; final SettingsRepository Function() settingsRepository; final ScheduleSnapshotRepository Function() scheduleSnapshotRepository; } /// Runs the cross-adapter behavioral checks expected of repository packages. void runRepositoryComplianceTests(RepositoryComplianceFactories factories) { group('task repository compliance', () { test('supports CRUD, owner isolation, and JSON-cloned returns', () async { final repo = factories.taskRepository(); final owner = core.OwnerId('owner-a'); final otherOwner = core.OwnerId('owner-b'); final task = _task('task-a'); final inserted = await repo.insert(ownerId: owner, task: task); expect(inserted.right, core.Revision.initial); final found = (await repo.findById(ownerId: owner, taskId: task.id)).right; expect(found, isNotNull); expect(found, isNot(same(task))); expect(found!.id, task.id); final hiddenFromOtherOwner = (await repo.findById(ownerId: otherOwner, taskId: task.id)).right; expect(hiddenFromOtherOwner, isNull); final updated = task.copyWith( title: 'Updated task', updatedAt: _instant(2), ); final saved = await repo.save( ownerId: owner, task: updated, expectedRevision: inserted.right, ); expect(saved.right, const core.Revision(2)); final afterSave = (await repo.findById(ownerId: owner, taskId: task.id)).right; expect(afterSave!.title, 'Updated task'); final deleted = await repo.delete( ownerId: owner, taskId: task.id, expectedRevision: saved.right, ); expect(deleted.right, const core.Revision(3)); expect( (await repo.findById(ownerId: owner, taskId: task.id)).right, isNull); }); test('enforces duplicate ids and optimistic revision conflicts', () async { final repo = factories.taskRepository(); final owner = core.OwnerId('owner-a'); final otherOwner = core.OwnerId('owner-b'); final task = _task('task-a'); final inserted = await repo.insert(ownerId: owner, task: task); expect( (await repo.insert(ownerId: owner, task: task)).left, isA(), ); expect( (await repo.save( ownerId: owner, task: task.copyWith(title: 'Invalid'), expectedRevision: const core.Revision(0), )) .left, isA(), ); expect( (await repo.save( ownerId: otherOwner, task: task.copyWith(title: 'Other owner'), expectedRevision: inserted.right, )) .left, isA(), ); final saved = await repo.save( ownerId: owner, task: task.copyWith(title: 'Fresh'), expectedRevision: inserted.right, ); final stale = (await repo.save( ownerId: owner, task: task.copyWith(title: 'Stale'), expectedRevision: inserted.right, )) .left; expect(stale, isA()); expect(stale.actualRevision, saved.right); }); test('returns deterministic pages and filtered task queries', () async { final repo = factories.taskRepository(); final owner = core.OwnerId('owner-a'); await repo.insert(ownerId: owner, task: _task('task-c')); await repo.insert( ownerId: owner, task: _task( 'task-a', projectId: 'project-filter', status: core.TaskStatus.completed, ), ); await repo.insert( ownerId: owner, task: _task('task-b', parentTaskId: 'parent-1'), ); final firstPage = (await repo.findByOwner( ownerId: owner, page: const core.PageRequest(limit: 2), )) .right; expect(firstPage.items.map((task) => task.id), ['task-a', 'task-b']); expect(firstPage.nextCursor, isNotNull); final secondPage = (await repo.findByOwner( ownerId: owner, page: core.PageRequest(cursor: firstPage.nextCursor, limit: 2), )) .right; expect(secondPage.items.map((task) => task.id), ['task-c']); expect(secondPage.nextCursor, isNull); expect( (await repo.findByProjectId( ownerId: owner, projectId: 'project-filter', )) .right .items .map((task) => task.id), ['task-a'], ); expect( (await repo.findByParentTaskId( ownerId: owner, parentTaskId: 'parent-1', )) .right .items .map((task) => task.id), ['task-b'], ); expect( (await repo.findByStatus( ownerId: owner, status: core.TaskStatus.completed, )) .right .items .map((task) => task.id), ['task-a'], ); }); }); group('project repository compliance', () { test('supports save, archive filters, and stale conflicts', () async { final repo = factories.projectRepository(); final owner = core.OwnerId('owner-a'); final project = _project('project-a'); final inserted = await repo.insert(ownerId: owner, project: project); expect(inserted.right, core.Revision.initial); expect( (await repo.insert(ownerId: owner, project: project)).left, isA(), ); final found = (await repo.findById(ownerId: owner, projectId: project.id)).right; expect(found, isNot(same(project))); expect(found!.name, project.name); final saved = await repo.save( ownerId: owner, project: project.copyWith(name: 'Updated project'), expectedRevision: inserted.right, ); expect(saved.right, const core.Revision(2)); expect( (await repo.save( ownerId: owner, project: project.copyWith(name: 'Stale project'), expectedRevision: inserted.right, )) .left, isA(), ); final archived = await repo.archive( ownerId: owner, projectId: project.id, expectedRevision: saved.right, archivedAtUtc: _instant(3), ); expect(archived.right, const core.Revision(3)); expect((await repo.findByOwner(ownerId: owner)).right.items, isEmpty); expect( (await repo.findByOwner(ownerId: owner, includeArchived: true)) .right .items .single .isArchived, isTrue, ); }); }); group('locked block repository compliance', () { test('supports block archive and override date lookup', () async { final repo = factories.lockedBlockRepository(); final owner = core.OwnerId('owner-a'); final block = _lockedBlock('block-a'); final override = _override('override-a'); final insertedBlock = await repo.insertBlock(ownerId: owner, block: block); expect(insertedBlock.right, core.Revision.initial); expect( (await repo.insertBlock(ownerId: owner, block: block)).left, isA(), ); final foundBlock = (await repo.findBlockById(ownerId: owner, blockId: block.id)).right; expect(foundBlock, isNot(same(block))); expect(foundBlock!.name, block.name); final savedBlock = await repo.saveBlock( ownerId: owner, block: block.copyWith(name: 'Updated block', updatedAt: _instant(2)), expectedRevision: insertedBlock.right, ); expect(savedBlock.right, const core.Revision(2)); expect( (await repo.saveBlock( ownerId: owner, block: block, expectedRevision: insertedBlock.right, )) .left, isA(), ); final archived = await repo.archiveBlock( ownerId: owner, blockId: block.id, expectedRevision: savedBlock.right, archivedAtUtc: _instant(3), ); expect(archived.right, const core.Revision(3)); expect( (await repo.findBlocksByOwner(ownerId: owner)).right.items, isEmpty); final insertedOverride = await repo.insertOverride(ownerId: owner, override: override); expect(insertedOverride.right, core.Revision.initial); expect( (await repo.findOverridesForDate(ownerId: owner, date: override.date)) .right .items .map((item) => item.id), [override.id], ); final savedOverride = await repo.saveOverride( ownerId: owner, override: _override('override-a', name: 'Replacement block'), expectedRevision: insertedOverride.right, ); expect(savedOverride.right, const core.Revision(2)); }); }); group('settings repository compliance', () { test('supports owner singleton save and stale conflict', () async { final repo = factories.settingsRepository(); final owner = core.OwnerId('owner-a'); final settings = _settings(owner.value); final inserted = await repo.insert(ownerId: owner, settings: settings); expect(inserted.right, core.Revision.initial); expect( (await repo.insert(ownerId: owner, settings: settings)).left, isA(), ); final found = (await repo.findByOwner(ownerId: owner)).right; expect(found, isNot(same(settings))); expect(found!.timeZoneId, 'America/Los_Angeles'); final saved = await repo.save( ownerId: owner, settings: settings.copyWith(compactModeEnabled: true), expectedRevision: inserted.right, ); expect(saved.right, const core.Revision(2)); expect((await repo.findByOwner(ownerId: owner)).right!.compactModeEnabled, isTrue); expect( (await repo.save( ownerId: owner, settings: settings, expectedRevision: inserted.right, )) .left, isA(), ); }); }); group('schedule snapshot repository compliance', () { test('supports window lookup, owner isolation, duplicates, and expiry', () async { final repo = factories.scheduleSnapshotRepository(); final owner = core.OwnerId('owner-a'); final otherOwner = core.OwnerId('owner-b'); final snapshot = _snapshot('snapshot-a'); final inserted = await repo.insert(ownerId: owner, snapshot: snapshot); expect(inserted.right, core.Revision.initial); expect( (await repo.insert(ownerId: owner, snapshot: snapshot)).left, isA(), ); final found = (await repo.findById(ownerId: owner, snapshotId: snapshot.id)).right; expect(found, isNot(same(snapshot))); expect(found!.tasks.single.id, 'task-a'); expect( (await repo.findById(ownerId: otherOwner, snapshotId: snapshot.id)) .right, isNull, ); final overlapping = (await repo.findInWindow( ownerId: owner, window: core.SchedulingWindow( start: _instant(0, hour: 12), end: _instant(0, hour: 13), ), )) .right; expect(overlapping.items.map((item) => item.id), [snapshot.id]); final deleted = await repo.deleteExpired( ownerId: owner, nowUtc: snapshot.capturedAt.add(const Duration(days: 31)), ); expect(deleted.right, 1); expect( (await repo.findById(ownerId: owner, snapshotId: snapshot.id)).right, isNull, ); }); }); } DateTime _instant(int day, {int hour = 9}) { return DateTime.utc(2026, 1, 1 + day, hour); } core.Task _task( String id, { String projectId = 'project-a', String? parentTaskId, core.TaskStatus status = core.TaskStatus.backlog, }) { return core.Task( id: id, title: 'Task $id', projectId: projectId, type: core.TaskType.flexible, status: status, durationMinutes: 30, parentTaskId: parentTaskId, createdAt: _instant(0), updatedAt: _instant(0), ); } core.ProjectProfile _project(String id) { return core.ProjectProfile( id: id, name: 'Project $id', colorKey: 'blue', defaultDurationMinutes: 30, ); } core.LockedBlock _lockedBlock(String id) { return core.LockedBlock( id: id, name: 'Block $id', date: core.CivilDate(2026, 1, 1), startTime: core.WallTime(hour: 10, minute: 0), endTime: core.WallTime(hour: 11, minute: 0), createdAt: _instant(0), updatedAt: _instant(0), ); } core.LockedBlockOverride _override( String id, { String name = 'Added block', }) { return core.LockedBlockOverride.add( id: id, date: core.CivilDate(2026, 1, 2), name: name, startTime: core.WallTime(hour: 14, minute: 0), endTime: core.WallTime(hour: 15, minute: 0), createdAt: _instant(1), ); } core.OwnerSettings _settings(String ownerId) { return core.OwnerSettings( ownerId: ownerId, timeZoneId: 'America/Los_Angeles', dayStart: core.WallTime(hour: 8, minute: 0), dayEnd: core.WallTime(hour: 22, minute: 0), ); } core.SchedulingStateSnapshot _snapshot(String id) { return core.SchedulingStateSnapshot( id: id, capturedAt: _instant(0), window: core.SchedulingWindow( start: _instant(0, hour: 8), end: _instant(0, hour: 17), ), tasks: [_task('task-a')], operationName: 'conformance', ); }