168 lines
6.1 KiB
Dart
168 lines
6.1 KiB
Dart
import 'package:adhd_scheduler_core/scheduler_core.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
void main() {
|
|
group('Persistence index contract', () {
|
|
test('index names and key fields are stable and adapter-neutral', () {
|
|
final names = <String>{};
|
|
|
|
for (final spec in PersistenceIndexCatalog.all) {
|
|
expect(spec.name, isNotEmpty);
|
|
expect(spec.name.trim(), spec.name);
|
|
expect(spec.name.contains(' '), isFalse);
|
|
expect(names.add(spec.name), isTrue, reason: spec.name);
|
|
expect(spec.collection, isNotEmpty);
|
|
expect(spec.keys, isNotEmpty);
|
|
expect(spec.keys.first.fieldName, DocumentFields.ownerId);
|
|
for (final key in spec.keys) {
|
|
expect(key.fieldName, isNotEmpty);
|
|
expect(key.fieldName.contains('.'), isFalse);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('every repository query has an index-backed access path', () {
|
|
const requiredQueries = {
|
|
RepositoryQueryNames.taskById,
|
|
RepositoryQueryNames.taskByOwner,
|
|
RepositoryQueryNames.taskByStatus,
|
|
RepositoryQueryNames.taskByProject,
|
|
RepositoryQueryNames.taskByParent,
|
|
RepositoryQueryNames.taskBacklogCandidates,
|
|
RepositoryQueryNames.taskScheduledWindow,
|
|
RepositoryQueryNames.taskLocalDay,
|
|
RepositoryQueryNames.projectByOwner,
|
|
RepositoryQueryNames.projectById,
|
|
RepositoryQueryNames.lockedBlockByOwner,
|
|
RepositoryQueryNames.lockedBlockById,
|
|
RepositoryQueryNames.lockedOverrideByDate,
|
|
RepositoryQueryNames.taskActivityByOwner,
|
|
RepositoryQueryNames.taskActivityByOperation,
|
|
RepositoryQueryNames.taskActivityByTask,
|
|
RepositoryQueryNames.taskActivityByProject,
|
|
RepositoryQueryNames.projectStatisticsByProject,
|
|
RepositoryQueryNames.ownerSettingsByOwner,
|
|
RepositoryQueryNames.noticeAcknowledgementByOwner,
|
|
RepositoryQueryNames.noticeAcknowledgementById,
|
|
RepositoryQueryNames.operationByIdempotencyKey,
|
|
RepositoryQueryNames.schedulingSnapshotById,
|
|
RepositoryQueryNames.schedulingSnapshotWindow,
|
|
};
|
|
|
|
expect(PersistenceIndexCatalog.supportedQueries,
|
|
containsAll(requiredQueries));
|
|
});
|
|
|
|
test('uniqueness and archive/partial index requirements are explicit', () {
|
|
expect(
|
|
uniqueIndexNames,
|
|
containsAll({
|
|
'tasks_owner_id_unique',
|
|
'projects_owner_id_unique',
|
|
'project_stats_owner_project_unique',
|
|
'locked_blocks_owner_id_unique',
|
|
'locked_overrides_owner_id_unique',
|
|
'task_activities_owner_id_unique',
|
|
'owner_settings_owner_unique',
|
|
'notice_ack_owner_notice_unique',
|
|
'operations_owner_operation_unique',
|
|
'snapshots_owner_id_unique',
|
|
}),
|
|
);
|
|
|
|
expect(
|
|
indexByName('projects_owner_archived_name').keys.map(
|
|
(key) => key.fieldName,
|
|
),
|
|
contains(ProjectDocumentFields.archivedAt));
|
|
expect(
|
|
indexByName('locked_blocks_owner_archived_name').keys.map(
|
|
(key) => key.fieldName,
|
|
),
|
|
contains(LockedBlockDocumentFields.archivedAt));
|
|
expect(
|
|
indexByName('tasks_owner_scheduled_window').partialFilterFields,
|
|
containsAll({
|
|
TaskDocumentFields.scheduledStart,
|
|
TaskDocumentFields.scheduledEnd,
|
|
}),
|
|
);
|
|
expect(
|
|
indexByName('tasks_owner_parent_status').partialFilterFields,
|
|
contains(TaskDocumentFields.parentTaskId),
|
|
);
|
|
});
|
|
|
|
test('collection retention does not target authoritative state', () {
|
|
expect(
|
|
PersistenceCollections.authoritative.intersection(
|
|
PersistenceCollections.internalRetained,
|
|
),
|
|
isEmpty,
|
|
);
|
|
expect(PersistencePayloadLimits.taskActivityRetention.inDays, 365);
|
|
expect(PersistencePayloadLimits.applicationOperationRetention.inDays, 90);
|
|
expect(PersistencePayloadLimits.schedulingSnapshotRetention.inDays, 30);
|
|
});
|
|
|
|
test('payload guards reject unbounded embedded growth', () {
|
|
final metadata = {
|
|
for (var index = 0;
|
|
index <= PersistencePayloadLimits.maxTaskActivityMetadataEntries;
|
|
index++)
|
|
'key-$index': index,
|
|
};
|
|
final taskIds = List<Object?>.generate(
|
|
PersistencePayloadLimits.maxSchedulingSnapshotTasks + 1,
|
|
(index) => 'task-$index',
|
|
);
|
|
final appliedActivityIds = List<Object?>.generate(
|
|
PersistencePayloadLimits.maxAppliedActivityIds + 1,
|
|
(index) => 'activity-$index',
|
|
);
|
|
|
|
final metadataResult = PersistencePayloadGuard.mapEntries(
|
|
fieldName: TaskActivityDocumentFields.metadata,
|
|
value: metadata,
|
|
limit: PersistencePayloadLimits.maxTaskActivityMetadataEntries,
|
|
);
|
|
final snapshotResult = PersistencePayloadGuard.listLength(
|
|
fieldName: SchedulingSnapshotDocumentFields.tasks,
|
|
value: taskIds,
|
|
limit: PersistencePayloadLimits.maxSchedulingSnapshotTasks,
|
|
);
|
|
final activityIdsResult = PersistencePayloadGuard.listLength(
|
|
fieldName: ProjectStatisticsDocumentFields.appliedActivityIds,
|
|
value: appliedActivityIds,
|
|
limit: PersistencePayloadLimits.maxAppliedActivityIds,
|
|
);
|
|
|
|
expect(metadataResult.isValid, isFalse);
|
|
expect(metadataResult.actual, metadata.length);
|
|
expect(snapshotResult.isValid, isFalse);
|
|
expect(snapshotResult.fieldName, SchedulingSnapshotDocumentFields.tasks);
|
|
expect(activityIdsResult.isValid, isFalse);
|
|
expect(activityIdsResult.limit,
|
|
PersistencePayloadLimits.maxAppliedActivityIds);
|
|
expect(
|
|
PersistencePayloadGuard.listLength(
|
|
fieldName: SchedulingSnapshotDocumentFields.notices,
|
|
value: const <Object?>[],
|
|
limit: PersistencePayloadLimits.maxSchedulingSnapshotNotices,
|
|
).isValid,
|
|
isTrue,
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
Set<String> get uniqueIndexNames {
|
|
return {
|
|
for (final spec in PersistenceIndexCatalog.all)
|
|
if (spec.unique) spec.name,
|
|
};
|
|
}
|
|
|
|
PersistenceIndexSpec indexByName(String name) {
|
|
return PersistenceIndexCatalog.all.singleWhere((spec) => spec.name == name);
|
|
}
|