focus-flow/test/domain_contracts_test.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,
);
}