focus-flow/test/persistence_index_contract_test.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);
}