forked from eva/focus-flow
feat(data): add legacy document migrations
This commit is contained in:
parent
3dc73e8d43
commit
55efa98182
13 changed files with 1626 additions and 0 deletions
|
|
@ -158,6 +158,8 @@ Verification:
|
||||||
|
|
||||||
Recommended Codex level: extra high
|
Recommended Codex level: extra high
|
||||||
|
|
||||||
|
Status: Complete
|
||||||
|
|
||||||
Tasks:
|
Tasks:
|
||||||
|
|
||||||
- Treat the existing task/task-statistics mapping as legacy schema version 0.
|
- Treat the existing task/task-statistics mapping as legacy schema version 0.
|
||||||
|
|
@ -192,6 +194,36 @@ Acceptance criteria:
|
||||||
- Date/backlog approximations carry provenance.
|
- Date/backlog approximations carry provenance.
|
||||||
- Migration fixtures are part of the automated suite.
|
- Migration fixtures are part of the automated suite.
|
||||||
|
|
||||||
|
Completed implementation:
|
||||||
|
|
||||||
|
- Added `document_migration.dart` with a pure Dart `V1DocumentMigrationService`
|
||||||
|
for V0 task, project, locked block, and locked override documents.
|
||||||
|
- Treated missing `schemaVersion` as schema V0 and validated already-current V1
|
||||||
|
documents through the V1 codecs without rewriting them.
|
||||||
|
- Added typed migration reports/results, non-fatal notes, and `shouldWrite`
|
||||||
|
semantics so adapters can dry-run migrations and avoid overwriting unreadable
|
||||||
|
source documents.
|
||||||
|
- Migrated legacy enum `.name` values to stable V1 codes for task, project, and
|
||||||
|
locked-time fields.
|
||||||
|
- Added conservative backlog provenance: legacy backlog tasks set
|
||||||
|
`backlogEnteredAt` from `createdAt` with
|
||||||
|
`approximated_from_created_at`; non-backlog tasks keep the metadata null.
|
||||||
|
- Preserved date-only locked override values as civil-date strings and rejected
|
||||||
|
ambiguous legacy date-time values rather than guessing a timezone.
|
||||||
|
- Added checked-in JSON fixtures for minimum/full V0 task documents, manually
|
||||||
|
produced project/locked documents, corrupt/malformed V0 documents, and an
|
||||||
|
unsupported future schema document.
|
||||||
|
- Added automated migration tests for V0→V1, already-V1 no-op, repeated
|
||||||
|
migration idempotency, partial corruption, ambiguous date-times, and future
|
||||||
|
schema failure.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
|
||||||
|
- `dart format lib test`: passed, 0 files changed after final format
|
||||||
|
- `dart analyze`: passed, no issues found
|
||||||
|
- `dart test`: passed, 289 tests
|
||||||
|
- `git diff --check`: passed
|
||||||
|
|
||||||
BREAKPOINT: Stop here. Confirm `high` mode before expanding repository/query
|
BREAKPOINT: Stop here. Confirm `high` mode before expanding repository/query
|
||||||
contracts and index specifications.
|
contracts and index specifications.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,22 @@ Chunk 15.2 intentionally changed the public API:
|
||||||
- Expanded `persistence_contract.dart` with `v1SchemaVersion`, common document
|
- Expanded `persistence_contract.dart` with `v1SchemaVersion`, common document
|
||||||
fields, and V1 field sets for every persisted repository entity.
|
fields, and V1 field sets for every persisted repository entity.
|
||||||
|
|
||||||
|
Chunk 15.3 intentionally changed the public API:
|
||||||
|
|
||||||
|
- Added `src/document_migration.dart` and exported it from `scheduler_core.dart`.
|
||||||
|
- Added `V1DocumentMigrationService` for pure Dart V0-to-V1 migration of task,
|
||||||
|
project, locked block, and locked override documents.
|
||||||
|
- Added typed migration surface area:
|
||||||
|
`DocumentMigrationEntity`, `DocumentMigrationStatus`,
|
||||||
|
`DocumentMigrationFailureCode`, `DocumentMigrationNoteCode`,
|
||||||
|
`DocumentMigrationProvenance`, `DocumentMigrationNote`,
|
||||||
|
`DocumentMigrationReport`, and `DocumentMigrationResult`.
|
||||||
|
- Migration results expose `document`, `isSuccess`, and `shouldWrite` so
|
||||||
|
persistence adapters can dry-run migrations and refuse to overwrite unreadable
|
||||||
|
source documents.
|
||||||
|
- Legacy backlog tasks now carry explicit provenance when `backlogEnteredAt` is
|
||||||
|
approximated from `createdAt`.
|
||||||
|
|
||||||
## Repository contracts
|
## Repository contracts
|
||||||
|
|
||||||
The current repository surface is pure Dart and in-memory only:
|
The current repository surface is pure Dart and in-memory only:
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export 'src/application_recovery.dart';
|
||||||
export 'src/backlog.dart';
|
export 'src/backlog.dart';
|
||||||
export 'src/child_tasks.dart';
|
export 'src/child_tasks.dart';
|
||||||
export 'src/document_mapping.dart';
|
export 'src/document_mapping.dart';
|
||||||
|
export 'src/document_migration.dart';
|
||||||
export 'src/free_slots.dart';
|
export 'src/free_slots.dart';
|
||||||
export 'src/locked_time.dart';
|
export 'src/locked_time.dart';
|
||||||
export 'src/occupancy_policy.dart';
|
export 'src/occupancy_policy.dart';
|
||||||
|
|
|
||||||
1138
lib/src/document_migration.dart
Normal file
1138
lib/src/document_migration.dart
Normal file
File diff suppressed because it is too large
Load diff
237
test/document_migration_test.dart
Normal file
237
test/document_migration_test.dart
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:adhd_scheduler_core/scheduler_core.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('V0 to V1 document migrations', () {
|
||||||
|
final migratedAt = DateTime.utc(2026, 6, 25, 12);
|
||||||
|
const ownerId = 'owner-1';
|
||||||
|
final service = V1DocumentMigrationService(
|
||||||
|
ownerId: ownerId,
|
||||||
|
migratedAt: migratedAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
test('legacy minimum task migrates with backlog provenance', () {
|
||||||
|
final legacy = fixture('task_v0_minimum.json');
|
||||||
|
|
||||||
|
final result = service.migrateTask(legacy);
|
||||||
|
final document = result.document!;
|
||||||
|
final task = TaskDocumentMapping.fromDocument(document);
|
||||||
|
|
||||||
|
expect(result.isSuccess, isTrue);
|
||||||
|
expect(result.shouldWrite, isTrue);
|
||||||
|
expect(result.report.status, DocumentMigrationStatus.migrated);
|
||||||
|
expect(result.report.fromSchemaVersion, 0);
|
||||||
|
expect(result.report.documentId, 'task-v0-minimum');
|
||||||
|
expect(result.report.notes.single.code,
|
||||||
|
DocumentMigrationNoteCode.approximatedBacklogEnteredAt);
|
||||||
|
expect(legacy.containsKey(DocumentFields.schemaVersion), isFalse);
|
||||||
|
expect(document, containsPair(DocumentFields.schemaVersion, 1));
|
||||||
|
expect(document, containsPair(DocumentFields.ownerId, ownerId));
|
||||||
|
expect(document, containsPair(DocumentFields.revision, 1));
|
||||||
|
expect(document, containsPair(TaskDocumentFields.reward, 'not_set'));
|
||||||
|
expect(document, containsPair(TaskDocumentFields.difficulty, 'not_set'));
|
||||||
|
expect(
|
||||||
|
document,
|
||||||
|
containsPair(
|
||||||
|
TaskDocumentFields.backlogEnteredAt,
|
||||||
|
'2026-06-20T09:15:00.000Z',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
document,
|
||||||
|
containsPair(
|
||||||
|
TaskDocumentFields.backlogEnteredAtProvenance,
|
||||||
|
DocumentMigrationProvenance.approximatedFromCreatedAt,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(task.id, 'task-v0-minimum');
|
||||||
|
expect(task.status, TaskStatus.backlog);
|
||||||
|
expect(task.stats.manuallyPushedCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('legacy full task migrates enum names to stable codes', () {
|
||||||
|
final legacy = fixture('task_v0_full.json');
|
||||||
|
|
||||||
|
final result = service.migrateTask(legacy);
|
||||||
|
final document = result.document!;
|
||||||
|
final task = TaskDocumentMapping.fromDocument(document);
|
||||||
|
|
||||||
|
expect(result.isSuccess, isTrue);
|
||||||
|
expect(document[TaskDocumentFields.type], 'free_slot');
|
||||||
|
expect(document[TaskDocumentFields.status], 'no_longer_relevant');
|
||||||
|
expect(document[TaskDocumentFields.priority], 'very_high');
|
||||||
|
expect(document[TaskDocumentFields.reward], 'very_low');
|
||||||
|
expect(document[TaskDocumentFields.difficulty], 'very_hard');
|
||||||
|
expect(document[TaskDocumentFields.backlogTags], ['wishlist']);
|
||||||
|
expect(document[TaskDocumentFields.backlogEnteredAt], isNull);
|
||||||
|
expect(document[TaskDocumentFields.backlogEnteredAtProvenance], isNull);
|
||||||
|
expect(task.type, TaskType.freeSlot);
|
||||||
|
expect(task.status, TaskStatus.noLongerRelevant);
|
||||||
|
expect(task.priority, PriorityLevel.veryHigh);
|
||||||
|
expect(task.reward, RewardLevel.veryLow);
|
||||||
|
expect(task.difficulty, DifficultyLevel.veryHard);
|
||||||
|
expect(task.stats.totalPushesBeforeCompletion, 13);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('legacy project and locked documents migrate to V1 shapes', () {
|
||||||
|
final projectResult = service.migrateProject(fixture('project_v0.json'));
|
||||||
|
final blockResult =
|
||||||
|
service.migrateLockedBlock(fixture('locked_block_v0.json'));
|
||||||
|
final overrideResult = service.migrateLockedBlockOverride(
|
||||||
|
fixture('locked_override_v0.json'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(projectResult.isSuccess, isTrue);
|
||||||
|
expect(
|
||||||
|
projectResult.report.notes.single.code,
|
||||||
|
DocumentMigrationNoteCode.synthesizedMetadataTimestamp,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
projectResult.document![ProjectDocumentFields.defaultPriority],
|
||||||
|
'very_high',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
projectResult.document![ProjectDocumentFields.defaultReward],
|
||||||
|
'not_set',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
projectResult.document![ProjectDocumentFields.defaultDifficulty],
|
||||||
|
'very_easy',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
projectResult.document![DocumentFields.createdAt],
|
||||||
|
'2026-06-25T12:00:00.000Z',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
ProjectDocumentMapping.fromDocument(projectResult.document!).id,
|
||||||
|
'project-v0-home',
|
||||||
|
);
|
||||||
|
|
||||||
|
final recurrence =
|
||||||
|
blockResult.document![LockedBlockDocumentFields.recurrence]
|
||||||
|
as Map<String, Object?>;
|
||||||
|
expect(blockResult.isSuccess, isTrue);
|
||||||
|
expect(
|
||||||
|
blockResult.document![LockedBlockDocumentFields.startTime], '09:00');
|
||||||
|
expect(blockResult.document![LockedBlockDocumentFields.endTime], '17:00');
|
||||||
|
expect(
|
||||||
|
recurrence[LockedBlockRecurrenceDocumentFields.type],
|
||||||
|
'weekly',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
recurrence[LockedBlockRecurrenceDocumentFields.weekdays],
|
||||||
|
['monday', 'thursday'],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
LockedBlockDocumentMapping.fromDocument(blockResult.document!)
|
||||||
|
.recurrence!
|
||||||
|
.weekdays,
|
||||||
|
{LockedWeekday.monday, LockedWeekday.thursday},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(overrideResult.isSuccess, isTrue);
|
||||||
|
expect(
|
||||||
|
overrideResult.document![LockedBlockOverrideDocumentFields.date],
|
||||||
|
'2026-06-25',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
overrideResult.document![LockedBlockOverrideDocumentFields.startTime],
|
||||||
|
'10:00',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
overrideResult.document![LockedBlockOverrideDocumentFields.endTime],
|
||||||
|
'15:00',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
LockedBlockOverrideDocumentMapping.fromDocument(
|
||||||
|
overrideResult.document!,
|
||||||
|
).date,
|
||||||
|
CivilDate(2026, 6, 25),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('already-current V1 documents are validated without rewrite', () {
|
||||||
|
final migrated = service
|
||||||
|
.migrateTask(
|
||||||
|
fixture('task_v0_minimum.json'),
|
||||||
|
)
|
||||||
|
.document!;
|
||||||
|
|
||||||
|
final result = service.migrateTask(migrated);
|
||||||
|
|
||||||
|
expect(result.isSuccess, isTrue);
|
||||||
|
expect(result.shouldWrite, isFalse);
|
||||||
|
expect(result.report.status, DocumentMigrationStatus.alreadyCurrent);
|
||||||
|
expect(result.report.fromSchemaVersion, 1);
|
||||||
|
expect(result.document, migrated);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('repeated migration is idempotent', () {
|
||||||
|
final first = service.migrateTask(fixture('task_v0_full.json'));
|
||||||
|
final second = service.migrateTask(first.document!);
|
||||||
|
|
||||||
|
expect(first.isSuccess, isTrue);
|
||||||
|
expect(second.isSuccess, isTrue);
|
||||||
|
expect(second.report.status, DocumentMigrationStatus.alreadyCurrent);
|
||||||
|
expect(second.document, first.document);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('partial corruption produces a typed failure report before writes',
|
||||||
|
() {
|
||||||
|
final legacy = fixture('task_v0_malformed_missing_title.json');
|
||||||
|
|
||||||
|
final result = service.migrateTask(legacy);
|
||||||
|
|
||||||
|
expect(result.isSuccess, isFalse);
|
||||||
|
expect(result.shouldWrite, isFalse);
|
||||||
|
expect(result.document, isNull);
|
||||||
|
expect(result.report.status, DocumentMigrationStatus.failed);
|
||||||
|
expect(
|
||||||
|
result.report.failureCode,
|
||||||
|
DocumentMigrationFailureCode.legacyDocumentFailure,
|
||||||
|
);
|
||||||
|
expect(result.report.fieldName, TaskDocumentFields.title);
|
||||||
|
expect(result.report.detailCode, 'missingField');
|
||||||
|
expect(legacy.containsKey(DocumentFields.schemaVersion), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ambiguous legacy date-times fail instead of guessing a zone', () {
|
||||||
|
final result = service.migrateTask(
|
||||||
|
fixture('task_v0_ambiguous_datetime.json'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.isSuccess, isFalse);
|
||||||
|
expect(result.shouldWrite, isFalse);
|
||||||
|
expect(result.document, isNull);
|
||||||
|
expect(
|
||||||
|
result.report.failureCode,
|
||||||
|
DocumentMigrationFailureCode.legacyDocumentFailure,
|
||||||
|
);
|
||||||
|
expect(result.report.fieldName, TaskDocumentFields.createdAt);
|
||||||
|
expect(result.report.detailCode, 'ambiguousDateTime');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unsupported future schema versions fail closed', () {
|
||||||
|
final result = service.migrateTask(fixture('task_future_schema.json'));
|
||||||
|
|
||||||
|
expect(result.isSuccess, isFalse);
|
||||||
|
expect(result.shouldWrite, isFalse);
|
||||||
|
expect(result.document, isNull);
|
||||||
|
expect(
|
||||||
|
result.report.failureCode,
|
||||||
|
DocumentMigrationFailureCode.unsupportedSchemaVersion,
|
||||||
|
);
|
||||||
|
expect(result.report.fromSchemaVersion, 99);
|
||||||
|
expect(result.report.fieldName, DocumentFields.schemaVersion);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object?> fixture(String name) {
|
||||||
|
final file = File('test/fixtures/migration/$name');
|
||||||
|
final decoded = jsonDecode(file.readAsStringSync()) as Map<String, dynamic>;
|
||||||
|
return Map<String, Object?>.from(decoded);
|
||||||
|
}
|
||||||
23
test/fixtures/migration/locked_block_v0.json
vendored
Normal file
23
test/fixtures/migration/locked_block_v0.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"_id": "locked-work",
|
||||||
|
"name": "Work",
|
||||||
|
"startTime": {
|
||||||
|
"hour": 9,
|
||||||
|
"minute": 0
|
||||||
|
},
|
||||||
|
"endTime": {
|
||||||
|
"hour": 17,
|
||||||
|
"minute": 0
|
||||||
|
},
|
||||||
|
"date": null,
|
||||||
|
"recurrence": {
|
||||||
|
"weekdays": [
|
||||||
|
"monday",
|
||||||
|
"thursday"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hiddenByDefault": true,
|
||||||
|
"projectId": "project-v0-home",
|
||||||
|
"createdAt": "2026-06-01T12:00:00.000Z",
|
||||||
|
"updatedAt": "2026-06-02T12:00:00.000Z"
|
||||||
|
}
|
||||||
18
test/fixtures/migration/locked_override_v0.json
vendored
Normal file
18
test/fixtures/migration/locked_override_v0.json
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"_id": "locked-override-short-work",
|
||||||
|
"lockedBlockId": "locked-work",
|
||||||
|
"date": "2026-06-25",
|
||||||
|
"type": "replace",
|
||||||
|
"name": "Short work",
|
||||||
|
"startTime": {
|
||||||
|
"value": "10:00"
|
||||||
|
},
|
||||||
|
"endTime": {
|
||||||
|
"hour": 15,
|
||||||
|
"minute": 0
|
||||||
|
},
|
||||||
|
"hiddenByDefault": true,
|
||||||
|
"projectId": "project-v0-home",
|
||||||
|
"createdAt": "2026-06-20T12:00:00.000Z",
|
||||||
|
"updatedAt": "2026-06-20T12:05:00.000Z"
|
||||||
|
}
|
||||||
10
test/fixtures/migration/project_v0.json
vendored
Normal file
10
test/fixtures/migration/project_v0.json
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"_id": "project-v0-home",
|
||||||
|
"name": "Home",
|
||||||
|
"colorKey": "home-blue",
|
||||||
|
"defaultPriority": "veryHigh",
|
||||||
|
"defaultReward": "notSet",
|
||||||
|
"defaultDifficulty": "veryEasy",
|
||||||
|
"defaultReminderProfile": "gentle",
|
||||||
|
"defaultDurationMinutes": 25
|
||||||
|
}
|
||||||
6
test/fixtures/migration/task_future_schema.json
vendored
Normal file
6
test/fixtures/migration/task_future_schema.json
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"schemaVersion": 99,
|
||||||
|
"_id": "task-future",
|
||||||
|
"ownerId": "owner-1",
|
||||||
|
"revision": 1
|
||||||
|
}
|
||||||
36
test/fixtures/migration/task_v0_ambiguous_datetime.json
vendored
Normal file
36
test/fixtures/migration/task_v0_ambiguous_datetime.json
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"_id": "task-v0-ambiguous-datetime",
|
||||||
|
"title": "Ambiguous datetime task",
|
||||||
|
"projectId": "inbox",
|
||||||
|
"type": "flexible",
|
||||||
|
"status": "backlog",
|
||||||
|
"priority": null,
|
||||||
|
"reward": "notSet",
|
||||||
|
"difficulty": "notSet",
|
||||||
|
"durationMinutes": null,
|
||||||
|
"scheduledStart": null,
|
||||||
|
"scheduledEnd": null,
|
||||||
|
"actualStart": null,
|
||||||
|
"actualEnd": null,
|
||||||
|
"completedAt": null,
|
||||||
|
"parentTaskId": null,
|
||||||
|
"backlogTags": [],
|
||||||
|
"reminderOverride": null,
|
||||||
|
"createdAt": "2026-06-20T09:15:00",
|
||||||
|
"updatedAt": "2026-06-20T09:15:00.000Z",
|
||||||
|
"stats": {
|
||||||
|
"skippedDuringBurnoutCount": 0,
|
||||||
|
"manuallyPushedCount": 0,
|
||||||
|
"autoPushedCount": 0,
|
||||||
|
"movedToBacklogCount": 0,
|
||||||
|
"restoredFromBacklogCount": 0,
|
||||||
|
"missedCount": 0,
|
||||||
|
"cancelledCount": 0,
|
||||||
|
"completedLateCount": 0,
|
||||||
|
"completedDuringLockedHoursCount": 0,
|
||||||
|
"completedDuringLockedHoursMinutes": 0,
|
||||||
|
"completedAfterShieldCount": 0,
|
||||||
|
"completedAfterPushCount": 0,
|
||||||
|
"totalPushesBeforeCompletion": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
38
test/fixtures/migration/task_v0_full.json
vendored
Normal file
38
test/fixtures/migration/task_v0_full.json
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"_id": "task-v0-full",
|
||||||
|
"title": "Legacy full task",
|
||||||
|
"projectId": "project-v0-home",
|
||||||
|
"type": "freeSlot",
|
||||||
|
"status": "noLongerRelevant",
|
||||||
|
"priority": "veryHigh",
|
||||||
|
"reward": "veryLow",
|
||||||
|
"difficulty": "veryHard",
|
||||||
|
"durationMinutes": 45,
|
||||||
|
"scheduledStart": "2026-06-23T16:00:00.000Z",
|
||||||
|
"scheduledEnd": "2026-06-23T16:45:00.000Z",
|
||||||
|
"actualStart": "2026-06-23T16:05:00.000Z",
|
||||||
|
"actualEnd": "2026-06-23T16:35:00.000Z",
|
||||||
|
"completedAt": "2026-06-23T16:35:00.000Z",
|
||||||
|
"parentTaskId": "parent-task",
|
||||||
|
"backlogTags": [
|
||||||
|
"wishlist"
|
||||||
|
],
|
||||||
|
"reminderOverride": "persistent",
|
||||||
|
"createdAt": "2026-06-21T08:00:00.000Z",
|
||||||
|
"updatedAt": "2026-06-23T17:00:00.000Z",
|
||||||
|
"stats": {
|
||||||
|
"skippedDuringBurnoutCount": 1,
|
||||||
|
"manuallyPushedCount": 2,
|
||||||
|
"autoPushedCount": 3,
|
||||||
|
"movedToBacklogCount": 4,
|
||||||
|
"restoredFromBacklogCount": 5,
|
||||||
|
"missedCount": 6,
|
||||||
|
"cancelledCount": 7,
|
||||||
|
"completedLateCount": 8,
|
||||||
|
"completedDuringLockedHoursCount": 9,
|
||||||
|
"completedDuringLockedHoursMinutes": 10,
|
||||||
|
"completedAfterShieldCount": 11,
|
||||||
|
"completedAfterPushCount": 12,
|
||||||
|
"totalPushesBeforeCompletion": 13
|
||||||
|
}
|
||||||
|
}
|
||||||
35
test/fixtures/migration/task_v0_malformed_missing_title.json
vendored
Normal file
35
test/fixtures/migration/task_v0_malformed_missing_title.json
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"_id": "task-v0-missing-title",
|
||||||
|
"projectId": "inbox",
|
||||||
|
"type": "flexible",
|
||||||
|
"status": "backlog",
|
||||||
|
"priority": null,
|
||||||
|
"reward": "notSet",
|
||||||
|
"difficulty": "notSet",
|
||||||
|
"durationMinutes": null,
|
||||||
|
"scheduledStart": null,
|
||||||
|
"scheduledEnd": null,
|
||||||
|
"actualStart": null,
|
||||||
|
"actualEnd": null,
|
||||||
|
"completedAt": null,
|
||||||
|
"parentTaskId": null,
|
||||||
|
"backlogTags": [],
|
||||||
|
"reminderOverride": null,
|
||||||
|
"createdAt": "2026-06-20T09:15:00.000Z",
|
||||||
|
"updatedAt": "2026-06-20T09:15:00.000Z",
|
||||||
|
"stats": {
|
||||||
|
"skippedDuringBurnoutCount": 0,
|
||||||
|
"manuallyPushedCount": 0,
|
||||||
|
"autoPushedCount": 0,
|
||||||
|
"movedToBacklogCount": 0,
|
||||||
|
"restoredFromBacklogCount": 0,
|
||||||
|
"missedCount": 0,
|
||||||
|
"cancelledCount": 0,
|
||||||
|
"completedLateCount": 0,
|
||||||
|
"completedDuringLockedHoursCount": 0,
|
||||||
|
"completedDuringLockedHoursMinutes": 0,
|
||||||
|
"completedAfterShieldCount": 0,
|
||||||
|
"completedAfterPushCount": 0,
|
||||||
|
"totalPushesBeforeCompletion": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
36
test/fixtures/migration/task_v0_minimum.json
vendored
Normal file
36
test/fixtures/migration/task_v0_minimum.json
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"_id": "task-v0-minimum",
|
||||||
|
"title": "Review inbox note",
|
||||||
|
"projectId": "inbox",
|
||||||
|
"type": "flexible",
|
||||||
|
"status": "backlog",
|
||||||
|
"priority": null,
|
||||||
|
"reward": "notSet",
|
||||||
|
"difficulty": "notSet",
|
||||||
|
"durationMinutes": null,
|
||||||
|
"scheduledStart": null,
|
||||||
|
"scheduledEnd": null,
|
||||||
|
"actualStart": null,
|
||||||
|
"actualEnd": null,
|
||||||
|
"completedAt": null,
|
||||||
|
"parentTaskId": null,
|
||||||
|
"backlogTags": [],
|
||||||
|
"reminderOverride": null,
|
||||||
|
"createdAt": "2026-06-20T09:15:00.000Z",
|
||||||
|
"updatedAt": "2026-06-20T09:15:00.000Z",
|
||||||
|
"stats": {
|
||||||
|
"skippedDuringBurnoutCount": 0,
|
||||||
|
"manuallyPushedCount": 0,
|
||||||
|
"autoPushedCount": 0,
|
||||||
|
"movedToBacklogCount": 0,
|
||||||
|
"restoredFromBacklogCount": 0,
|
||||||
|
"missedCount": 0,
|
||||||
|
"cancelledCount": 0,
|
||||||
|
"completedLateCount": 0,
|
||||||
|
"completedDuringLockedHoursCount": 0,
|
||||||
|
"completedDuringLockedHoursMinutes": 0,
|
||||||
|
"completedAfterShieldCount": 0,
|
||||||
|
"completedAfterPushCount": 0,
|
||||||
|
"totalPushesBeforeCompletion": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue