126 lines
3.9 KiB
Dart
126 lines
3.9 KiB
Dart
import 'package:adhd_scheduler_core/scheduler_core.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
void main() {
|
|
group('V1 lifecycle contracts', () {
|
|
final now = DateTime(2026, 6, 24, 9);
|
|
const engine = SchedulingEngine();
|
|
|
|
test('durable task statuses exclude movement and V2 burnout labels', () {
|
|
expect(TaskStatus.values, const [
|
|
TaskStatus.planned,
|
|
TaskStatus.active,
|
|
TaskStatus.completed,
|
|
TaskStatus.missed,
|
|
TaskStatus.cancelled,
|
|
TaskStatus.noLongerRelevant,
|
|
TaskStatus.backlog,
|
|
]);
|
|
expect(TaskStatus.values.map((status) => status.name),
|
|
isNot(contains('pushed')));
|
|
expect(
|
|
TaskStatus.values.map((status) => status.name),
|
|
isNot(contains('skipped')),
|
|
);
|
|
});
|
|
|
|
test('manual push preserves durable status and records movement metadata',
|
|
() {
|
|
final task = Task(
|
|
id: 'flexible-1',
|
|
title: 'Clean coffee maker',
|
|
projectId: 'home',
|
|
type: TaskType.flexible,
|
|
status: TaskStatus.planned,
|
|
durationMinutes: 15,
|
|
scheduledStart: DateTime(2026, 6, 24, 9),
|
|
scheduledEnd: DateTime(2026, 6, 24, 9, 15),
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
);
|
|
|
|
final result = engine.pushFlexibleTaskToNextAvailableSlot(
|
|
input: SchedulingInput(
|
|
tasks: [task],
|
|
window: SchedulingWindow(
|
|
start: DateTime(2026, 6, 24, 9),
|
|
end: DateTime(2026, 6, 24, 10),
|
|
),
|
|
),
|
|
taskId: task.id,
|
|
updatedAt: now.add(const Duration(minutes: 1)),
|
|
);
|
|
final pushed = result.tasks.single;
|
|
|
|
expect(pushed.status, TaskStatus.planned);
|
|
expect(pushed.stats.manuallyPushedCount, 1);
|
|
expect(result.changes.single.taskId, task.id);
|
|
expect(result.notices.single.type, SchedulingNoticeType.moved);
|
|
});
|
|
|
|
test('burnout skip remains statistics-compatible without a status', () {
|
|
final task = Task.quickCapture(
|
|
id: 'task-1',
|
|
title: 'Fold laundry',
|
|
createdAt: now,
|
|
).copyWith(
|
|
status: TaskStatus.planned,
|
|
stats: const TaskStatistics().incrementSkippedDuringBurnout(),
|
|
);
|
|
|
|
expect(task.status, TaskStatus.planned);
|
|
expect(task.stats.skippedDuringBurnoutCount, 1);
|
|
expect(TaskStatus.values.map((status) => status.name),
|
|
isNot(contains('skipped')));
|
|
});
|
|
|
|
test(
|
|
'critical missed moves to backlog while inflexible missed stays in place',
|
|
() {
|
|
final critical = _scheduledRequiredTask(
|
|
id: 'critical-1',
|
|
type: TaskType.critical,
|
|
now: now,
|
|
);
|
|
final inflexible = _scheduledRequiredTask(
|
|
id: 'inflexible-1',
|
|
type: TaskType.inflexible,
|
|
now: now,
|
|
);
|
|
|
|
final missedCritical = engine.markMissed(critical, updatedAt: now);
|
|
final missedInflexible = engine.markMissed(inflexible, updatedAt: now);
|
|
|
|
expect(missedCritical.status, TaskStatus.backlog);
|
|
expect(missedCritical.scheduledStart, isNull);
|
|
expect(missedCritical.scheduledEnd, isNull);
|
|
expect(missedCritical.stats.missedCount, 1);
|
|
expect(missedCritical.stats.movedToBacklogCount, 1);
|
|
|
|
expect(missedInflexible.status, TaskStatus.missed);
|
|
expect(missedInflexible.scheduledStart, inflexible.scheduledStart);
|
|
expect(missedInflexible.scheduledEnd, inflexible.scheduledEnd);
|
|
expect(missedInflexible.stats.missedCount, 1);
|
|
expect(missedInflexible.stats.movedToBacklogCount, 0);
|
|
});
|
|
});
|
|
}
|
|
|
|
Task _scheduledRequiredTask({
|
|
required String id,
|
|
required TaskType type,
|
|
required DateTime now,
|
|
}) {
|
|
return Task(
|
|
id: id,
|
|
title: 'Required task',
|
|
projectId: 'home',
|
|
type: type,
|
|
status: TaskStatus.planned,
|
|
durationMinutes: 30,
|
|
scheduledStart: DateTime(2026, 6, 24, 9),
|
|
scheduledEnd: DateTime(2026, 6, 24, 9, 30),
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
);
|
|
}
|