focus-flow/test/reminder_policy_test.dart

320 lines
9.7 KiB
Dart

import 'package:adhd_scheduler_core/scheduler_core.dart';
import 'package:test/test.dart';
void main() {
group('Effective reminder profile resolution', () {
const service = ReminderPolicyService();
final now = DateTime.utc(2026, 6, 25, 9);
test('uses task override before project default and fallback', () {
final project = ProjectProfile(
id: 'home',
name: 'Home',
colorKey: 'green',
defaultReminderProfile: ReminderProfile.gentle,
);
final task = scheduledTask(
id: 'task',
now: now,
reminderOverride: ReminderProfile.strict,
);
final effective = service.resolveEffectiveProfile(
task: task,
project: project,
);
expect(effective.profile, ReminderProfile.strict);
expect(effective.source, EffectiveReminderProfileSource.taskOverride);
});
test('uses project default before application fallback', () {
final project = ProjectProfile(
id: 'home',
name: 'Home',
colorKey: 'green',
defaultReminderProfile: ReminderProfile.persistent,
);
final task = scheduledTask(id: 'task', now: now);
final effective = service.resolveEffectiveProfile(
task: task,
project: project,
);
expect(effective.profile, ReminderProfile.persistent);
expect(effective.source, EffectiveReminderProfileSource.projectDefault);
});
test('uses fallback when no task or project value exists', () {
final task = scheduledTask(id: 'task', now: now);
final effective = service.resolveEffectiveProfile(task: task);
expect(effective.profile, ReminderProfile.gentle);
expect(effective.source, EffectiveReminderProfileSource.fallback);
});
test('does not silently use learned project suggestions', () {
final project = ProjectProfile(
id: 'home',
name: 'Home',
colorKey: 'green',
defaultReminderProfile: ReminderProfile.strict,
);
final statistics = ProjectStatistics(
projectId: 'home',
completedTaskCount: 3,
reminderProfileCounts: const {ReminderProfile.gentle: 3},
);
final suggestionResolution = const ProjectSuggestionService()
.resolveDefaults(project: project, statistics: statistics);
final task = scheduledTask(id: 'task', now: now);
final effective = service.resolveEffectiveProfile(
task: task,
project: suggestionResolution.project,
);
expect(suggestionResolution.suggestions.reminderProfile!.value,
ReminderProfile.gentle);
expect(effective.profile, ReminderProfile.strict);
expect(effective.source, EffectiveReminderProfileSource.projectDefault);
});
});
group('Reminder directives', () {
const service = ReminderPolicyService();
final now = DateTime.utc(2026, 6, 25, 9);
test('all non-silent reminder profiles produce normal flexible delivery',
() {
for (final profile in [
ReminderProfile.gentle,
ReminderProfile.persistent,
ReminderProfile.strict,
]) {
final directive = service.directiveForTask(
task: scheduledTask(
id: 'task-${profile.name}',
now: now,
reminderOverride: profile,
),
now: now,
);
expect(directive.action, ReminderDirectiveAction.deliver);
expect(directive.reason, ReminderDirectiveReason.deliverNormal);
expect(directive.effectiveProfile, profile);
}
});
test('silent profile suppresses normal reminders', () {
final directive = service.directiveForTask(
task: scheduledTask(
id: 'task',
now: now,
reminderOverride: ReminderProfile.silent,
),
now: now,
);
expect(directive.action, ReminderDirectiveAction.suppress);
expect(directive.reason, ReminderDirectiveReason.suppressSilentProfile);
});
test(
'defers before the scheduled window and delivers at the start boundary',
() {
final task = scheduledTask(
id: 'task',
now: now,
start: DateTime.utc(2026, 6, 25, 10),
);
final before = service.directiveForTask(
task: task,
now: DateTime.utc(2026, 6, 25, 9, 59),
);
final atStart = service.directiveForTask(
task: task,
now: DateTime.utc(2026, 6, 25, 10),
);
expect(before.action, ReminderDirectiveAction.defer);
expect(before.reason, ReminderDirectiveReason.deferUntilTaskWindow);
expect(before.nextEligibleAt, DateTime.utc(2026, 6, 25, 10));
expect(atStart.action, ReminderDirectiveAction.deliver);
});
test('suppresses flexible reminders during active protected Free Slots',
() {
final freeSlot = freeSlotTask(
id: 'rest',
start: DateTime.utc(2026, 6, 25, 9),
end: DateTime.utc(2026, 6, 25, 9, 30),
createdAt: now,
);
final flexible = scheduledTask(id: 'flexible', now: now);
final atStart = service.directiveForTask(
task: flexible,
now: DateTime.utc(2026, 6, 25, 9),
contextTasks: [freeSlot, flexible],
);
final inside = service.directiveForTask(
task: flexible,
now: DateTime.utc(2026, 6, 25, 9, 29),
contextTasks: [freeSlot, flexible],
);
final atEnd = service.directiveForTask(
task: flexible,
now: DateTime.utc(2026, 6, 25, 9, 30),
contextTasks: [freeSlot, flexible],
);
expect(atStart.action, ReminderDirectiveAction.suppress);
expect(atStart.reason, ReminderDirectiveReason.suppressProtectedRest);
expect(atStart.protectedFreeSlotTaskId, 'rest');
expect(atStart.nextEligibleAt, DateTime.utc(2026, 6, 25, 9, 30));
expect(inside.action, ReminderDirectiveAction.suppress);
expect(atEnd.action, ReminderDirectiveAction.deliver);
});
test('required reminders may pass protected rest by typed policy', () {
final freeSlot = freeSlotTask(
id: 'rest',
start: DateTime.utc(2026, 6, 25, 9),
end: DateTime.utc(2026, 6, 25, 10),
createdAt: now,
);
final critical = scheduledTask(
id: 'critical',
now: now,
type: TaskType.critical,
reminderOverride: ReminderProfile.strict,
);
final inflexiblePersistent = scheduledTask(
id: 'inflexible-persistent',
now: now,
type: TaskType.inflexible,
reminderOverride: ReminderProfile.persistent,
);
final inflexibleGentle = scheduledTask(
id: 'inflexible-gentle',
now: now,
type: TaskType.inflexible,
reminderOverride: ReminderProfile.gentle,
);
final criticalDirective = service.directiveForTask(
task: critical,
now: now,
contextTasks: [freeSlot, critical],
);
final persistentDirective = service.directiveForTask(
task: inflexiblePersistent,
now: now,
contextTasks: [freeSlot, inflexiblePersistent],
);
final gentleDirective = service.directiveForTask(
task: inflexibleGentle,
now: now,
contextTasks: [freeSlot, inflexibleGentle],
);
expect(
criticalDirective.action,
ReminderDirectiveAction.requireAcknowledgement,
);
expect(
criticalDirective.reason,
ReminderDirectiveReason.requireRequiredAcknowledgement,
);
expect(persistentDirective.action, ReminderDirectiveAction.deliver);
expect(gentleDirective.action, ReminderDirectiveAction.defer);
expect(
gentleDirective.reason,
ReminderDirectiveReason.deferRequiredDuringProtectedRest,
);
expect(gentleDirective.nextEligibleAt, DateTime.utc(2026, 6, 25, 10));
});
test('suppresses locked free-slot completed and unscheduled records', () {
final locked = scheduledTask(
id: 'locked',
now: now,
type: TaskType.locked,
);
final freeSlot = freeSlotTask(
id: 'rest',
start: now,
end: now.add(const Duration(minutes: 30)),
createdAt: now,
);
final completed = scheduledTask(
id: 'completed',
now: now,
status: TaskStatus.completed,
);
final unscheduled = scheduledTask(id: 'unscheduled', now: now)
.copyWith(clearSchedule: true);
expect(
service.directiveForTask(task: locked, now: now).reason,
ReminderDirectiveReason.suppressHiddenLockedTime,
);
expect(
service.directiveForTask(task: freeSlot, now: now).reason,
ReminderDirectiveReason.suppressFreeSlotRecord,
);
expect(
service.directiveForTask(task: completed, now: now).reason,
ReminderDirectiveReason.suppressInactiveStatus,
);
expect(
service.directiveForTask(task: unscheduled, now: now).reason,
ReminderDirectiveReason.suppressNoScheduledWindow,
);
});
});
}
Task scheduledTask({
required String id,
required DateTime now,
TaskType type = TaskType.flexible,
TaskStatus status = TaskStatus.planned,
ReminderProfile? reminderOverride,
DateTime? start,
}) {
final scheduledStart = start ?? now;
return Task(
id: id,
title: 'Task',
projectId: 'home',
type: type,
status: status,
durationMinutes: 30,
scheduledStart: scheduledStart,
scheduledEnd: scheduledStart.add(const Duration(minutes: 30)),
reminderOverride: reminderOverride,
createdAt: now,
updatedAt: now,
);
}
Task freeSlotTask({
required String id,
required DateTime start,
required DateTime end,
required DateTime createdAt,
}) {
return const FreeSlotService().create(
id: id,
title: 'Rest',
start: start,
end: end,
createdAt: createdAt,
);
}