import 'package:adhd_scheduler_core/scheduler_core.dart'; import 'package:test/test.dart'; void main() { group('Application unit of work', () { final now = DateTime.utc(2026, 6, 25, 16); final context = appContext(operationId: 'op-1', now: now); test( 'commits task, activity, aggregate, settings, and operation atomically', () async { final original = task( id: 'task-1', status: TaskStatus.planned, createdAt: now, ); final unitOfWork = InMemoryApplicationUnitOfWork( initialTasks: [original], ); final result = await unitOfWork.run( context: context, operationName: 'complete-task', action: (repositories) async { final loaded = await repositories.tasks.findById(original.id); final completed = loaded!.copyWith( status: TaskStatus.completed, completedAt: now, updatedAt: now, ); final activity = activityFor( id: 'activity-1', operationId: context.operationId, task: completed, occurredAt: now, ); await repositories.tasks.save(completed); await repositories.taskActivities.save(activity); await repositories.projectStatistics.save( ProjectStatistics(projectId: completed.projectId) .recordCompletion(completedAt: now, durationMinutes: 30), ); await repositories.ownerSettings.save( OwnerSettings( ownerId: 'owner-1', timeZoneId: 'America/Los_Angeles'), ); return ApplicationResult.success(completed.id); }, ); expect(result.isSuccess, isTrue); expect(result.requireValue, 'task-1'); expect(unitOfWork.currentTasks.single.status, TaskStatus.completed); expect(unitOfWork.currentTaskActivities.single.id, 'activity-1'); expect( unitOfWork.currentProjectStatistics.single.completedTaskCount, 1, ); expect(unitOfWork.currentOwnerSettings.single.ownerId, 'owner-1'); expect(unitOfWork.currentOperations.single.operationId, 'op-1'); expect( unitOfWork.currentOperations.single.operationName, 'complete-task'); }); test('rolls back staged changes when the action returns a typed failure', () async { final original = task( id: 'task-1', status: TaskStatus.planned, createdAt: now, ); final unitOfWork = InMemoryApplicationUnitOfWork( initialTasks: [original], ); final result = await unitOfWork.run( context: context, operationName: 'complete-task', action: (repositories) async { await repositories.tasks.save( original.copyWith(status: TaskStatus.completed, updatedAt: now), ); await repositories.taskActivities.save( activityFor( id: 'activity-1', operationId: context.operationId, task: original, occurredAt: now, ), ); return ApplicationResult.failure( const ApplicationFailure( code: ApplicationFailureCode.conflict, entityId: 'task-1', ), ); }, ); expect(result.failure!.code, ApplicationFailureCode.conflict); expect(unitOfWork.currentTasks.single.status, TaskStatus.planned); expect(unitOfWork.currentTaskActivities, isEmpty); expect(unitOfWork.currentOperations, isEmpty); }); test('rolls back staged changes when commit persistence fails', () async { final original = task( id: 'task-1', status: TaskStatus.planned, createdAt: now, ); final unitOfWork = InMemoryApplicationUnitOfWork( initialTasks: [original], )..failNextCommitForTesting(); final result = await unitOfWork.run( context: context, operationName: 'complete-task', action: (repositories) async { await repositories.tasks.save( original.copyWith(status: TaskStatus.completed, updatedAt: now), ); return ApplicationResult.success(null); }, ); expect(result.failure!.code, ApplicationFailureCode.persistenceFailure); expect(unitOfWork.currentTasks.single.status, TaskStatus.planned); expect(unitOfWork.currentOperations, isEmpty); }); test('rejects duplicate operation IDs without running the action', () async { final unitOfWork = InMemoryApplicationUnitOfWork(); var runCount = 0; final first = await unitOfWork.run( context: context, operationName: 'capture', action: (_) async { runCount += 1; return ApplicationResult.success('done'); }, ); final duplicate = await unitOfWork.run( context: context, operationName: 'capture', action: (_) async { runCount += 1; return ApplicationResult.success('duplicate'); }, ); expect(first.requireValue, 'done'); expect( duplicate.failure!.code, ApplicationFailureCode.duplicateOperation); expect(duplicate.failure!.entityId, 'op-1'); expect(runCount, 1); expect(unitOfWork.currentOperations, hasLength(1)); }); test('maps domain validation exceptions to stable validation failures', () async { final unitOfWork = InMemoryApplicationUnitOfWork(); final result = await unitOfWork.run( context: context, operationName: 'bad-task', action: (_) async { Task( id: '', title: 'Bad task', projectId: 'inbox', type: TaskType.flexible, status: TaskStatus.backlog, createdAt: now, updatedAt: now, ); return ApplicationResult.success(null); }, ); expect(result.failure!.code, ApplicationFailureCode.validation); expect( result.failure!.detailCode, DomainValidationCode.blankStableId.name); expect(unitOfWork.currentOperations, isEmpty); }); }); group('Application scheduling loader', () { test('centralizes repository loading into scheduler input', () async { final now = DateTime.utc(2026, 6, 25, 8); final inWindow = task( id: 'in-window', status: TaskStatus.planned, createdAt: now, scheduledStart: DateTime.utc(2026, 6, 25, 10), scheduledEnd: DateTime.utc(2026, 6, 25, 10, 30), ); final outsideWindow = task( id: 'outside-window', status: TaskStatus.planned, createdAt: now, scheduledStart: DateTime.utc(2026, 6, 26, 10), scheduledEnd: DateTime.utc(2026, 6, 26, 10, 30), ); final unitOfWork = InMemoryApplicationUnitOfWork( initialTasks: [inWindow, outsideWindow], ); late SchedulingInput input; final result = await unitOfWork.run( context: appContext(operationId: 'load', now: now), operationName: 'load-input', action: (repositories) async { input = await ApplicationSchedulingLoader(repositories) .loadInputForWindow( window: SchedulingWindow( start: DateTime.utc(2026, 6, 25, 0), end: DateTime.utc(2026, 6, 26, 0), ), lockedIntervals: [ TimeInterval( start: DateTime.utc(2026, 6, 25, 12), end: DateTime.utc(2026, 6, 25, 13), ), ], ); return ApplicationResult.success(null); }, ); expect(result.isSuccess, isTrue); expect(input.tasks.map((task) => task.id), ['in-window']); expect(input.lockedIntervals, hasLength(1)); }); }); } ApplicationOperationContext appContext({ required String operationId, required DateTime now, }) { return ApplicationOperationContext.start( operationId: operationId, ownerTimeZone: OwnerTimeZoneContext( ownerId: 'owner-1', timeZoneId: 'America/Los_Angeles', ), clock: FixedClock(now), idGenerator: SequentialIdGenerator(prefix: 'generated'), ); } Task task({ required String id, required TaskStatus status, required DateTime createdAt, DateTime? scheduledStart, DateTime? scheduledEnd, }) { return Task( id: id, title: 'Task $id', projectId: 'inbox', type: TaskType.flexible, status: status, durationMinutes: 30, scheduledStart: scheduledStart, scheduledEnd: scheduledEnd, createdAt: createdAt, updatedAt: createdAt, ); } TaskActivity activityFor({ required String id, required String operationId, required Task task, required DateTime occurredAt, }) { return TaskActivity( id: id, operationId: operationId, code: TaskActivityCode.completed, taskId: task.id, projectId: task.projectId, occurredAt: occurredAt, ); }