forked from eva/focus-flow
feat(data): prepare MongoDB-friendly repository interfaces
This commit is contained in:
parent
abf069db1f
commit
305090250a
5 changed files with 508 additions and 21 deletions
|
|
@ -1,8 +1,10 @@
|
|||
# V1 Block 09 — Persistence Preparation
|
||||
|
||||
Status: Planned
|
||||
Status: In progress — Chunk 9.1 complete
|
||||
|
||||
Purpose: Prepare the domain layer for local persistence without overbuilding sync or database implementation too early.
|
||||
Purpose: Prepare the domain layer for future MongoDB-backed persistence without adding a MongoDB driver, network behavior, sync behavior, or database runtime dependency too early.
|
||||
|
||||
MongoDB is the committed persistence target. This block should make the core persistence-friendly and MongoDB-document-friendly while keeping the scheduling engine independent from database APIs.
|
||||
|
||||
## Chunk 9.1 — Repository interface
|
||||
|
||||
|
|
@ -19,16 +21,27 @@ Define repository interfaces for:
|
|||
|
||||
Rules:
|
||||
|
||||
- Do not couple scheduling engine directly to SQLite.
|
||||
- Use interfaces that a future Drift/SQLite layer can implement.
|
||||
- Keep current implementation in-memory if needed.
|
||||
- Do not couple the scheduling engine directly to MongoDB APIs.
|
||||
- Use interfaces that a future MongoDB adapter can implement.
|
||||
- Keep the current implementation in-memory if needed.
|
||||
- Repository interfaces should preserve domain rules rather than bypassing services.
|
||||
- Do not introduce alternative database assumptions.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Domain services can depend on interfaces.
|
||||
- Tests can use fake/in-memory repositories.
|
||||
- Interfaces do not import Drift, SQLite, Flutter, or platform-specific APIs.
|
||||
- Interfaces do not import MongoDB, Flutter, platform-specific APIs, or network/client APIs.
|
||||
- Interface names and method shapes are compatible with document-style persistence.
|
||||
|
||||
Completed:
|
||||
|
||||
- Added pure Dart repository interfaces for tasks, projects, locked blocks/overrides, and scheduling state snapshots.
|
||||
- Added in-memory repository implementations for tests and early app wiring.
|
||||
- Added scheduling snapshot state that can rebuild `SchedulingInput` and `SchedulingResult` without persistence APIs.
|
||||
- Exported repository interfaces through the public core library.
|
||||
- Added repository tests covering task/project/locked-block/snapshot fake usage.
|
||||
- Verified with `dart format lib test`, `dart analyze`, and `dart test`.
|
||||
|
||||
BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the persistence edge-case test suite.
|
||||
|
||||
|
|
@ -53,70 +66,87 @@ Cover these cases:
|
|||
- backlog age behavior has a documented source timestamp or TODO before database work
|
||||
- in-memory fake repositories can round-trip saved records without losing scheduling fields
|
||||
- repository operations do not mutate input objects unexpectedly
|
||||
- document-shaped maps preserve all fields needed by future MongoDB persistence
|
||||
- generated document field names are stable and migration-friendly
|
||||
|
||||
Rules:
|
||||
|
||||
- This chunk may add tests and small model helpers needed to make persistence behavior testable.
|
||||
- Do not add Drift/SQLite implementation yet.
|
||||
- Do not add a MongoDB driver, MongoDB client, Atlas setup, connection string, local MongoDB service requirement, or database adapter yet.
|
||||
- Do not add alternative database mappings or non-MongoDB terminology.
|
||||
- Do not add sync, networking, cloud accounts, or background services.
|
||||
- Do not mark this chunk complete unless `dart analyze` and `dart test` pass.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Persistence-sensitive test group exists.
|
||||
- Tests capture stable IDs, enum mapping, nullable-field behavior, and DateTime convention.
|
||||
- Tests capture stable IDs, enum mapping, nullable-field behavior, DateTime convention, and document-shaped mapping expectations.
|
||||
- Any unresolved persistence detail is documented as a TODO in the plan or architecture notes.
|
||||
- No alternative-database references remain in the active persistence plan.
|
||||
|
||||
BREAKPOINT: Stop here. Confirm `high` mode before model serialization changes.
|
||||
|
||||
## Chunk 9.3 — Serialization-safe models
|
||||
## Chunk 9.3 — MongoDB-document-safe models
|
||||
|
||||
Recommended Codex level: high
|
||||
|
||||
Tasks:
|
||||
|
||||
Make models persistence-friendly:
|
||||
Make models persistence-friendly and MongoDB-document-friendly:
|
||||
|
||||
- stable IDs
|
||||
- enum serialization helpers or clear mapping plan
|
||||
- DateTime handling convention
|
||||
- nullable fields documented
|
||||
- nullable field clear helpers where needed
|
||||
- migration-safe field names where possible
|
||||
- map/json-like conversion helpers only if they do not over-couple the domain layer
|
||||
- migration-safe document field names where possible
|
||||
- document-shaped map conversion helpers only if they do not over-couple the domain layer
|
||||
- clear mapping rules for nested task statistics and future child-task ownership fields
|
||||
|
||||
Rules:
|
||||
|
||||
- Keep model helpers independent from MongoDB client libraries.
|
||||
- Prefer plain Dart map/document shapes that a later adapter can translate into MongoDB documents.
|
||||
- Do not introduce database connection logic in this chunk.
|
||||
- Do not add table assumptions, joins, or relational schema requirements.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Models can round-trip through map/json-like structures or have a clear TODO for Drift mappings.
|
||||
- Models can round-trip through document-shaped map/json-like structures or have a clear TODO for MongoDB document mappings.
|
||||
- Tests cover at least task serialization if implemented.
|
||||
- Tests cover enum persistence mapping if implemented.
|
||||
- Tests cover intentional clearing of nullable fields if helper behavior is added.
|
||||
- Tests confirm `not set` reward remains distinct from very low reward through mapping.
|
||||
|
||||
BREAKPOINT: Stop here. Confirm `low` mode before documenting the explicit non-sync boundary.
|
||||
|
||||
## Chunk 9.4 — Explicit non-sync boundary
|
||||
## Chunk 9.4 — Explicit non-sync / no-database-runtime boundary
|
||||
|
||||
Recommended Codex level: low
|
||||
|
||||
Tasks:
|
||||
|
||||
Document that V1 persistence is local-first and sync is out of scope.
|
||||
Document that V1 persistence work prepares for MongoDB but does not implement runtime database access, sync, accounts, or background services.
|
||||
|
||||
Rules:
|
||||
|
||||
- No network sync code.
|
||||
- No cloud assumptions.
|
||||
- No MongoDB connection strings.
|
||||
- No Atlas setup.
|
||||
- No local MongoDB server requirement.
|
||||
- No background service implementation.
|
||||
- No mobile notification or background reconciliation implementation in this block.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- README or architecture doc states sync is future work.
|
||||
- Docs distinguish local persistence preparation from actual sync.
|
||||
- README or architecture doc states MongoDB is the committed persistence target.
|
||||
- README or architecture doc states this block does not require a running MongoDB instance.
|
||||
- Docs distinguish MongoDB persistence preparation from actual sync or database adapter implementation.
|
||||
- Wishlist/future notes contain sync as a later feature, not a V1 requirement.
|
||||
|
||||
Commit suggestion:
|
||||
|
||||
```text
|
||||
feat(data): prepare persistence interfaces for scheduling core
|
||||
feat(data): prepare MongoDB-friendly persistence interfaces
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# V1 Block 09 — Persistence Preparation
|
||||
|
||||
Status: Planned
|
||||
Status: In progress — Chunk 9.1 complete
|
||||
|
||||
Purpose: Prepare the domain layer for future MongoDB-backed persistence without adding a MongoDB driver, network behavior, sync behavior, or database runtime dependency too early.
|
||||
|
||||
|
|
@ -34,6 +34,15 @@ Acceptance criteria:
|
|||
- Interfaces do not import MongoDB, Flutter, platform-specific APIs, or network/client APIs.
|
||||
- Interface names and method shapes are compatible with document-style persistence.
|
||||
|
||||
Completed:
|
||||
|
||||
- Added pure Dart repository interfaces for tasks, projects, locked blocks/overrides, and scheduling state snapshots.
|
||||
- Added in-memory repository implementations for tests and early app wiring.
|
||||
- Added scheduling snapshot state that can rebuild `SchedulingInput` and `SchedulingResult` without persistence APIs.
|
||||
- Exported repository interfaces through the public core library.
|
||||
- Added repository tests covering task/project/locked-block/snapshot fake usage.
|
||||
- Verified with `dart format lib test`, `dart analyze`, and `dart test`.
|
||||
|
||||
BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the persistence edge-case test suite.
|
||||
|
||||
## Chunk 9.2 — Persistence edge-case regression test suite
|
||||
|
|
@ -64,7 +73,7 @@ Rules:
|
|||
|
||||
- This chunk may add tests and small model helpers needed to make persistence behavior testable.
|
||||
- Do not add a MongoDB driver, MongoDB client, Atlas setup, connection string, local MongoDB service requirement, or database adapter yet.
|
||||
- Do not add alternative database mappings or SQL-specific terminology.
|
||||
- Do not add alternative database mappings or non-MongoDB terminology.
|
||||
- Do not add sync, networking, cloud accounts, or background services.
|
||||
- Do not mark this chunk complete unless `dart analyze` and `dart test` pass.
|
||||
|
||||
|
|
@ -99,7 +108,7 @@ Rules:
|
|||
- Keep model helpers independent from MongoDB client libraries.
|
||||
- Prefer plain Dart map/document shapes that a later adapter can translate into MongoDB documents.
|
||||
- Do not introduce database connection logic in this chunk.
|
||||
- Do not add SQL-style table assumptions, joins, or relational schema requirements.
|
||||
- Do not add table assumptions, joins, or relational schema requirements.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export 'src/backlog.dart';
|
|||
export 'src/child_tasks.dart';
|
||||
export 'src/locked_time.dart';
|
||||
export 'src/quick_capture.dart';
|
||||
export 'src/repositories.dart';
|
||||
export 'src/scheduling_engine.dart';
|
||||
export 'src/task_actions.dart';
|
||||
export 'src/task_statistics.dart';
|
||||
|
|
|
|||
300
lib/src/repositories.dart
Normal file
300
lib/src/repositories.dart
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
// Persistence boundary interfaces for the scheduling core.
|
||||
//
|
||||
// These interfaces are intentionally database-client-free. A future MongoDB
|
||||
// adapter can implement them with document storage while the domain layer and
|
||||
// tests keep using the same pure Dart contracts.
|
||||
|
||||
import 'locked_time.dart';
|
||||
import 'models.dart';
|
||||
import 'scheduling_engine.dart';
|
||||
|
||||
/// Repository contract for task documents.
|
||||
abstract interface class TaskRepository {
|
||||
/// Return a task by stable id, or null when it does not exist.
|
||||
Future<Task?> findById(String id);
|
||||
|
||||
/// Return all tasks currently known to the repository.
|
||||
Future<List<Task>> findAll();
|
||||
|
||||
/// Return tasks with a lifecycle status.
|
||||
Future<List<Task>> findByStatus(TaskStatus status);
|
||||
|
||||
/// Return tasks that overlap [window] by scheduled start/end.
|
||||
Future<List<Task>> findScheduledInWindow(SchedulingWindow window);
|
||||
|
||||
/// Save or replace [task] by stable id.
|
||||
Future<void> save(Task task);
|
||||
|
||||
/// Save or replace every task by stable id.
|
||||
Future<void> saveAll(Iterable<Task> tasks);
|
||||
}
|
||||
|
||||
/// Repository contract for project profile documents.
|
||||
abstract interface class ProjectRepository {
|
||||
/// Return a project by stable id, or null when it does not exist.
|
||||
Future<ProjectProfile?> findById(String id);
|
||||
|
||||
/// Return all projects currently known to the repository.
|
||||
Future<List<ProjectProfile>> findAll();
|
||||
|
||||
/// Save or replace [project] by stable id.
|
||||
Future<void> save(ProjectProfile project);
|
||||
}
|
||||
|
||||
/// Repository contract for locked-block and one-day override documents.
|
||||
abstract interface class LockedBlockRepository {
|
||||
/// Return a locked block by stable id, or null when it does not exist.
|
||||
Future<LockedBlock?> findBlockById(String id);
|
||||
|
||||
/// Return all locked block definitions.
|
||||
Future<List<LockedBlock>> findAllBlocks();
|
||||
|
||||
/// Return an override by stable id, or null when it does not exist.
|
||||
Future<LockedBlockOverride?> findOverrideById(String id);
|
||||
|
||||
/// Return all one-day overrides.
|
||||
Future<List<LockedBlockOverride>> findAllOverrides();
|
||||
|
||||
/// Save or replace [block] by stable id.
|
||||
Future<void> saveBlock(LockedBlock block);
|
||||
|
||||
/// Save or replace [override] by stable id.
|
||||
Future<void> saveOverride(LockedBlockOverride override);
|
||||
}
|
||||
|
||||
/// Snapshot of one scheduling operation or persisted planning state.
|
||||
class SchedulingStateSnapshot {
|
||||
const SchedulingStateSnapshot({
|
||||
required this.id,
|
||||
required this.capturedAt,
|
||||
required this.window,
|
||||
required this.tasks,
|
||||
this.lockedIntervals = const <TimeInterval>[],
|
||||
this.requiredVisibleIntervals = const <TimeInterval>[],
|
||||
this.notices = const <SchedulingNotice>[],
|
||||
this.changes = const <SchedulingChange>[],
|
||||
this.overlaps = const <SchedulingOverlap>[],
|
||||
this.operationName,
|
||||
});
|
||||
|
||||
/// Stable snapshot id.
|
||||
final String id;
|
||||
|
||||
/// Time this snapshot was created.
|
||||
final DateTime capturedAt;
|
||||
|
||||
/// Planning window represented by the snapshot.
|
||||
final SchedulingWindow window;
|
||||
|
||||
/// Task documents visible to the operation.
|
||||
final List<Task> tasks;
|
||||
|
||||
/// Locked intervals supplied to the scheduler.
|
||||
final List<TimeInterval> lockedIntervals;
|
||||
|
||||
/// Extra visible required intervals supplied to the scheduler.
|
||||
final List<TimeInterval> requiredVisibleIntervals;
|
||||
|
||||
/// Human-readable scheduling notices.
|
||||
final List<SchedulingNotice> notices;
|
||||
|
||||
/// Machine-readable task movements.
|
||||
final List<SchedulingChange> changes;
|
||||
|
||||
/// Analysis-only overlap findings.
|
||||
final List<SchedulingOverlap> overlaps;
|
||||
|
||||
/// Optional operation label, such as `push` or `rollover`.
|
||||
final String? operationName;
|
||||
|
||||
/// Rebuild scheduler input from the persisted state shape.
|
||||
SchedulingInput get input {
|
||||
return SchedulingInput(
|
||||
tasks: List<Task>.unmodifiable(tasks),
|
||||
window: window,
|
||||
lockedIntervals: List<TimeInterval>.unmodifiable(lockedIntervals),
|
||||
requiredVisibleIntervals:
|
||||
List<TimeInterval>.unmodifiable(requiredVisibleIntervals),
|
||||
);
|
||||
}
|
||||
|
||||
/// Rebuild scheduler result details from the persisted state shape.
|
||||
SchedulingResult get result {
|
||||
return SchedulingResult(
|
||||
tasks: List<Task>.unmodifiable(tasks),
|
||||
notices: List<SchedulingNotice>.unmodifiable(notices),
|
||||
changes: List<SchedulingChange>.unmodifiable(changes),
|
||||
overlaps: List<SchedulingOverlap>.unmodifiable(overlaps),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Repository contract for scheduling operation/state snapshot documents.
|
||||
abstract interface class SchedulingSnapshotRepository {
|
||||
/// Return a snapshot by stable id, or null when it does not exist.
|
||||
Future<SchedulingStateSnapshot?> findById(String id);
|
||||
|
||||
/// Return all snapshots overlapping [window].
|
||||
Future<List<SchedulingStateSnapshot>> findInWindow(SchedulingWindow window);
|
||||
|
||||
/// Save or replace [snapshot] by stable id.
|
||||
Future<void> save(SchedulingStateSnapshot snapshot);
|
||||
}
|
||||
|
||||
/// In-memory task repository useful for tests and early application wiring.
|
||||
class InMemoryTaskRepository implements TaskRepository {
|
||||
InMemoryTaskRepository([Iterable<Task> initialTasks = const []])
|
||||
: _tasksById = {
|
||||
for (final task in initialTasks) task.id: task,
|
||||
};
|
||||
|
||||
final Map<String, Task> _tasksById;
|
||||
|
||||
@override
|
||||
Future<Task?> findById(String id) async {
|
||||
return _tasksById[id];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Task>> findAll() async {
|
||||
return List<Task>.unmodifiable(_tasksById.values);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Task>> findByStatus(TaskStatus status) async {
|
||||
return List<Task>.unmodifiable(
|
||||
_tasksById.values.where((task) => task.status == status),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Task>> findScheduledInWindow(SchedulingWindow window) async {
|
||||
return List<Task>.unmodifiable(
|
||||
_tasksById.values.where((task) {
|
||||
final start = task.scheduledStart;
|
||||
final end = task.scheduledEnd;
|
||||
if (start == null || end == null) {
|
||||
return false;
|
||||
}
|
||||
return TimeInterval(start: start, end: end).overlaps(window.interval);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(Task task) async {
|
||||
_tasksById[task.id] = task;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveAll(Iterable<Task> tasks) async {
|
||||
for (final task in tasks) {
|
||||
_tasksById[task.id] = task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// In-memory project repository useful for tests and early application wiring.
|
||||
class InMemoryProjectRepository implements ProjectRepository {
|
||||
InMemoryProjectRepository(
|
||||
[Iterable<ProjectProfile> initialProjects = const []])
|
||||
: _projectsById = {
|
||||
for (final project in initialProjects) project.id: project,
|
||||
};
|
||||
|
||||
final Map<String, ProjectProfile> _projectsById;
|
||||
|
||||
@override
|
||||
Future<ProjectProfile?> findById(String id) async {
|
||||
return _projectsById[id];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ProjectProfile>> findAll() async {
|
||||
return List<ProjectProfile>.unmodifiable(_projectsById.values);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(ProjectProfile project) async {
|
||||
_projectsById[project.id] = project;
|
||||
}
|
||||
}
|
||||
|
||||
/// In-memory locked-time repository useful for tests and early wiring.
|
||||
class InMemoryLockedBlockRepository implements LockedBlockRepository {
|
||||
InMemoryLockedBlockRepository({
|
||||
Iterable<LockedBlock> initialBlocks = const [],
|
||||
Iterable<LockedBlockOverride> initialOverrides = const [],
|
||||
}) : _blocksById = {
|
||||
for (final block in initialBlocks) block.id: block,
|
||||
},
|
||||
_overridesById = {
|
||||
for (final override in initialOverrides) override.id: override,
|
||||
};
|
||||
|
||||
final Map<String, LockedBlock> _blocksById;
|
||||
final Map<String, LockedBlockOverride> _overridesById;
|
||||
|
||||
@override
|
||||
Future<LockedBlock?> findBlockById(String id) async {
|
||||
return _blocksById[id];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<LockedBlock>> findAllBlocks() async {
|
||||
return List<LockedBlock>.unmodifiable(_blocksById.values);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LockedBlockOverride?> findOverrideById(String id) async {
|
||||
return _overridesById[id];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<LockedBlockOverride>> findAllOverrides() async {
|
||||
return List<LockedBlockOverride>.unmodifiable(_overridesById.values);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveBlock(LockedBlock block) async {
|
||||
_blocksById[block.id] = block;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveOverride(LockedBlockOverride override) async {
|
||||
_overridesById[override.id] = override;
|
||||
}
|
||||
}
|
||||
|
||||
/// In-memory snapshot repository useful for tests and early application wiring.
|
||||
class InMemorySchedulingSnapshotRepository
|
||||
implements SchedulingSnapshotRepository {
|
||||
InMemorySchedulingSnapshotRepository([
|
||||
Iterable<SchedulingStateSnapshot> initialSnapshots = const [],
|
||||
]) : _snapshotsById = {
|
||||
for (final snapshot in initialSnapshots) snapshot.id: snapshot,
|
||||
};
|
||||
|
||||
final Map<String, SchedulingStateSnapshot> _snapshotsById;
|
||||
|
||||
@override
|
||||
Future<SchedulingStateSnapshot?> findById(String id) async {
|
||||
return _snapshotsById[id];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<SchedulingStateSnapshot>> findInWindow(
|
||||
SchedulingWindow window,
|
||||
) async {
|
||||
return List<SchedulingStateSnapshot>.unmodifiable(
|
||||
_snapshotsById.values.where(
|
||||
(snapshot) => snapshot.window.interval.overlaps(window.interval),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(SchedulingStateSnapshot snapshot) async {
|
||||
_snapshotsById[snapshot.id] = snapshot;
|
||||
}
|
||||
}
|
||||
147
test/repositories_test.dart
Normal file
147
test/repositories_test.dart
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import 'package:adhd_scheduler_core/scheduler_core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('Repository interfaces', () {
|
||||
final createdAt = DateTime(2026, 6, 21, 8);
|
||||
final window = SchedulingWindow(
|
||||
start: DateTime(2026, 6, 21, 9),
|
||||
end: DateTime(2026, 6, 21, 17),
|
||||
);
|
||||
|
||||
test('task repository supports in-memory fakes for domain services',
|
||||
() async {
|
||||
final backlogTask = task(
|
||||
id: 'backlog',
|
||||
title: 'Backlog task',
|
||||
status: TaskStatus.backlog,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
final scheduledTask = task(
|
||||
id: 'scheduled',
|
||||
title: 'Scheduled task',
|
||||
status: TaskStatus.planned,
|
||||
createdAt: createdAt,
|
||||
scheduledStart: DateTime(2026, 6, 21, 10),
|
||||
scheduledEnd: DateTime(2026, 6, 21, 10, 30),
|
||||
);
|
||||
final repository = InMemoryTaskRepository([backlogTask]);
|
||||
|
||||
await repository.save(scheduledTask);
|
||||
|
||||
expect(await repository.findById('backlog'), same(backlogTask));
|
||||
expect(await repository.findByStatus(TaskStatus.backlog), [backlogTask]);
|
||||
expect(
|
||||
(await repository.findScheduledInWindow(window)).map((task) => task.id),
|
||||
['scheduled'],
|
||||
);
|
||||
});
|
||||
|
||||
test('project repository stores project profiles by stable id', () async {
|
||||
const project = ProjectProfile(
|
||||
id: 'home',
|
||||
name: 'Home',
|
||||
colorKey: 'project-home',
|
||||
);
|
||||
final repository = InMemoryProjectRepository();
|
||||
|
||||
await repository.save(project);
|
||||
|
||||
expect(await repository.findById('home'), same(project));
|
||||
expect(await repository.findAll(), [project]);
|
||||
});
|
||||
|
||||
test('locked block repository separates blocks from overrides', () async {
|
||||
final block = LockedBlock(
|
||||
id: 'work',
|
||||
name: 'Work',
|
||||
startTime: const ClockTime(hour: 9, minute: 0),
|
||||
endTime: const ClockTime(hour: 17, minute: 0),
|
||||
recurrence: const LockedBlockRecurrence.weekly(
|
||||
weekdays: {LockedWeekday.monday},
|
||||
),
|
||||
createdAt: createdAt,
|
||||
updatedAt: createdAt,
|
||||
);
|
||||
final override = LockedBlockOverride.remove(
|
||||
id: 'holiday',
|
||||
lockedBlockId: block.id,
|
||||
date: DateTime(2026, 6, 22),
|
||||
createdAt: createdAt,
|
||||
);
|
||||
final repository = InMemoryLockedBlockRepository();
|
||||
|
||||
await repository.saveBlock(block);
|
||||
await repository.saveOverride(override);
|
||||
|
||||
expect(await repository.findBlockById('work'), same(block));
|
||||
expect(await repository.findOverrideById('holiday'), same(override));
|
||||
expect(await repository.findAllBlocks(), [block]);
|
||||
expect(await repository.findAllOverrides(), [override]);
|
||||
});
|
||||
|
||||
test('scheduling snapshot repository preserves scheduler input shape',
|
||||
() async {
|
||||
final scheduledTask = task(
|
||||
id: 'scheduled',
|
||||
title: 'Scheduled task',
|
||||
status: TaskStatus.planned,
|
||||
createdAt: createdAt,
|
||||
scheduledStart: DateTime(2026, 6, 21, 10),
|
||||
scheduledEnd: DateTime(2026, 6, 21, 10, 30),
|
||||
);
|
||||
final lockedInterval = TimeInterval(
|
||||
start: DateTime(2026, 6, 21, 12),
|
||||
end: DateTime(2026, 6, 21, 13),
|
||||
label: 'Lunch',
|
||||
);
|
||||
final snapshot = SchedulingStateSnapshot(
|
||||
id: 'snapshot-1',
|
||||
capturedAt: DateTime(2026, 6, 21, 8, 30),
|
||||
window: window,
|
||||
tasks: [scheduledTask],
|
||||
lockedIntervals: [lockedInterval],
|
||||
notices: const [
|
||||
SchedulingNotice('Saved schedule'),
|
||||
],
|
||||
operationName: 'load-day',
|
||||
);
|
||||
final repository = InMemorySchedulingSnapshotRepository();
|
||||
|
||||
await repository.save(snapshot);
|
||||
|
||||
final saved = await repository.findById('snapshot-1');
|
||||
expect(saved, same(snapshot));
|
||||
expect(saved!.input.tasks, [scheduledTask]);
|
||||
expect(saved.input.lockedIntervals, [lockedInterval]);
|
||||
expect(saved.result.tasks, [scheduledTask]);
|
||||
expect(saved.result.notices.single.message, 'Saved schedule');
|
||||
expect(
|
||||
(await repository.findInWindow(window)).map((snapshot) => snapshot.id),
|
||||
['snapshot-1'],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Task task({
|
||||
required String id,
|
||||
required String title,
|
||||
required TaskStatus status,
|
||||
required DateTime createdAt,
|
||||
DateTime? scheduledStart,
|
||||
DateTime? scheduledEnd,
|
||||
}) {
|
||||
return Task(
|
||||
id: id,
|
||||
title: title,
|
||||
projectId: 'inbox',
|
||||
type: TaskType.flexible,
|
||||
status: status,
|
||||
durationMinutes: 30,
|
||||
scheduledStart: scheduledStart,
|
||||
scheduledEnd: scheduledEnd,
|
||||
createdAt: createdAt,
|
||||
updatedAt: createdAt,
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue