feat(data): add v1 document codecs
This commit is contained in:
parent
dcd049c549
commit
3dc73e8d43
6 changed files with 2694 additions and 382 deletions
|
|
@ -83,6 +83,8 @@ and migrations.
|
|||
|
||||
Recommended Codex level: extra high
|
||||
|
||||
Status: Complete
|
||||
|
||||
Tasks:
|
||||
|
||||
- Implement plain-Dart map codecs for every V1 persisted entity from Chunk 15.1.
|
||||
|
|
@ -117,6 +119,41 @@ Acceptance criteria:
|
|||
- Nullable fields can be preserved and explicitly cleared.
|
||||
- All codec tests pass without a database runtime.
|
||||
|
||||
Completed implementation:
|
||||
|
||||
- Replaced the partial document mapper with V1 map codecs for every Chunk 15.1
|
||||
persisted entity: tasks, task statistics, project profiles, project
|
||||
statistics, locked recurrence, locked blocks, locked overrides, task
|
||||
activities, owner settings, backlog staleness settings, notice
|
||||
acknowledgements, operation records, scheduling snapshots, windows,
|
||||
intervals, notices, changes, and overlaps.
|
||||
- Added explicit stable encode/decode tables for all persisted enum/code values
|
||||
instead of relying on Dart enum `.name`.
|
||||
- Added `DocumentMappingFailureCode`, `DocumentMappingException`, and
|
||||
`DocumentMetadata` so malformed V1 documents fail with typed mapping
|
||||
categories.
|
||||
- Added common V1 metadata validation for `schemaVersion`, `_id`, `ownerId`,
|
||||
positive `revision`, `createdAt`, and `updatedAt`.
|
||||
- Expanded persistence field-name contracts to include common metadata, archive
|
||||
fields, owner settings, notice acknowledgement, operation record, scheduling
|
||||
snapshot, notice-code, and retention fields.
|
||||
- Preserved null/clear behavior for optional fields and validated required
|
||||
nullable V1 fields even when the current domain object does not carry that
|
||||
metadata directly.
|
||||
- Added forward-compatible unknown-field tolerance by ignoring extra document
|
||||
fields during decoding.
|
||||
- Added exhaustive codec tests for full and nullable task documents, configured
|
||||
projects, project statistics with applied activity IDs, locked time civil
|
||||
dates/wall times, settings, activities, acknowledgements, operation records,
|
||||
scheduling snapshots, stable code tables, and malformed document failures.
|
||||
|
||||
Verification:
|
||||
|
||||
- `dart format lib test`: passed, 0 files changed after final format
|
||||
- `dart analyze`: passed, no issues found
|
||||
- `dart test`: passed, 281 tests
|
||||
- `git diff --check`: passed
|
||||
|
||||
## Chunk 15.3 — Legacy schema migration and fixture suite
|
||||
|
||||
Recommended Codex level: extra high
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ following source files:
|
|||
- `src/models.dart`
|
||||
- `src/application_commands.dart`
|
||||
- `src/application_layer.dart`
|
||||
- `src/application_management.dart`
|
||||
- `src/application_recovery.dart`
|
||||
- `src/backlog.dart`
|
||||
- `src/child_tasks.dart`
|
||||
- `src/document_mapping.dart`
|
||||
|
|
@ -43,7 +45,10 @@ changes from accidental API drift.
|
|||
| File | Enums |
|
||||
|---|---|
|
||||
| `src/application_commands.dart` | `ApplicationCommandCode` |
|
||||
| `src/application_management.dart` | `ApplicationManagementCommandCode`, `OwnerSettingsWarningCode` |
|
||||
| `src/application_recovery.dart` | `AppOpenRecoveryOutcome` |
|
||||
| `src/application_layer.dart` | `ApplicationFailureCode` |
|
||||
| `src/document_mapping.dart` | `DocumentMappingFailureCode` |
|
||||
| `src/models.dart` | `DomainValidationCode`, `TaskType`, `TaskStatus`, `PriorityLevel`, `RewardLevel`, `DifficultyLevel`, `ReminderProfile`, `BacklogTag` |
|
||||
| `src/backlog.dart` | `BacklogFilter`, `BacklogSortKey`, `BacklogStalenessMarker` |
|
||||
| `src/free_slots.dart` | `RequiredCommitmentScheduleStatus` |
|
||||
|
|
@ -64,15 +69,17 @@ changes from accidental API drift.
|
|||
| File | Public API |
|
||||
|---|---|
|
||||
| `src/application_commands.dart` | `ApplicationChildTaskDraft`, `ApplicationCommandReadHint`, `ApplicationCommandResult`, `V1ApplicationCommandUseCases` |
|
||||
| `src/application_layer.dart` | `OwnerTimeZoneContext`, `ApplicationOperationContext`, `ApplicationFailure`, `ApplicationResult`, `ApplicationFailureException`, `ApplicationPersistenceException`, `ApplicationOperationRecord`, `OwnerSettings`, `TaskActivityRepository`, `ProjectStatisticsRepository`, `OwnerSettingsRepository`, `ApplicationOperationRepository`, `ApplicationUnitOfWorkRepositories`, `ApplicationUnitOfWork`, `ApplicationSchedulingLoader`, `InMemoryApplicationUnitOfWork` |
|
||||
| `src/application_layer.dart` | `OwnerTimeZoneContext`, `ApplicationOperationContext`, `ApplicationFailure`, `ApplicationResult`, `ApplicationFailureException`, `ApplicationPersistenceException`, `ApplicationOperationRecord`, `NoticeAcknowledgementRecord`, `OwnerSettings`, `TaskActivityRepository`, `ProjectStatisticsRepository`, `OwnerSettingsRepository`, `NoticeAcknowledgementRepository`, `ApplicationOperationRepository`, `ApplicationUnitOfWorkRepositories`, `ApplicationUnitOfWork`, `ApplicationSchedulingLoader`, `InMemoryApplicationUnitOfWork` |
|
||||
| `src/application_management.dart` | `GetBacklogRequest`, `BacklogItemReadModel`, `BacklogQueryResult`, `GetProjectDefaultsRequest`, `ProjectDefaultsQueryResult`, `ProjectProfileDraft`, `ProjectProfileUpdate`, `LockedBlockDraft`, `LockedBlockUpdate`, `OwnerSettingsUpdateResult`, `OwnerSettingsUpdate`, `NoticeAcknowledgementResult`, `V1ApplicationManagementUseCases` |
|
||||
| `src/application_recovery.dart` | `RunAppOpenRecoveryRequest`, `AppOpenRecoveryResult`, `V1AppOpenRecoveryUseCases` |
|
||||
| `src/models.dart` | `DomainValidationException`, `Task`, `ProjectProfile`, `TimeInterval` |
|
||||
| `src/backlog.dart` | `BacklogStalenessSettings`, `BacklogView` |
|
||||
| `src/child_tasks.dart` | `ChildTaskEntry`, `ChildTaskBreakUpRequest`, `ChildTaskBreakUpResult`, `ChildTaskBreakUpService`, `ChildTaskView`, `ChildTaskCompletionResult`, `ChildTaskCompletionService`, `ChildTaskSummary` |
|
||||
| `src/document_mapping.dart` | `TaskDocumentExtension`, `TaskStatisticsDocumentExtension`, `ProjectStatisticsDocumentExtension` |
|
||||
| `src/document_mapping.dart` | `DocumentMappingException`, `DocumentMetadata`, `PersistenceEnumMapping`, `TaskDocumentMapping`, `TaskStatisticsDocumentMapping`, `ProjectDocumentMapping`, `ProjectStatisticsDocumentMapping`, `LockedBlockRecurrenceDocumentMapping`, `LockedBlockDocumentMapping`, `LockedBlockOverrideDocumentMapping`, `TaskActivityDocumentMapping`, `OwnerSettingsDocumentMapping`, `BacklogStalenessDocumentMapping`, `NoticeAcknowledgementDocumentMapping`, `ApplicationOperationDocumentMapping`, `SchedulingSnapshotDocumentMapping`, `SchedulingWindowDocumentMapping`, `TimeIntervalDocumentMapping`, `SchedulingNoticeDocumentMapping`, `SchedulingChangeDocumentMapping`, `SchedulingOverlapDocumentMapping`, `TaskDocumentExtension`, `TaskStatisticsDocumentExtension`, `ProjectStatisticsDocumentExtension` |
|
||||
| `src/free_slots.dart` | `ProtectedFreeSlotConflict`, `RequiredCommitmentScheduleResult`, `FreeSlotService` |
|
||||
| `src/locked_time.dart` | `ClockTime`, `LockedBlockRecurrence`, `LockedBlock`, `LockedBlockOccurrence`, `LockedBlockOverride`, `LockedScheduleExpansion`, `expandLockedBlocksForDay`, `lockedSchedulingIntervalsForDay`, `trackCompletedDuringLockedHours`, `completedDuringLockedHoursMinutes` |
|
||||
| `src/occupancy_policy.dart` | `OccupancyEntry`, `OccupancyPolicy` |
|
||||
| `src/persistence_contract.dart` | `PersistenceEnumName` |
|
||||
| `src/persistence_contract.dart` | `v1SchemaVersion`, `PersistenceDateTimeConvention`, `PersistenceCivilDateConvention`, `PersistenceWallTimeConvention`, `PersistenceEnumName`, V1 document field-name classes |
|
||||
| `src/project_statistics.dart` | `ProjectStatistics`, `ProjectStatisticsApplicationResult`, `ProjectStatisticsAggregationService`, `ProjectSuggestionPolicy`, `ProjectSuggestion`, `ProjectSuggestionSet`, `ProjectDefaultResolution`, `ProjectSuggestionService` |
|
||||
| `src/quick_capture.dart` | `QuickCaptureRequest`, `QuickCaptureResult`, `QuickCaptureService` |
|
||||
| `src/reminder_policy.dart` | `EffectiveReminderProfile`, `ReminderDirective`, `ReminderPolicyService` |
|
||||
|
|
@ -113,7 +120,7 @@ Current notable model fields:
|
|||
`actualStart`, `actualEnd`, `completedAt`, `parentTaskId`, `backlogTags`,
|
||||
`reminderOverride`, `createdAt`, `updatedAt`, `stats`
|
||||
- `ProjectProfile`: `id`, `name`, `colorKey`, default priority/reward/
|
||||
difficulty/reminder profile/duration fields
|
||||
difficulty/reminder profile/duration fields, `archivedAt`
|
||||
- `ProjectStatistics`: completion count, duration sample counts,
|
||||
completion-time buckets, pushes-before-completion totals, reward/difficulty
|
||||
distributions, and reminder-profile observations
|
||||
|
|
@ -309,6 +316,44 @@ Chunk 14.3 intentionally changed the public API:
|
|||
not found, stale revision, no slot, validation, persistence, and duplicate
|
||||
operation outcomes.
|
||||
|
||||
Chunk 14.4 intentionally changed the public API:
|
||||
|
||||
- Added `src/application_management.dart` and exported it from
|
||||
`scheduler_core.dart`.
|
||||
- Added management query/command contracts for backlog, project defaults,
|
||||
project create/update/archive, locked-block create/update/archive, one-day
|
||||
locked overrides, owner settings updates, and notice acknowledgement.
|
||||
- Added `ProjectProfile.archivedAt` and `LockedBlock.archivedAt` soft-archive
|
||||
fields.
|
||||
- Added `NoticeAcknowledgementRecord` and
|
||||
`NoticeAcknowledgementRepository`.
|
||||
- Added stable notice IDs to `TodayPendingNotice` and Today-state filtering for
|
||||
acknowledged notices.
|
||||
|
||||
Chunk 14.5 intentionally changed the public API:
|
||||
|
||||
- Added `src/application_recovery.dart` and exported it from
|
||||
`scheduler_core.dart`.
|
||||
- Added `AppOpenRecoveryOutcome`, `RunAppOpenRecoveryRequest`,
|
||||
`AppOpenRecoveryResult`, and `V1AppOpenRecoveryUseCases`.
|
||||
- Added `InMemoryApplicationUnitOfWork.currentSchedulingSnapshots` for
|
||||
application/recovery contract tests.
|
||||
|
||||
Chunk 15.2 intentionally changed the public API:
|
||||
|
||||
- Replaced legacy enum `.name` document encoding in `document_mapping.dart` with
|
||||
explicit stable V1 code tables through `PersistenceEnumMapping`.
|
||||
- Added `DocumentMappingFailureCode`, `DocumentMappingException`, and
|
||||
`DocumentMetadata`.
|
||||
- Expanded document codecs to every V1 persisted entity: project profiles,
|
||||
project statistics, locked blocks/overrides, activities, owner settings,
|
||||
notice acknowledgements, operation records, scheduling snapshots, notices,
|
||||
changes, overlaps, windows, intervals, civil dates, and wall times.
|
||||
- Changed top-level document `toDocument` helpers/extensions to require owner
|
||||
scope and V1 metadata where the domain model does not carry it directly.
|
||||
- Expanded `persistence_contract.dart` with `v1SchemaVersion`, common document
|
||||
fields, and V1 field sets for every persisted repository entity.
|
||||
|
||||
## Repository contracts
|
||||
|
||||
The current repository surface is pure Dart and in-memory only:
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,11 +1,14 @@
|
|||
// MongoDB-oriented persistence contract helpers.
|
||||
//
|
||||
// This file defines stable names and conventions that future document mapping
|
||||
// code should use. It deliberately does not serialize domain models or import a
|
||||
// MongoDB client.
|
||||
// This file defines stable field names and encoding conventions that document
|
||||
// codecs and future adapters should use. It deliberately does not serialize
|
||||
// domain models or import a MongoDB client.
|
||||
|
||||
import 'time_contracts.dart';
|
||||
|
||||
/// V1 document-schema version.
|
||||
const v1SchemaVersion = 1;
|
||||
|
||||
/// DateTime storage convention for future document persistence.
|
||||
abstract final class PersistenceDateTimeConvention {
|
||||
/// Human-readable convention kept close to the helper for tests and docs.
|
||||
|
|
@ -62,9 +65,12 @@ abstract final class PersistenceWallTimeConvention {
|
|||
}
|
||||
}
|
||||
|
||||
/// Stable enum-name mapping plan for future document persistence.
|
||||
/// Legacy enum-name extension retained for older callers.
|
||||
///
|
||||
/// V1 codecs use explicit stable code maps in `document_mapping.dart` instead
|
||||
/// of relying on this extension.
|
||||
extension PersistenceEnumName on Enum {
|
||||
/// Persist enum values by their Dart enum name.
|
||||
/// Legacy persisted name.
|
||||
String get persistenceName => name;
|
||||
}
|
||||
|
||||
|
|
@ -72,11 +78,38 @@ extension PersistenceEnumName on Enum {
|
|||
abstract final class DocumentFields {
|
||||
/// MongoDB primary id field.
|
||||
static const id = '_id';
|
||||
|
||||
/// Version of the document shape.
|
||||
static const schemaVersion = 'schemaVersion';
|
||||
|
||||
/// Authorization-neutral owner scope.
|
||||
static const ownerId = 'ownerId';
|
||||
|
||||
/// Optimistic-concurrency revision.
|
||||
static const revision = 'revision';
|
||||
|
||||
/// Creation instant.
|
||||
static const createdAt = 'createdAt';
|
||||
|
||||
/// Last update instant.
|
||||
static const updatedAt = 'updatedAt';
|
||||
|
||||
static const common = <String>{
|
||||
schemaVersion,
|
||||
id,
|
||||
ownerId,
|
||||
revision,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/// Document field names for [Task] documents.
|
||||
abstract final class TaskDocumentFields {
|
||||
static const schemaVersion = DocumentFields.schemaVersion;
|
||||
static const id = DocumentFields.id;
|
||||
static const ownerId = DocumentFields.ownerId;
|
||||
static const revision = DocumentFields.revision;
|
||||
static const title = 'title';
|
||||
static const projectId = 'projectId';
|
||||
static const type = 'type';
|
||||
|
|
@ -93,12 +126,14 @@ abstract final class TaskDocumentFields {
|
|||
static const parentTaskId = 'parentTaskId';
|
||||
static const backlogTags = 'backlogTags';
|
||||
static const reminderOverride = 'reminderOverride';
|
||||
static const createdAt = 'createdAt';
|
||||
static const updatedAt = 'updatedAt';
|
||||
static const createdAt = DocumentFields.createdAt;
|
||||
static const updatedAt = DocumentFields.updatedAt;
|
||||
static const stats = 'stats';
|
||||
static const backlogEnteredAt = 'backlogEnteredAt';
|
||||
static const backlogEnteredAtProvenance = 'backlogEnteredAtProvenance';
|
||||
|
||||
static const all = <String>{
|
||||
id,
|
||||
...DocumentFields.common,
|
||||
title,
|
||||
projectId,
|
||||
type,
|
||||
|
|
@ -115,15 +150,20 @@ abstract final class TaskDocumentFields {
|
|||
parentTaskId,
|
||||
backlogTags,
|
||||
reminderOverride,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
stats,
|
||||
backlogEnteredAt,
|
||||
backlogEnteredAtProvenance,
|
||||
};
|
||||
}
|
||||
|
||||
/// Document field names for internal [TaskActivity] documents.
|
||||
abstract final class TaskActivityDocumentFields {
|
||||
static const schemaVersion = DocumentFields.schemaVersion;
|
||||
static const id = DocumentFields.id;
|
||||
static const ownerId = DocumentFields.ownerId;
|
||||
static const revision = DocumentFields.revision;
|
||||
static const createdAt = DocumentFields.createdAt;
|
||||
static const updatedAt = DocumentFields.updatedAt;
|
||||
static const operationId = 'operationId';
|
||||
static const code = 'code';
|
||||
static const taskId = 'taskId';
|
||||
|
|
@ -132,7 +172,7 @@ abstract final class TaskActivityDocumentFields {
|
|||
static const metadata = 'metadata';
|
||||
|
||||
static const all = <String>{
|
||||
id,
|
||||
...DocumentFields.common,
|
||||
operationId,
|
||||
code,
|
||||
taskId,
|
||||
|
|
@ -179,7 +219,12 @@ abstract final class TaskStatisticsDocumentFields {
|
|||
|
||||
/// Document field names for [ProjectProfile] documents.
|
||||
abstract final class ProjectDocumentFields {
|
||||
static const schemaVersion = DocumentFields.schemaVersion;
|
||||
static const id = DocumentFields.id;
|
||||
static const ownerId = DocumentFields.ownerId;
|
||||
static const revision = DocumentFields.revision;
|
||||
static const createdAt = DocumentFields.createdAt;
|
||||
static const updatedAt = DocumentFields.updatedAt;
|
||||
static const name = 'name';
|
||||
static const colorKey = 'colorKey';
|
||||
static const defaultPriority = 'defaultPriority';
|
||||
|
|
@ -187,9 +232,10 @@ abstract final class ProjectDocumentFields {
|
|||
static const defaultDifficulty = 'defaultDifficulty';
|
||||
static const defaultReminderProfile = 'defaultReminderProfile';
|
||||
static const defaultDurationMinutes = 'defaultDurationMinutes';
|
||||
static const archivedAt = 'archivedAt';
|
||||
|
||||
static const all = <String>{
|
||||
id,
|
||||
...DocumentFields.common,
|
||||
name,
|
||||
colorKey,
|
||||
defaultPriority,
|
||||
|
|
@ -197,12 +243,19 @@ abstract final class ProjectDocumentFields {
|
|||
defaultDifficulty,
|
||||
defaultReminderProfile,
|
||||
defaultDurationMinutes,
|
||||
archivedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/// Document field names for [ProjectStatistics] documents.
|
||||
abstract final class ProjectStatisticsDocumentFields {
|
||||
static const schemaVersion = DocumentFields.schemaVersion;
|
||||
static const id = DocumentFields.id;
|
||||
static const ownerId = DocumentFields.ownerId;
|
||||
static const revision = DocumentFields.revision;
|
||||
static const createdAt = DocumentFields.createdAt;
|
||||
static const updatedAt = DocumentFields.updatedAt;
|
||||
static const projectId = 'projectId';
|
||||
static const completedTaskCount = 'completedTaskCount';
|
||||
static const durationMinuteCounts = 'durationMinuteCounts';
|
||||
static const completionTimeBucketCounts = 'completionTimeBucketCounts';
|
||||
|
|
@ -211,9 +264,11 @@ abstract final class ProjectStatisticsDocumentFields {
|
|||
static const rewardCounts = 'rewardCounts';
|
||||
static const difficultyCounts = 'difficultyCounts';
|
||||
static const reminderProfileCounts = 'reminderProfileCounts';
|
||||
static const appliedActivityIds = 'appliedActivityIds';
|
||||
|
||||
static const all = <String>{
|
||||
id,
|
||||
...DocumentFields.common,
|
||||
projectId,
|
||||
completedTaskCount,
|
||||
durationMinuteCounts,
|
||||
completionTimeBucketCounts,
|
||||
|
|
@ -222,32 +277,36 @@ abstract final class ProjectStatisticsDocumentFields {
|
|||
rewardCounts,
|
||||
difficultyCounts,
|
||||
reminderProfileCounts,
|
||||
appliedActivityIds,
|
||||
};
|
||||
}
|
||||
|
||||
/// Document field names for [ClockTime] embedded documents.
|
||||
abstract final class ClockTimeDocumentFields {
|
||||
static const hour = 'hour';
|
||||
static const minute = 'minute';
|
||||
static const value = 'value';
|
||||
|
||||
static const all = <String>{
|
||||
hour,
|
||||
minute,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
/// Document field names for [LockedBlockRecurrence] embedded documents.
|
||||
abstract final class LockedBlockRecurrenceDocumentFields {
|
||||
static const type = 'type';
|
||||
static const weekdays = 'weekdays';
|
||||
|
||||
static const all = <String>{
|
||||
type,
|
||||
weekdays,
|
||||
};
|
||||
}
|
||||
|
||||
/// Document field names for [LockedBlock] documents.
|
||||
abstract final class LockedBlockDocumentFields {
|
||||
static const schemaVersion = DocumentFields.schemaVersion;
|
||||
static const id = DocumentFields.id;
|
||||
static const ownerId = DocumentFields.ownerId;
|
||||
static const revision = DocumentFields.revision;
|
||||
static const name = 'name';
|
||||
static const startTime = 'startTime';
|
||||
static const endTime = 'endTime';
|
||||
|
|
@ -255,11 +314,12 @@ abstract final class LockedBlockDocumentFields {
|
|||
static const recurrence = 'recurrence';
|
||||
static const hiddenByDefault = 'hiddenByDefault';
|
||||
static const projectId = 'projectId';
|
||||
static const createdAt = 'createdAt';
|
||||
static const updatedAt = 'updatedAt';
|
||||
static const createdAt = DocumentFields.createdAt;
|
||||
static const updatedAt = DocumentFields.updatedAt;
|
||||
static const archivedAt = 'archivedAt';
|
||||
|
||||
static const all = <String>{
|
||||
id,
|
||||
...DocumentFields.common,
|
||||
name,
|
||||
startTime,
|
||||
endTime,
|
||||
|
|
@ -267,14 +327,16 @@ abstract final class LockedBlockDocumentFields {
|
|||
recurrence,
|
||||
hiddenByDefault,
|
||||
projectId,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
archivedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/// Document field names for [LockedBlockOverride] documents.
|
||||
abstract final class LockedBlockOverrideDocumentFields {
|
||||
static const schemaVersion = DocumentFields.schemaVersion;
|
||||
static const id = DocumentFields.id;
|
||||
static const ownerId = DocumentFields.ownerId;
|
||||
static const revision = DocumentFields.revision;
|
||||
static const lockedBlockId = 'lockedBlockId';
|
||||
static const date = 'date';
|
||||
static const type = 'type';
|
||||
|
|
@ -283,11 +345,11 @@ abstract final class LockedBlockOverrideDocumentFields {
|
|||
static const endTime = 'endTime';
|
||||
static const hiddenByDefault = 'hiddenByDefault';
|
||||
static const projectId = 'projectId';
|
||||
static const createdAt = 'createdAt';
|
||||
static const updatedAt = 'updatedAt';
|
||||
static const createdAt = DocumentFields.createdAt;
|
||||
static const updatedAt = DocumentFields.updatedAt;
|
||||
|
||||
static const all = <String>{
|
||||
id,
|
||||
...DocumentFields.common,
|
||||
lockedBlockId,
|
||||
date,
|
||||
type,
|
||||
|
|
@ -296,15 +358,93 @@ abstract final class LockedBlockOverrideDocumentFields {
|
|||
endTime,
|
||||
hiddenByDefault,
|
||||
projectId,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/// Document field names for [OwnerSettings] documents.
|
||||
abstract final class OwnerSettingsDocumentFields {
|
||||
static const schemaVersion = DocumentFields.schemaVersion;
|
||||
static const id = DocumentFields.id;
|
||||
static const ownerId = DocumentFields.ownerId;
|
||||
static const revision = DocumentFields.revision;
|
||||
static const createdAt = DocumentFields.createdAt;
|
||||
static const updatedAt = DocumentFields.updatedAt;
|
||||
static const timeZoneId = 'timeZoneId';
|
||||
static const dayStart = 'dayStart';
|
||||
static const dayEnd = 'dayEnd';
|
||||
static const compactModeEnabled = 'compactModeEnabled';
|
||||
static const backlogStaleness = 'backlogStaleness';
|
||||
|
||||
static const all = <String>{
|
||||
...DocumentFields.common,
|
||||
timeZoneId,
|
||||
dayStart,
|
||||
dayEnd,
|
||||
compactModeEnabled,
|
||||
backlogStaleness,
|
||||
};
|
||||
}
|
||||
|
||||
/// Document field names for backlog staleness settings.
|
||||
abstract final class BacklogStalenessDocumentFields {
|
||||
static const greenMaxAgeDays = 'greenMaxAgeDays';
|
||||
static const blueMaxAgeDays = 'blueMaxAgeDays';
|
||||
|
||||
static const all = <String>{
|
||||
greenMaxAgeDays,
|
||||
blueMaxAgeDays,
|
||||
};
|
||||
}
|
||||
|
||||
/// Document field names for notice acknowledgement documents.
|
||||
abstract final class NoticeAcknowledgementDocumentFields {
|
||||
static const schemaVersion = DocumentFields.schemaVersion;
|
||||
static const id = DocumentFields.id;
|
||||
static const ownerId = DocumentFields.ownerId;
|
||||
static const revision = DocumentFields.revision;
|
||||
static const createdAt = DocumentFields.createdAt;
|
||||
static const updatedAt = DocumentFields.updatedAt;
|
||||
static const noticeId = 'noticeId';
|
||||
static const acknowledgedAt = 'acknowledgedAt';
|
||||
|
||||
static const all = <String>{
|
||||
...DocumentFields.common,
|
||||
noticeId,
|
||||
acknowledgedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/// Document field names for idempotent operation records.
|
||||
abstract final class ApplicationOperationDocumentFields {
|
||||
static const schemaVersion = DocumentFields.schemaVersion;
|
||||
static const id = DocumentFields.id;
|
||||
static const ownerId = DocumentFields.ownerId;
|
||||
static const revision = DocumentFields.revision;
|
||||
static const createdAt = DocumentFields.createdAt;
|
||||
static const updatedAt = DocumentFields.updatedAt;
|
||||
static const operationId = 'operationId';
|
||||
static const operationName = 'operationName';
|
||||
static const committedAt = 'committedAt';
|
||||
|
||||
static const all = <String>{
|
||||
...DocumentFields.common,
|
||||
operationId,
|
||||
operationName,
|
||||
committedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/// Document field names for [SchedulingStateSnapshot] documents.
|
||||
abstract final class SchedulingSnapshotDocumentFields {
|
||||
static const schemaVersion = DocumentFields.schemaVersion;
|
||||
static const id = DocumentFields.id;
|
||||
static const ownerId = DocumentFields.ownerId;
|
||||
static const revision = DocumentFields.revision;
|
||||
static const createdAt = DocumentFields.createdAt;
|
||||
static const updatedAt = DocumentFields.updatedAt;
|
||||
static const capturedAt = 'capturedAt';
|
||||
static const sourceDate = 'sourceDate';
|
||||
static const targetDate = 'targetDate';
|
||||
static const window = 'window';
|
||||
static const tasks = 'tasks';
|
||||
static const lockedIntervals = 'lockedIntervals';
|
||||
|
|
@ -313,10 +453,14 @@ abstract final class SchedulingSnapshotDocumentFields {
|
|||
static const changes = 'changes';
|
||||
static const overlaps = 'overlaps';
|
||||
static const operationName = 'operationName';
|
||||
static const retentionExpiresAt = 'retentionExpiresAt';
|
||||
static const truncated = 'truncated';
|
||||
|
||||
static const all = <String>{
|
||||
id,
|
||||
...DocumentFields.common,
|
||||
capturedAt,
|
||||
sourceDate,
|
||||
targetDate,
|
||||
window,
|
||||
tasks,
|
||||
lockedIntervals,
|
||||
|
|
@ -325,6 +469,8 @@ abstract final class SchedulingSnapshotDocumentFields {
|
|||
changes,
|
||||
overlaps,
|
||||
operationName,
|
||||
retentionExpiresAt,
|
||||
truncated,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -357,11 +503,19 @@ abstract final class SchedulingNoticeDocumentFields {
|
|||
static const message = 'message';
|
||||
static const type = 'type';
|
||||
static const taskId = 'taskId';
|
||||
static const issueCode = 'issueCode';
|
||||
static const movementCode = 'movementCode';
|
||||
static const conflictCode = 'conflictCode';
|
||||
static const parameters = 'parameters';
|
||||
|
||||
static const all = <String>{
|
||||
message,
|
||||
type,
|
||||
taskId,
|
||||
issueCode,
|
||||
movementCode,
|
||||
conflictCode,
|
||||
parameters,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -398,6 +552,7 @@ abstract final class SchedulingOverlapDocumentFields {
|
|||
/// Every field-name set currently committed for future document mapping.
|
||||
abstract final class PersistenceDocumentFieldSets {
|
||||
static const all = <Set<String>>[
|
||||
DocumentFields.common,
|
||||
TaskDocumentFields.all,
|
||||
TaskActivityDocumentFields.all,
|
||||
TaskStatisticsDocumentFields.all,
|
||||
|
|
@ -407,6 +562,10 @@ abstract final class PersistenceDocumentFieldSets {
|
|||
LockedBlockRecurrenceDocumentFields.all,
|
||||
LockedBlockDocumentFields.all,
|
||||
LockedBlockOverrideDocumentFields.all,
|
||||
OwnerSettingsDocumentFields.all,
|
||||
BacklogStalenessDocumentFields.all,
|
||||
NoticeAcknowledgementDocumentFields.all,
|
||||
ApplicationOperationDocumentFields.all,
|
||||
SchedulingSnapshotDocumentFields.all,
|
||||
SchedulingWindowDocumentFields.all,
|
||||
TimeIntervalDocumentFields.all,
|
||||
|
|
|
|||
|
|
@ -2,23 +2,29 @@ import 'package:adhd_scheduler_core/scheduler_core.dart';
|
|||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('MongoDB document mapping helpers', () {
|
||||
group('MongoDB V1 document codecs', () {
|
||||
final createdAt = DateTime.utc(2026, 6, 23, 8);
|
||||
final updatedAt = DateTime.utc(2026, 6, 23, 8, 5);
|
||||
|
||||
test('task documents round-trip every scheduling field', () {
|
||||
test('task documents round-trip full and nullable V1 fields', () {
|
||||
final task = scheduledTask(
|
||||
id: 'task-1',
|
||||
createdAt: createdAt,
|
||||
reward: RewardLevel.high,
|
||||
difficulty: DifficultyLevel.hard,
|
||||
).copyWith(
|
||||
stats: const TaskStatistics().incrementManualPush(),
|
||||
);
|
||||
).copyWith(stats: const TaskStatistics().incrementManualPush());
|
||||
|
||||
final document = task.toDocument();
|
||||
final document = task.toDocument(
|
||||
ownerId: 'owner-1',
|
||||
revision: 4,
|
||||
backlogEnteredAt: createdAt,
|
||||
backlogEnteredAtProvenance: 'recorded',
|
||||
)..['forwardCompatibleExtra'] = 'ignored';
|
||||
final restored = TaskDocumentMapping.fromDocument(document);
|
||||
|
||||
expect(document[TaskDocumentFields.id], task.id);
|
||||
expect(document[DocumentFields.schemaVersion], v1SchemaVersion);
|
||||
expect(document[TaskDocumentFields.ownerId], 'owner-1');
|
||||
expect(document[TaskDocumentFields.revision], 4);
|
||||
expect(document[TaskDocumentFields.type], 'flexible');
|
||||
expect(document[TaskDocumentFields.status], 'planned');
|
||||
expect(document[TaskDocumentFields.reward], 'high');
|
||||
|
|
@ -26,68 +32,53 @@ void main() {
|
|||
expect(document[TaskDocumentFields.scheduledStart],
|
||||
'2026-06-23T09:00:00.000Z');
|
||||
expect(restored.id, task.id);
|
||||
expect(restored.title, task.title);
|
||||
expect(restored.projectId, task.projectId);
|
||||
expect(restored.type, task.type);
|
||||
expect(restored.status, task.status);
|
||||
expect(restored.priority, task.priority);
|
||||
expect(restored.reward, task.reward);
|
||||
expect(restored.difficulty, task.difficulty);
|
||||
expect(restored.durationMinutes, task.durationMinutes);
|
||||
expect(restored.scheduledStart, task.scheduledStart);
|
||||
expect(restored.scheduledEnd, task.scheduledEnd);
|
||||
expect(restored.actualStart, task.actualStart);
|
||||
expect(restored.actualEnd, task.actualEnd);
|
||||
expect(restored.completedAt, task.completedAt);
|
||||
expect(restored.parentTaskId, task.parentTaskId);
|
||||
expect(restored.backlogTags, task.backlogTags);
|
||||
expect(restored.reminderOverride, task.reminderOverride);
|
||||
expect(restored.createdAt, task.createdAt);
|
||||
expect(restored.updatedAt, task.updatedAt);
|
||||
expect(restored.stats.manuallyPushedCount, 1);
|
||||
|
||||
final nullableTask = Task(
|
||||
id: 'task-nullable',
|
||||
title: 'Nullable task',
|
||||
projectId: 'inbox',
|
||||
type: TaskType.flexible,
|
||||
status: TaskStatus.backlog,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
final nullableDocument = nullableTask.toDocument(ownerId: 'owner-1');
|
||||
final nullableRestored = TaskDocumentMapping.fromDocument(
|
||||
nullableDocument,
|
||||
);
|
||||
|
||||
expect(nullableDocument[TaskDocumentFields.priority], isNull);
|
||||
expect(nullableDocument[TaskDocumentFields.durationMinutes], isNull);
|
||||
expect(nullableDocument[TaskDocumentFields.scheduledStart], isNull);
|
||||
expect(nullableDocument[TaskDocumentFields.backlogEnteredAt], isNull);
|
||||
expect(nullableRestored.priority, isNull);
|
||||
expect(nullableRestored.durationMinutes, isNull);
|
||||
expect(nullableRestored.scheduledStart, isNull);
|
||||
});
|
||||
|
||||
test('task documents preserve not-set reward distinctly from very low', () {
|
||||
final noReward = scheduledTask(
|
||||
id: 'no-reward',
|
||||
createdAt: createdAt,
|
||||
reward: RewardLevel.notSet,
|
||||
);
|
||||
final veryLowReward = scheduledTask(
|
||||
id: 'very-low',
|
||||
createdAt: createdAt,
|
||||
reward: RewardLevel.veryLow,
|
||||
);
|
||||
test('task documents preserve explicit schedule clearing', () {
|
||||
final task = scheduledTask(id: 'scheduled', createdAt: createdAt);
|
||||
|
||||
final noRewardDocument = noReward.toDocument();
|
||||
final veryLowRewardDocument = veryLowReward.toDocument();
|
||||
|
||||
expect(noRewardDocument[TaskDocumentFields.reward], 'notSet');
|
||||
expect(veryLowRewardDocument[TaskDocumentFields.reward], 'veryLow');
|
||||
expect(
|
||||
TaskDocumentMapping.fromDocument(noRewardDocument).reward,
|
||||
RewardLevel.notSet,
|
||||
final document = task.toDocument(
|
||||
ownerId: 'owner-1',
|
||||
clearSchedule: true,
|
||||
);
|
||||
expect(
|
||||
TaskDocumentMapping.fromDocument(veryLowRewardDocument).reward,
|
||||
RewardLevel.veryLow,
|
||||
);
|
||||
});
|
||||
final restored = TaskDocumentMapping.fromDocument(document);
|
||||
|
||||
test('task documents support explicit nullable schedule clearing', () {
|
||||
final task = scheduledTask(
|
||||
id: 'scheduled',
|
||||
createdAt: createdAt,
|
||||
);
|
||||
|
||||
final clearedDocument = task.toDocument(clearSchedule: true);
|
||||
final restored = TaskDocumentMapping.fromDocument(clearedDocument);
|
||||
|
||||
expect(clearedDocument[TaskDocumentFields.scheduledStart], isNull);
|
||||
expect(clearedDocument[TaskDocumentFields.scheduledEnd], isNull);
|
||||
expect(document[TaskDocumentFields.scheduledStart], isNull);
|
||||
expect(document[TaskDocumentFields.scheduledEnd], isNull);
|
||||
expect(restored.scheduledStart, isNull);
|
||||
expect(restored.scheduledEnd, isNull);
|
||||
expect(restored.id, task.id);
|
||||
expect(restored.actualStart, task.actualStart);
|
||||
});
|
||||
|
||||
test('task statistics document mapping preserves every counter', () {
|
||||
|
|
@ -126,7 +117,39 @@ void main() {
|
|||
expect(restored.totalPushesBeforeCompletion, 13);
|
||||
});
|
||||
|
||||
test('project statistics document mapping preserves aggregate fields', () {
|
||||
test('project profile documents round-trip configured defaults', () {
|
||||
final project = ProjectProfile(
|
||||
id: 'home',
|
||||
name: 'Home',
|
||||
colorKey: 'home-blue',
|
||||
defaultPriority: PriorityLevel.high,
|
||||
defaultReward: RewardLevel.low,
|
||||
defaultDifficulty: DifficultyLevel.easy,
|
||||
defaultReminderProfile: ReminderProfile.persistent,
|
||||
defaultDurationMinutes: 45,
|
||||
archivedAt: DateTime.utc(2026, 6, 24, 9),
|
||||
);
|
||||
|
||||
final document = ProjectDocumentMapping.toDocument(
|
||||
project,
|
||||
ownerId: 'owner-1',
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
revision: 2,
|
||||
);
|
||||
final restored = ProjectDocumentMapping.fromDocument(document);
|
||||
|
||||
expect(document.keys.toSet(), ProjectDocumentFields.all);
|
||||
expect(document[ProjectDocumentFields.defaultPriority], 'high');
|
||||
expect(
|
||||
document[ProjectDocumentFields.defaultReminderProfile], 'persistent');
|
||||
expect(restored.id, project.id);
|
||||
expect(restored.defaultDurationMinutes, 45);
|
||||
expect(restored.archivedAt, project.archivedAt);
|
||||
});
|
||||
|
||||
test('project statistics documents preserve aggregates and applied ids',
|
||||
() {
|
||||
final statistics = ProjectStatistics(
|
||||
projectId: 'home',
|
||||
completedTaskCount: 8,
|
||||
|
|
@ -151,36 +174,371 @@ void main() {
|
|||
},
|
||||
);
|
||||
|
||||
final document = statistics.toDocument();
|
||||
final document = statistics.toDocument(
|
||||
ownerId: 'owner-1',
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
appliedActivityIds: const ['activity-1'],
|
||||
);
|
||||
final restored = ProjectStatisticsDocumentMapping.fromDocument(document);
|
||||
|
||||
expect(document.keys.toSet(), ProjectStatisticsDocumentFields.all);
|
||||
expect(document[ProjectStatisticsDocumentFields.id], 'home');
|
||||
expect(document[ProjectStatisticsDocumentFields.projectId], 'home');
|
||||
expect(document[ProjectStatisticsDocumentFields.durationMinuteCounts], {
|
||||
'20': 3,
|
||||
'45': 5,
|
||||
});
|
||||
expect(document[ProjectStatisticsDocumentFields.rewardCounts], {
|
||||
'high': 5,
|
||||
'low': 3,
|
||||
});
|
||||
expect(document[ProjectStatisticsDocumentFields.appliedActivityIds], [
|
||||
'activity-1',
|
||||
]);
|
||||
expect(restored.projectId, statistics.projectId);
|
||||
expect(restored.completedTaskCount, 8);
|
||||
expect(restored.durationMinuteCounts, statistics.durationMinuteCounts);
|
||||
expect(restored.completionTimeBucketCounts,
|
||||
statistics.completionTimeBucketCounts);
|
||||
expect(restored.totalPushesBeforeCompletion, 11);
|
||||
expect(restored.completedAfterPushCount, 4);
|
||||
expect(restored.rewardCounts, statistics.rewardCounts);
|
||||
expect(restored.difficultyCounts, statistics.difficultyCounts);
|
||||
expect(restored.reminderProfileCounts, statistics.reminderProfileCounts);
|
||||
});
|
||||
|
||||
test('enum document mapping rejects unknown persistence names', () {
|
||||
test(
|
||||
'locked block and override documents preserve civil dates and wall time',
|
||||
() {
|
||||
final recurrence = LockedBlockRecurrence.weekly(
|
||||
weekdays: {LockedWeekday.monday, LockedWeekday.thursday},
|
||||
);
|
||||
final recurringBlock = LockedBlock(
|
||||
id: 'work',
|
||||
name: 'Work',
|
||||
startTime: WallTime(hour: 9, minute: 0),
|
||||
endTime: WallTime(hour: 17, minute: 0),
|
||||
recurrence: recurrence,
|
||||
hiddenByDefault: true,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
archivedAt: DateTime.utc(2026, 6, 30),
|
||||
);
|
||||
final replaceOverride = LockedBlockOverride.replace(
|
||||
id: 'replace-work',
|
||||
lockedBlockId: 'work',
|
||||
date: CivilDate(2026, 6, 25),
|
||||
startTime: WallTime(hour: 10, minute: 0),
|
||||
endTime: WallTime(hour: 15, minute: 0),
|
||||
createdAt: createdAt,
|
||||
name: 'Short work',
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
final addOverride = LockedBlockOverride.add(
|
||||
id: 'appointment',
|
||||
date: CivilDate(2026, 6, 25),
|
||||
name: 'Appointment',
|
||||
startTime: WallTime(hour: 18, minute: 0),
|
||||
endTime: WallTime(hour: 19, minute: 0),
|
||||
createdAt: createdAt,
|
||||
);
|
||||
final removeOverride = LockedBlockOverride.remove(
|
||||
id: 'remove-work',
|
||||
lockedBlockId: 'work',
|
||||
date: CivilDate(2026, 6, 26),
|
||||
createdAt: createdAt,
|
||||
);
|
||||
|
||||
final blockDocument = LockedBlockDocumentMapping.toDocument(
|
||||
recurringBlock,
|
||||
ownerId: 'owner-1',
|
||||
);
|
||||
final restoredBlock = LockedBlockDocumentMapping.fromDocument(
|
||||
blockDocument,
|
||||
);
|
||||
final replaceDocument = LockedBlockOverrideDocumentMapping.toDocument(
|
||||
replaceOverride,
|
||||
ownerId: 'owner-1',
|
||||
);
|
||||
final addDocument = LockedBlockOverrideDocumentMapping.toDocument(
|
||||
addOverride,
|
||||
ownerId: 'owner-1',
|
||||
);
|
||||
final removeDocument = LockedBlockOverrideDocumentMapping.toDocument(
|
||||
removeOverride,
|
||||
ownerId: 'owner-1',
|
||||
);
|
||||
|
||||
expect(blockDocument[LockedBlockDocumentFields.startTime], '09:00');
|
||||
expect(
|
||||
() => PersistenceEnumMapping.decodeTaskType('unknown'),
|
||||
throwsArgumentError,
|
||||
(blockDocument[LockedBlockDocumentFields.recurrence] as Map<String,
|
||||
Object?>)[LockedBlockRecurrenceDocumentFields.weekdays],
|
||||
['monday', 'thursday'],
|
||||
);
|
||||
expect(restoredBlock.recurrence!.weekdays, recurrence.weekdays);
|
||||
expect(restoredBlock.archivedAt, recurringBlock.archivedAt);
|
||||
expect(replaceDocument[LockedBlockOverrideDocumentFields.date],
|
||||
'2026-06-25');
|
||||
expect(
|
||||
replaceDocument[LockedBlockOverrideDocumentFields.type], 'replace');
|
||||
expect(addDocument[LockedBlockOverrideDocumentFields.type], 'add');
|
||||
expect(removeDocument[LockedBlockOverrideDocumentFields.type], 'remove');
|
||||
expect(
|
||||
LockedBlockOverrideDocumentMapping.fromDocument(replaceDocument).date,
|
||||
CivilDate(2026, 6, 25),
|
||||
);
|
||||
expect(
|
||||
LockedBlockOverrideDocumentMapping.fromDocument(removeDocument)
|
||||
.startTime,
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('settings, notices, operations, and activities round-trip', () {
|
||||
final settings = OwnerSettings(
|
||||
ownerId: 'owner-1',
|
||||
timeZoneId: 'America/Los_Angeles',
|
||||
dayStart: WallTime(hour: 7, minute: 30),
|
||||
dayEnd: WallTime(hour: 21, minute: 0),
|
||||
compactModeEnabled: true,
|
||||
backlogStalenessSettings: const BacklogStalenessSettings(
|
||||
greenMaxAge: Duration(days: 3),
|
||||
blueMaxAge: Duration(days: 10),
|
||||
),
|
||||
);
|
||||
final activity = TaskActivity(
|
||||
id: 'activity-1',
|
||||
operationId: 'op-1',
|
||||
code: TaskActivityCode.completed,
|
||||
taskId: 'task-1',
|
||||
projectId: 'home',
|
||||
occurredAt: createdAt,
|
||||
metadata: {'completedAt': createdAt},
|
||||
);
|
||||
final acknowledgement = NoticeAcknowledgementRecord(
|
||||
noticeId: 'snapshot:0',
|
||||
ownerId: 'owner-1',
|
||||
acknowledgedAt: updatedAt,
|
||||
);
|
||||
final operation = ApplicationOperationRecord(
|
||||
operationId: 'op-1',
|
||||
ownerId: 'owner-1',
|
||||
operationName: 'completeTask',
|
||||
committedAt: updatedAt,
|
||||
);
|
||||
|
||||
final settingsDocument = OwnerSettingsDocumentMapping.toDocument(
|
||||
settings,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
final activityDocument = TaskActivityDocumentMapping.toDocument(
|
||||
activity,
|
||||
ownerId: 'owner-1',
|
||||
);
|
||||
final acknowledgementDocument =
|
||||
NoticeAcknowledgementDocumentMapping.toDocument(acknowledgement);
|
||||
final operationDocument = ApplicationOperationDocumentMapping.toDocument(
|
||||
operation,
|
||||
);
|
||||
|
||||
expect(settingsDocument[OwnerSettingsDocumentFields.dayStart], '07:30');
|
||||
expect(
|
||||
(settingsDocument[OwnerSettingsDocumentFields.backlogStaleness] as Map<
|
||||
String, Object?>)[BacklogStalenessDocumentFields.blueMaxAgeDays],
|
||||
10,
|
||||
);
|
||||
expect(activityDocument[TaskActivityDocumentFields.code], 'completed');
|
||||
expect(
|
||||
(activityDocument[TaskActivityDocumentFields.metadata]
|
||||
as Map<String, Object?>)['completedAt'],
|
||||
'2026-06-23T08:00:00.000Z',
|
||||
);
|
||||
expect(acknowledgementDocument[NoticeAcknowledgementDocumentFields.id],
|
||||
'snapshot:0');
|
||||
expect(operationDocument[ApplicationOperationDocumentFields.operationId],
|
||||
'op-1');
|
||||
|
||||
expect(OwnerSettingsDocumentMapping.fromDocument(settingsDocument).dayEnd,
|
||||
settings.dayEnd);
|
||||
expect(TaskActivityDocumentMapping.fromDocument(activityDocument).code,
|
||||
TaskActivityCode.completed);
|
||||
expect(
|
||||
NoticeAcknowledgementDocumentMapping.fromDocument(
|
||||
acknowledgementDocument,
|
||||
).noticeId,
|
||||
'snapshot:0',
|
||||
);
|
||||
expect(
|
||||
ApplicationOperationDocumentMapping.fromDocument(operationDocument)
|
||||
.operationName,
|
||||
'completeTask',
|
||||
);
|
||||
});
|
||||
|
||||
test('scheduling snapshot structures round-trip nested data', () {
|
||||
final task = scheduledTask(id: 'task-1', createdAt: createdAt);
|
||||
final snapshot = SchedulingStateSnapshot(
|
||||
id: 'rollover:owner-1:2026-06-25',
|
||||
capturedAt: updatedAt,
|
||||
window: SchedulingWindow(
|
||||
start: DateTime.utc(2026, 6, 24, 9),
|
||||
end: DateTime.utc(2026, 6, 24, 17),
|
||||
),
|
||||
tasks: [task],
|
||||
lockedIntervals: [
|
||||
TimeInterval(
|
||||
start: DateTime.utc(2026, 6, 24, 12),
|
||||
end: DateTime.utc(2026, 6, 24, 13),
|
||||
label: 'Hidden locked',
|
||||
),
|
||||
],
|
||||
notices: [
|
||||
SchedulingNotice(
|
||||
'1 unfinished flexible task was moved to today.',
|
||||
type: SchedulingNoticeType.moved,
|
||||
movementCode:
|
||||
SchedulingMovementCode.unfinishedFlexibleTasksRolledOver,
|
||||
parameters: {'count': 1},
|
||||
),
|
||||
],
|
||||
changes: [
|
||||
SchedulingChange(
|
||||
taskId: task.id,
|
||||
previousStart: task.scheduledStart,
|
||||
previousEnd: task.scheduledEnd,
|
||||
nextStart: DateTime.utc(2026, 6, 24, 9),
|
||||
nextEnd: DateTime.utc(2026, 6, 24, 9, 30),
|
||||
),
|
||||
],
|
||||
overlaps: [
|
||||
SchedulingOverlap(
|
||||
taskId: task.id,
|
||||
taskInterval: TimeInterval(
|
||||
start: DateTime.utc(2026, 6, 23, 9),
|
||||
end: DateTime.utc(2026, 6, 23, 9, 30),
|
||||
),
|
||||
blockedInterval: TimeInterval(
|
||||
start: DateTime.utc(2026, 6, 23, 9, 15),
|
||||
end: DateTime.utc(2026, 6, 23, 9, 45),
|
||||
),
|
||||
),
|
||||
],
|
||||
operationName: 'appOpenRecovery',
|
||||
);
|
||||
|
||||
final document = SchedulingSnapshotDocumentMapping.toDocument(
|
||||
snapshot,
|
||||
ownerId: 'owner-1',
|
||||
sourceDate: CivilDate(2026, 6, 23),
|
||||
targetDate: CivilDate(2026, 6, 24),
|
||||
retentionExpiresAt: DateTime.utc(2026, 7, 24),
|
||||
);
|
||||
final restored = SchedulingSnapshotDocumentMapping.fromDocument(document);
|
||||
|
||||
expect(document.keys.toSet(), SchedulingSnapshotDocumentFields.all);
|
||||
expect(
|
||||
document[SchedulingSnapshotDocumentFields.sourceDate], '2026-06-23');
|
||||
expect(restored.id, snapshot.id);
|
||||
expect(restored.window.start, snapshot.window.start);
|
||||
expect(restored.tasks.single.id, task.id);
|
||||
expect(restored.lockedIntervals.single.label, 'Hidden locked');
|
||||
expect(restored.notices.single.movementCode,
|
||||
SchedulingMovementCode.unfinishedFlexibleTasksRolledOver);
|
||||
expect(restored.notices.single.parameters['count'], 1);
|
||||
expect(restored.changes.single.taskId, task.id);
|
||||
expect(restored.overlaps.single.taskId, task.id);
|
||||
});
|
||||
|
||||
test('stable enum code tables do not depend on Dart enum names', () {
|
||||
expect(
|
||||
PersistenceEnumMapping.encodeReward(RewardLevel.notSet), 'not_set');
|
||||
expect(
|
||||
PersistenceEnumMapping.encodeTaskStatus(
|
||||
TaskStatus.noLongerRelevant,
|
||||
),
|
||||
'no_longer_relevant');
|
||||
expect(PersistenceEnumMapping.encodeTaskType(TaskType.freeSlot),
|
||||
'free_slot');
|
||||
expect(
|
||||
PersistenceEnumMapping.decodeTaskStatus('no_longer_relevant'),
|
||||
TaskStatus.noLongerRelevant,
|
||||
);
|
||||
expect(
|
||||
PersistenceEnumMapping.decodeReward('not_set'),
|
||||
RewardLevel.notSet,
|
||||
);
|
||||
});
|
||||
|
||||
test('malformed documents produce typed mapping failures', () {
|
||||
final valid = scheduledTask(id: 'task-1', createdAt: createdAt)
|
||||
.toDocument(ownerId: 'owner-1');
|
||||
|
||||
expect(
|
||||
() => TaskDocumentMapping.fromDocument(
|
||||
{...valid}..remove(TaskDocumentFields.title),
|
||||
),
|
||||
throwsMapping(
|
||||
DocumentMappingFailureCode.missingField, TaskDocumentFields.title),
|
||||
);
|
||||
expect(
|
||||
() => TaskDocumentMapping.fromDocument({
|
||||
...valid,
|
||||
TaskDocumentFields.reward: 'notSet',
|
||||
}),
|
||||
throwsMapping(DocumentMappingFailureCode.unknownCode, 'RewardLevel'),
|
||||
);
|
||||
expect(
|
||||
() => TaskDocumentMapping.fromDocument({
|
||||
...valid,
|
||||
TaskDocumentFields.durationMinutes: '30',
|
||||
}),
|
||||
throwsMapping(DocumentMappingFailureCode.wrongType,
|
||||
TaskDocumentFields.durationMinutes),
|
||||
);
|
||||
expect(
|
||||
() => TaskDocumentMapping.fromDocument({
|
||||
...valid,
|
||||
DocumentFields.revision: 0,
|
||||
}),
|
||||
throwsMapping(DocumentMappingFailureCode.invalidRevision,
|
||||
DocumentFields.revision),
|
||||
);
|
||||
expect(
|
||||
() => TaskDocumentMapping.fromDocument({
|
||||
...valid,
|
||||
DocumentFields.schemaVersion: 99,
|
||||
}),
|
||||
throwsMapping(DocumentMappingFailureCode.invalidSchemaVersion,
|
||||
DocumentFields.schemaVersion),
|
||||
);
|
||||
expect(
|
||||
() => SchedulingWindowDocumentMapping.fromDocument({
|
||||
SchedulingWindowDocumentFields.start: '2026-06-23T10:00:00.000Z',
|
||||
SchedulingWindowDocumentFields.end: '2026-06-23T09:00:00.000Z',
|
||||
}),
|
||||
throwsMapping(DocumentMappingFailureCode.invalidValue),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Matcher throwsMapping(
|
||||
DocumentMappingFailureCode code, [
|
||||
String? fieldName,
|
||||
]) {
|
||||
var matcher = isA<DocumentMappingException>().having(
|
||||
(error) => error.code,
|
||||
'code',
|
||||
code,
|
||||
);
|
||||
if (fieldName != null) {
|
||||
matcher = matcher.having(
|
||||
(error) => error.fieldName,
|
||||
'fieldName',
|
||||
fieldName,
|
||||
);
|
||||
}
|
||||
return throwsA(matcher);
|
||||
}
|
||||
|
||||
Task scheduledTask({
|
||||
required String id,
|
||||
required DateTime createdAt,
|
||||
|
|
|
|||
|
|
@ -204,7 +204,12 @@ void main() {
|
|||
|
||||
test('document field sets preserve all task and statistics fields', () {
|
||||
expect(TaskDocumentFields.all, {
|
||||
'schemaVersion',
|
||||
'_id',
|
||||
'ownerId',
|
||||
'revision',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'title',
|
||||
'projectId',
|
||||
'type',
|
||||
|
|
@ -221,12 +226,17 @@ void main() {
|
|||
'parentTaskId',
|
||||
'backlogTags',
|
||||
'reminderOverride',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'stats',
|
||||
'backlogEnteredAt',
|
||||
'backlogEnteredAtProvenance',
|
||||
});
|
||||
expect(TaskActivityDocumentFields.all, {
|
||||
'schemaVersion',
|
||||
'_id',
|
||||
'ownerId',
|
||||
'revision',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'operationId',
|
||||
'code',
|
||||
'taskId',
|
||||
|
|
@ -252,9 +262,30 @@ void main() {
|
|||
});
|
||||
|
||||
test('document field sets cover project locked and snapshot documents', () {
|
||||
expect(ProjectDocumentFields.all, containsAll(['_id', 'colorKey']));
|
||||
expect(ProjectStatisticsDocumentFields.all, {
|
||||
expect(ProjectDocumentFields.all, {
|
||||
'schemaVersion',
|
||||
'_id',
|
||||
'ownerId',
|
||||
'revision',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'name',
|
||||
'colorKey',
|
||||
'defaultPriority',
|
||||
'defaultReward',
|
||||
'defaultDifficulty',
|
||||
'defaultReminderProfile',
|
||||
'defaultDurationMinutes',
|
||||
'archivedAt',
|
||||
});
|
||||
expect(ProjectStatisticsDocumentFields.all, {
|
||||
'schemaVersion',
|
||||
'_id',
|
||||
'ownerId',
|
||||
'revision',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'projectId',
|
||||
'completedTaskCount',
|
||||
'durationMinuteCounts',
|
||||
'completionTimeBucketCounts',
|
||||
|
|
@ -263,20 +294,48 @@ void main() {
|
|||
'rewardCounts',
|
||||
'difficultyCounts',
|
||||
'reminderProfileCounts',
|
||||
'appliedActivityIds',
|
||||
});
|
||||
expect(
|
||||
LockedBlockDocumentFields.all,
|
||||
containsAll(['_id', 'startTime', 'recurrence', 'hiddenByDefault']),
|
||||
containsAll([
|
||||
'schemaVersion',
|
||||
'_id',
|
||||
'ownerId',
|
||||
'revision',
|
||||
'startTime',
|
||||
'recurrence',
|
||||
'hiddenByDefault',
|
||||
'archivedAt',
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
LockedBlockOverrideDocumentFields.all,
|
||||
containsAll(['_id', 'lockedBlockId', 'type']),
|
||||
containsAll(
|
||||
['schemaVersion', '_id', 'ownerId', 'lockedBlockId', 'type']),
|
||||
);
|
||||
expect(
|
||||
OwnerSettingsDocumentFields.all,
|
||||
containsAll(['_id', 'ownerId', 'timeZoneId', 'backlogStaleness']),
|
||||
);
|
||||
expect(
|
||||
NoticeAcknowledgementDocumentFields.all,
|
||||
containsAll(['_id', 'ownerId', 'noticeId', 'acknowledgedAt']),
|
||||
);
|
||||
expect(
|
||||
ApplicationOperationDocumentFields.all,
|
||||
containsAll(['_id', 'ownerId', 'operationId', 'committedAt']),
|
||||
);
|
||||
expect(
|
||||
SchedulingSnapshotDocumentFields.all,
|
||||
containsAll([
|
||||
'schemaVersion',
|
||||
'_id',
|
||||
'ownerId',
|
||||
'revision',
|
||||
'capturedAt',
|
||||
'sourceDate',
|
||||
'targetDate',
|
||||
'window',
|
||||
'tasks',
|
||||
'lockedIntervals',
|
||||
|
|
@ -285,6 +344,8 @@ void main() {
|
|||
'changes',
|
||||
'overlaps',
|
||||
'operationName',
|
||||
'retentionExpiresAt',
|
||||
'truncated',
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue