forked from eva/focus-flow
128 lines
4.6 KiB
Dart
128 lines
4.6 KiB
Dart
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
/// Exercises Scheduler Flow integration behavior for the Scheduler Integration Tests package.
|
|
library;
|
|
|
|
import 'package:drift/native.dart';
|
|
import 'package:scheduler_core/scheduler_core.dart' as core;
|
|
import 'package:scheduler_notifications/notifications.dart';
|
|
import 'package:scheduler_persistence_memory/persistence_memory.dart';
|
|
import 'package:scheduler_persistence_sqlite/sqlite.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
/// Entry point for this executable Dart file.
|
|
/// It wires the local command, test, or application behavior needed by this repository without exposing additional public API.
|
|
void main() {
|
|
test('scheduler flow schedules, notifies, persists, and reloads', () async {
|
|
final runtime = Stopwatch()..start();
|
|
addTearDown(() {
|
|
runtime.stop();
|
|
expect(runtime.elapsed, lessThan(const Duration(seconds: 2)));
|
|
});
|
|
final ownerId = core.OwnerId('owner-a');
|
|
final now = DateTime.utc(2026, 6, 26, 8);
|
|
final window = core.SchedulingWindow(
|
|
start: DateTime.utc(2026, 6, 26, 9),
|
|
end: DateTime.utc(2026, 6, 26, 10, 30),
|
|
);
|
|
final lockedInterval = core.TimeInterval(
|
|
start: DateTime.utc(2026, 6, 26, 9),
|
|
end: DateTime.utc(2026, 6, 26, 9, 15),
|
|
label: 'Protected setup time',
|
|
);
|
|
final backlogTask = core.Task(
|
|
id: 'backlog-a',
|
|
title: 'Write planning note',
|
|
projectId: 'project-a',
|
|
type: core.TaskType.flexible,
|
|
status: core.TaskStatus.backlog,
|
|
durationMinutes: 15,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
);
|
|
final plannedTask = core.Task(
|
|
id: 'planned-a',
|
|
title: 'Existing focus block',
|
|
projectId: 'project-a',
|
|
type: core.TaskType.flexible,
|
|
status: core.TaskStatus.planned,
|
|
durationMinutes: 30,
|
|
scheduledStart: DateTime.utc(2026, 6, 26, 9, 30),
|
|
scheduledEnd: DateTime.utc(2026, 6, 26, 10),
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
);
|
|
|
|
final memoryTasks = InMemoryTaskRepository();
|
|
expect(
|
|
(await memoryTasks.insert(ownerId: ownerId, task: backlogTask)).isRight,
|
|
isTrue,
|
|
);
|
|
expect(
|
|
(await memoryTasks.insert(ownerId: ownerId, task: plannedTask)).isRight,
|
|
isTrue,
|
|
);
|
|
|
|
final loaded = await memoryTasks.findByOwner(ownerId: ownerId);
|
|
expect(loaded.isRight, isTrue);
|
|
final result =
|
|
const core.SchedulingEngine().insertBacklogTaskIntoNextAvailableSlot(
|
|
input: core.SchedulingInput(
|
|
tasks: loaded.right.items,
|
|
window: window,
|
|
lockedIntervals: [lockedInterval],
|
|
),
|
|
taskId: backlogTask.id,
|
|
updatedAt: now,
|
|
);
|
|
|
|
expect(result.outcomeCode, core.SchedulingOutcomeCode.success);
|
|
final scheduledBacklog = _taskById(result.tasks, backlogTask.id);
|
|
expect(scheduledBacklog.status, core.TaskStatus.planned);
|
|
expect(scheduledBacklog.scheduledStart, lockedInterval.end);
|
|
expect(
|
|
scheduledBacklog.scheduledEnd,
|
|
DateTime.utc(2026, 6, 26, 9, 30),
|
|
);
|
|
|
|
final notifications = FakeNotificationAdapter();
|
|
addTearDown(notifications.close);
|
|
await notifications.schedule(NotificationRequest(
|
|
id: 'task-${scheduledBacklog.id}-start',
|
|
title: scheduledBacklog.title,
|
|
body: 'Scheduled task is ready.',
|
|
scheduledDateTimeUtc: scheduledBacklog.scheduledStart!,
|
|
payloadJson: '{"taskId":"${scheduledBacklog.id}"}',
|
|
));
|
|
expect(
|
|
notifications.scheduledRequests.keys,
|
|
contains('task-backlog-a-start'),
|
|
);
|
|
|
|
final db = SchedulerDb(NativeDatabase.memory());
|
|
addTearDown(db.close);
|
|
final sqliteTasks = SqliteTaskRepository(db);
|
|
for (final task in result.tasks) {
|
|
final inserted = await sqliteTasks.insert(ownerId: ownerId, task: task);
|
|
expect(inserted.isRight, isTrue);
|
|
}
|
|
|
|
final reloaded = await sqliteTasks.findById(
|
|
ownerId: ownerId,
|
|
taskId: backlogTask.id,
|
|
);
|
|
expect(reloaded.isRight, isTrue);
|
|
final persistedBacklog = reloaded.right;
|
|
expect(persistedBacklog, isNotNull);
|
|
expect(persistedBacklog!.scheduledStart, scheduledBacklog.scheduledStart);
|
|
expect(persistedBacklog.scheduledEnd, scheduledBacklog.scheduledEnd);
|
|
expect(persistedBacklog.status, core.TaskStatus.planned);
|
|
});
|
|
}
|
|
|
|
/// Top-level helper that performs the `_taskById` operation for this file.
|
|
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
|
|
core.Task _taskById(Iterable<core.Task> tasks, String id) {
|
|
return tasks.singleWhere((task) => task.id == id);
|
|
}
|