feat(locked-time): expand recurring blocks for scheduling
This commit is contained in:
parent
c803b53a71
commit
0cb81d2036
3 changed files with 420 additions and 1 deletions
|
|
@ -77,6 +77,8 @@ BREAKPOINT: Stop here. Confirm `high` mode before handling recurrence/override e
|
||||||
|
|
||||||
Recommended Codex level: extra high
|
Recommended Codex level: extra high
|
||||||
|
|
||||||
|
Status: Completed
|
||||||
|
|
||||||
Tasks:
|
Tasks:
|
||||||
|
|
||||||
Implement expansion from recurring locked blocks + overrides into concrete intervals for a day.
|
Implement expansion from recurring locked blocks + overrides into concrete intervals for a day.
|
||||||
|
|
@ -93,6 +95,15 @@ Acceptance criteria:
|
||||||
- Test: one-day override removes/modifies the occurrence.
|
- Test: one-day override removes/modifies the occurrence.
|
||||||
- Test: scheduling engine sees the final locked intervals.
|
- Test: scheduling engine sees the final locked intervals.
|
||||||
|
|
||||||
|
Execution notes:
|
||||||
|
|
||||||
|
- Added an explicit `date` field for one-off locked blocks so non-recurring constraints have a testable calendar anchor.
|
||||||
|
- Added `LockedBlockOccurrence`, `LockedScheduleExpansion`, `expandLockedBlocksForDay`, and `lockedSchedulingIntervalsForDay`.
|
||||||
|
- Expansion resolves weekly recurrence, one-day remove/replace overrides, and added surprise locked time for a target day.
|
||||||
|
- Hidden-by-default remains occurrence metadata only; scheduler-facing intervals are always returned for scheduling.
|
||||||
|
- Added tests for weekday recurrence expansion, one-day remove/replace behavior, surprise locked time expansion, and scheduling engine use of final expanded locked intervals.
|
||||||
|
- `dart format lib test`, `dart analyze`, `dart test`, and `git diff --check` passed.
|
||||||
|
|
||||||
## Chunk 5.4 — Track completed during locked hours
|
## Chunk 5.4 — Track completed during locked hours
|
||||||
|
|
||||||
Recommended Codex level: medium
|
Recommended Codex level: medium
|
||||||
|
|
|
||||||
|
|
@ -53,15 +53,22 @@ class LockedBlock {
|
||||||
required this.endTime,
|
required this.endTime,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
|
this.date,
|
||||||
this.recurrence,
|
this.recurrence,
|
||||||
this.hiddenByDefault = true,
|
this.hiddenByDefault = true,
|
||||||
this.projectId,
|
this.projectId,
|
||||||
});
|
}) : assert(
|
||||||
|
recurrence != null || date != null,
|
||||||
|
'date is required for one-off locked blocks',
|
||||||
|
);
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final ClockTime startTime;
|
final ClockTime startTime;
|
||||||
final ClockTime endTime;
|
final ClockTime endTime;
|
||||||
|
|
||||||
|
/// Calendar date for one-off locked blocks. Recurring blocks leave this null.
|
||||||
|
final DateTime? date;
|
||||||
final LockedBlockRecurrence? recurrence;
|
final LockedBlockRecurrence? recurrence;
|
||||||
final bool hiddenByDefault;
|
final bool hiddenByDefault;
|
||||||
final String? projectId;
|
final String? projectId;
|
||||||
|
|
@ -71,6 +78,34 @@ class LockedBlock {
|
||||||
bool get isRecurring => recurrence != null;
|
bool get isRecurring => recurrence != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Concrete locked-time occurrence for one calendar day.
|
||||||
|
class LockedBlockOccurrence {
|
||||||
|
const LockedBlockOccurrence({
|
||||||
|
required this.name,
|
||||||
|
required this.interval,
|
||||||
|
required this.hiddenByDefault,
|
||||||
|
this.lockedBlockId,
|
||||||
|
this.overrideId,
|
||||||
|
this.projectId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final TimeInterval interval;
|
||||||
|
final bool hiddenByDefault;
|
||||||
|
final String? lockedBlockId;
|
||||||
|
final String? overrideId;
|
||||||
|
final String? projectId;
|
||||||
|
|
||||||
|
/// Scheduler-facing interval. Visibility only affects future UI overlays.
|
||||||
|
TimeInterval get schedulingInterval {
|
||||||
|
return TimeInterval(
|
||||||
|
start: interval.start,
|
||||||
|
end: interval.end,
|
||||||
|
label: name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Type of one-day override applied to locked time.
|
/// Type of one-day override applied to locked time.
|
||||||
enum LockedBlockOverrideType {
|
enum LockedBlockOverrideType {
|
||||||
remove,
|
remove,
|
||||||
|
|
@ -201,6 +236,194 @@ class LockedBlockOverride {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Expands locked blocks and one-day overrides into concrete intervals.
|
||||||
|
class LockedScheduleExpansion {
|
||||||
|
const LockedScheduleExpansion({
|
||||||
|
required this.date,
|
||||||
|
required this.occurrences,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DateTime date;
|
||||||
|
final List<LockedBlockOccurrence> occurrences;
|
||||||
|
|
||||||
|
List<TimeInterval> get schedulingIntervals {
|
||||||
|
return List<TimeInterval>.unmodifiable(
|
||||||
|
occurrences.map((occurrence) => occurrence.schedulingInterval),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns concrete locked-time occurrences for [date].
|
||||||
|
LockedScheduleExpansion expandLockedBlocksForDay({
|
||||||
|
required List<LockedBlock> blocks,
|
||||||
|
required List<LockedBlockOverride> overrides,
|
||||||
|
required DateTime date,
|
||||||
|
}) {
|
||||||
|
final occurrences = <LockedBlockOccurrence>[];
|
||||||
|
final sortedOverrides = [...overrides]..sort((a, b) {
|
||||||
|
final createdComparison = a.createdAt.compareTo(b.createdAt);
|
||||||
|
if (createdComparison != 0) {
|
||||||
|
return createdComparison;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.id.compareTo(b.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (final block in blocks) {
|
||||||
|
if (!_blockOccursOn(block, date)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final blockOverrides = sortedOverrides
|
||||||
|
.where(
|
||||||
|
(override) =>
|
||||||
|
override.appliesTo(blockId: block.id, occurrenceDate: date),
|
||||||
|
)
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
if (blockOverrides.any(
|
||||||
|
(override) => override.type == LockedBlockOverrideType.remove,
|
||||||
|
)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final replacement = _lastReplacementOverride(blockOverrides);
|
||||||
|
|
||||||
|
occurrences.add(
|
||||||
|
replacement == null
|
||||||
|
? _occurrenceFromBlock(block, date)
|
||||||
|
: _occurrenceFromReplacement(
|
||||||
|
block: block,
|
||||||
|
override: replacement,
|
||||||
|
date: date,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final override in sortedOverrides) {
|
||||||
|
if (override.type != LockedBlockOverrideType.add ||
|
||||||
|
!_sameDate(override.date, date)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final occurrence = _occurrenceFromAddedOverride(override, date);
|
||||||
|
if (occurrence != null) {
|
||||||
|
occurrences.add(occurrence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
occurrences.sort((a, b) {
|
||||||
|
final startComparison = a.interval.start.compareTo(b.interval.start);
|
||||||
|
if (startComparison != 0) {
|
||||||
|
return startComparison;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.name.compareTo(b.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return LockedScheduleExpansion(
|
||||||
|
date: DateTime(date.year, date.month, date.day),
|
||||||
|
occurrences: List<LockedBlockOccurrence>.unmodifiable(occurrences),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LockedBlockOverride? _lastReplacementOverride(
|
||||||
|
List<LockedBlockOverride> overrides,
|
||||||
|
) {
|
||||||
|
for (final override in overrides.reversed) {
|
||||||
|
if (override.type == LockedBlockOverrideType.replace) {
|
||||||
|
return override;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TimeInterval> lockedSchedulingIntervalsForDay({
|
||||||
|
required List<LockedBlock> blocks,
|
||||||
|
required List<LockedBlockOverride> overrides,
|
||||||
|
required DateTime date,
|
||||||
|
}) {
|
||||||
|
return expandLockedBlocksForDay(
|
||||||
|
blocks: blocks,
|
||||||
|
overrides: overrides,
|
||||||
|
date: date,
|
||||||
|
).schedulingIntervals;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _blockOccursOn(LockedBlock block, DateTime date) {
|
||||||
|
final recurrence = block.recurrence;
|
||||||
|
if (recurrence != null) {
|
||||||
|
return recurrence.occursOn(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _sameDate(block.date!, date);
|
||||||
|
}
|
||||||
|
|
||||||
|
LockedBlockOccurrence _occurrenceFromBlock(
|
||||||
|
LockedBlock block,
|
||||||
|
DateTime date,
|
||||||
|
) {
|
||||||
|
return LockedBlockOccurrence(
|
||||||
|
lockedBlockId: block.id,
|
||||||
|
name: block.name,
|
||||||
|
interval: TimeInterval(
|
||||||
|
start: block.startTime.onDate(date),
|
||||||
|
end: block.endTime.onDate(date),
|
||||||
|
label: block.name,
|
||||||
|
),
|
||||||
|
hiddenByDefault: block.hiddenByDefault,
|
||||||
|
projectId: block.projectId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LockedBlockOccurrence _occurrenceFromReplacement({
|
||||||
|
required LockedBlock block,
|
||||||
|
required LockedBlockOverride override,
|
||||||
|
required DateTime date,
|
||||||
|
}) {
|
||||||
|
final startTime = override.startTime ?? block.startTime;
|
||||||
|
final endTime = override.endTime ?? block.endTime;
|
||||||
|
final name = override.name ?? block.name;
|
||||||
|
|
||||||
|
return LockedBlockOccurrence(
|
||||||
|
lockedBlockId: block.id,
|
||||||
|
overrideId: override.id,
|
||||||
|
name: name,
|
||||||
|
interval: TimeInterval(
|
||||||
|
start: startTime.onDate(date),
|
||||||
|
end: endTime.onDate(date),
|
||||||
|
label: name,
|
||||||
|
),
|
||||||
|
hiddenByDefault: override.hiddenByDefault,
|
||||||
|
projectId: override.projectId ?? block.projectId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LockedBlockOccurrence? _occurrenceFromAddedOverride(
|
||||||
|
LockedBlockOverride override,
|
||||||
|
DateTime date,
|
||||||
|
) {
|
||||||
|
final interval = override.intervalForDate(date);
|
||||||
|
final name = override.name;
|
||||||
|
|
||||||
|
if (interval == null || name == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return LockedBlockOccurrence(
|
||||||
|
overrideId: override.id,
|
||||||
|
name: name,
|
||||||
|
interval: TimeInterval(
|
||||||
|
start: interval.start,
|
||||||
|
end: interval.end,
|
||||||
|
label: name,
|
||||||
|
),
|
||||||
|
hiddenByDefault: override.hiddenByDefault,
|
||||||
|
projectId: override.projectId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
bool _sameDate(DateTime first, DateTime second) {
|
bool _sameDate(DateTime first, DateTime second) {
|
||||||
return first.year == second.year &&
|
return first.year == second.year &&
|
||||||
first.month == second.month &&
|
first.month == second.month &&
|
||||||
|
|
|
||||||
|
|
@ -411,6 +411,7 @@ void main() {
|
||||||
name: 'Appointment',
|
name: 'Appointment',
|
||||||
startTime: const ClockTime(hour: 13, minute: 0),
|
startTime: const ClockTime(hour: 13, minute: 0),
|
||||||
endTime: const ClockTime(hour: 14, minute: 0),
|
endTime: const ClockTime(hour: 14, minute: 0),
|
||||||
|
date: DateTime(2026, 6, 19),
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
projectId: 'health',
|
projectId: 'health',
|
||||||
|
|
@ -523,6 +524,138 @@ void main() {
|
||||||
expect(interval!.start, DateTime(2026, 6, 19, 15));
|
expect(interval!.start, DateTime(2026, 6, 19, 15));
|
||||||
expect(interval.end, DateTime(2026, 6, 19, 16));
|
expect(interval.end, DateTime(2026, 6, 19, 16));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('locked block expansion returns weekday occurrences', () {
|
||||||
|
final workHours = LockedBlock(
|
||||||
|
id: 'work-hours',
|
||||||
|
name: 'Work',
|
||||||
|
startTime: const ClockTime(hour: 9, minute: 0),
|
||||||
|
endTime: const ClockTime(hour: 17, minute: 0),
|
||||||
|
recurrence: const LockedBlockRecurrence.weekly(
|
||||||
|
weekdays: {
|
||||||
|
LockedWeekday.monday,
|
||||||
|
LockedWeekday.tuesday,
|
||||||
|
LockedWeekday.wednesday,
|
||||||
|
LockedWeekday.thursday,
|
||||||
|
LockedWeekday.friday,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
);
|
||||||
|
|
||||||
|
final fridayExpansion = expandLockedBlocksForDay(
|
||||||
|
blocks: [workHours],
|
||||||
|
overrides: const [],
|
||||||
|
date: DateTime(2026, 6, 19),
|
||||||
|
);
|
||||||
|
final saturdayExpansion = expandLockedBlocksForDay(
|
||||||
|
blocks: [workHours],
|
||||||
|
overrides: const [],
|
||||||
|
date: DateTime(2026, 6, 20),
|
||||||
|
);
|
||||||
|
final occurrence = fridayExpansion.occurrences.single;
|
||||||
|
|
||||||
|
expect(fridayExpansion.date, DateTime(2026, 6, 19));
|
||||||
|
expect(occurrence.lockedBlockId, 'work-hours');
|
||||||
|
expect(occurrence.overrideId, isNull);
|
||||||
|
expect(occurrence.name, 'Work');
|
||||||
|
expect(occurrence.interval.start, DateTime(2026, 6, 19, 9));
|
||||||
|
expect(occurrence.interval.end, DateTime(2026, 6, 19, 17));
|
||||||
|
expect(occurrence.hiddenByDefault, isTrue);
|
||||||
|
expect(occurrence.schedulingInterval.label, 'Work');
|
||||||
|
expect(saturdayExpansion.occurrences, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('locked block expansion applies remove and replace overrides', () {
|
||||||
|
final workHours = LockedBlock(
|
||||||
|
id: 'work-hours',
|
||||||
|
name: 'Work',
|
||||||
|
startTime: const ClockTime(hour: 9, minute: 0),
|
||||||
|
endTime: const ClockTime(hour: 17, minute: 0),
|
||||||
|
recurrence: const LockedBlockRecurrence.weekly(
|
||||||
|
weekdays: {
|
||||||
|
LockedWeekday.monday,
|
||||||
|
LockedWeekday.tuesday,
|
||||||
|
LockedWeekday.wednesday,
|
||||||
|
LockedWeekday.thursday,
|
||||||
|
LockedWeekday.friday,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
hiddenByDefault: true,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
);
|
||||||
|
final fridayOff = LockedBlockOverride.remove(
|
||||||
|
id: 'friday-off',
|
||||||
|
lockedBlockId: 'work-hours',
|
||||||
|
date: DateTime(2026, 6, 19),
|
||||||
|
createdAt: now,
|
||||||
|
);
|
||||||
|
final halfDay = LockedBlockOverride.replace(
|
||||||
|
id: 'half-day',
|
||||||
|
lockedBlockId: 'work-hours',
|
||||||
|
date: DateTime(2026, 6, 22),
|
||||||
|
name: 'Half-day work',
|
||||||
|
startTime: const ClockTime(hour: 9, minute: 0),
|
||||||
|
endTime: const ClockTime(hour: 12, minute: 0),
|
||||||
|
hiddenByDefault: false,
|
||||||
|
createdAt: now.add(const Duration(minutes: 1)),
|
||||||
|
);
|
||||||
|
|
||||||
|
final removed = expandLockedBlocksForDay(
|
||||||
|
blocks: [workHours],
|
||||||
|
overrides: [fridayOff, halfDay],
|
||||||
|
date: DateTime(2026, 6, 19),
|
||||||
|
);
|
||||||
|
final modified = expandLockedBlocksForDay(
|
||||||
|
blocks: [workHours],
|
||||||
|
overrides: [fridayOff, halfDay],
|
||||||
|
date: DateTime(2026, 6, 22),
|
||||||
|
);
|
||||||
|
final normal = expandLockedBlocksForDay(
|
||||||
|
blocks: [workHours],
|
||||||
|
overrides: [fridayOff, halfDay],
|
||||||
|
date: DateTime(2026, 6, 23),
|
||||||
|
);
|
||||||
|
final halfDayOccurrence = modified.occurrences.single;
|
||||||
|
|
||||||
|
expect(removed.occurrences, isEmpty);
|
||||||
|
expect(halfDayOccurrence.lockedBlockId, 'work-hours');
|
||||||
|
expect(halfDayOccurrence.overrideId, 'half-day');
|
||||||
|
expect(halfDayOccurrence.name, 'Half-day work');
|
||||||
|
expect(halfDayOccurrence.interval.start, DateTime(2026, 6, 22, 9));
|
||||||
|
expect(halfDayOccurrence.interval.end, DateTime(2026, 6, 22, 12));
|
||||||
|
expect(halfDayOccurrence.hiddenByDefault, isFalse);
|
||||||
|
expect(normal.occurrences.single.interval.end, DateTime(2026, 6, 23, 17));
|
||||||
|
expect(workHours.recurrence!.occursOn(DateTime(2026, 6, 22)), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('locked block expansion includes surprise locked time overrides', () {
|
||||||
|
final surprise = LockedBlockOverride.add(
|
||||||
|
id: 'surprise-lock',
|
||||||
|
date: DateTime(2026, 6, 19),
|
||||||
|
name: 'Urgent errand',
|
||||||
|
startTime: const ClockTime(hour: 15, minute: 0),
|
||||||
|
endTime: const ClockTime(hour: 16, minute: 0),
|
||||||
|
createdAt: now,
|
||||||
|
projectId: 'home',
|
||||||
|
);
|
||||||
|
|
||||||
|
final expansion = expandLockedBlocksForDay(
|
||||||
|
blocks: const [],
|
||||||
|
overrides: [surprise],
|
||||||
|
date: DateTime(2026, 6, 19),
|
||||||
|
);
|
||||||
|
final occurrence = expansion.occurrences.single;
|
||||||
|
|
||||||
|
expect(occurrence.lockedBlockId, isNull);
|
||||||
|
expect(occurrence.overrideId, 'surprise-lock');
|
||||||
|
expect(occurrence.name, 'Urgent errand');
|
||||||
|
expect(occurrence.projectId, 'home');
|
||||||
|
expect(occurrence.interval.start, DateTime(2026, 6, 19, 15));
|
||||||
|
expect(occurrence.interval.end, DateTime(2026, 6, 19, 16));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('SchedulingEngine starter behavior', () {
|
group('SchedulingEngine starter behavior', () {
|
||||||
|
|
@ -738,6 +871,58 @@ void main() {
|
||||||
expect(inserted.scheduledEnd, DateTime(2026, 6, 19, 13, 35));
|
expect(inserted.scheduledEnd, DateTime(2026, 6, 19, 13, 35));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('insert backlog task respects expanded locked blocks', () {
|
||||||
|
final backlogTask = Task.quickCapture(
|
||||||
|
id: 'backlog-1',
|
||||||
|
title: 'Start laundry',
|
||||||
|
createdAt: now,
|
||||||
|
).copyWith(durationMinutes: 15);
|
||||||
|
final workHours = LockedBlock(
|
||||||
|
id: 'work-hours',
|
||||||
|
name: 'Work',
|
||||||
|
startTime: const ClockTime(hour: 13, minute: 0),
|
||||||
|
endTime: const ClockTime(hour: 13, minute: 20),
|
||||||
|
recurrence: const LockedBlockRecurrence.weekly(
|
||||||
|
weekdays: {LockedWeekday.friday},
|
||||||
|
),
|
||||||
|
hiddenByDefault: true,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
);
|
||||||
|
final replacement = LockedBlockOverride.replace(
|
||||||
|
id: 'longer-work',
|
||||||
|
lockedBlockId: 'work-hours',
|
||||||
|
date: DateTime(2026, 6, 19),
|
||||||
|
startTime: const ClockTime(hour: 13, minute: 0),
|
||||||
|
endTime: const ClockTime(hour: 13, minute: 30),
|
||||||
|
createdAt: now,
|
||||||
|
);
|
||||||
|
final lockedIntervals = lockedSchedulingIntervalsForDay(
|
||||||
|
blocks: [workHours],
|
||||||
|
overrides: [replacement],
|
||||||
|
date: DateTime(2026, 6, 19),
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = engine.insertBacklogTaskIntoNextAvailableSlot(
|
||||||
|
input: SchedulingInput(
|
||||||
|
tasks: [backlogTask],
|
||||||
|
window: SchedulingWindow(
|
||||||
|
start: DateTime(2026, 6, 19, 13),
|
||||||
|
end: DateTime(2026, 6, 19, 14),
|
||||||
|
),
|
||||||
|
lockedIntervals: lockedIntervals,
|
||||||
|
),
|
||||||
|
taskId: 'backlog-1',
|
||||||
|
updatedAt: now,
|
||||||
|
);
|
||||||
|
final inserted = result.tasks.single;
|
||||||
|
|
||||||
|
expect(lockedIntervals.single.start, DateTime(2026, 6, 19, 13));
|
||||||
|
expect(lockedIntervals.single.end, DateTime(2026, 6, 19, 13, 30));
|
||||||
|
expect(inserted.scheduledStart, DateTime(2026, 6, 19, 13, 30));
|
||||||
|
expect(inserted.scheduledEnd, DateTime(2026, 6, 19, 13, 45));
|
||||||
|
});
|
||||||
|
|
||||||
test('insert backlog task does not move inflexible or critical blocks', () {
|
test('insert backlog task does not move inflexible or critical blocks', () {
|
||||||
final backlogTask = Task.quickCapture(
|
final backlogTask = Task.quickCapture(
|
||||||
id: 'backlog-1',
|
id: 'backlog-1',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue