feat: add local-first private AI digest workflow
Migrate app code into canonical feature slices, add phone-only AI digest scheduling and review, wire local notification/background task support, and cover the flow with tests.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:relationship_saver/app/data/relationship_repository.dart';
|
||||
import 'package:relationship_saver/app/data/storage/local_data_store_in_memory.dart';
|
||||
import 'package:relationship_saver/app/data/storage/local_data_store_provider.dart';
|
||||
import 'package:relationship_saver/app/state/local_data_state.dart';
|
||||
import 'package:relationship_saver/features/ai_digest/domain/ai_digest_models.dart';
|
||||
import 'package:relationship_saver/features/ideas/domain/idea_models.dart';
|
||||
import 'package:relationship_saver/features/reminders/application/reminder_scheduler.dart';
|
||||
import 'package:relationship_saver/features/reminders/application/reminder_scheduler_provider.dart';
|
||||
import 'package:relationship_saver/features/sync/data/storage/sync_state_store_in_memory.dart';
|
||||
import 'package:relationship_saver/features/sync/data/storage/sync_state_store_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
});
|
||||
|
||||
test('accepting gift draft creates idea and marks draft accepted', () async {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final LocalDataState initial = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
final String personId = initial.people.first.id;
|
||||
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.saveAiSuggestionDrafts(<AiSuggestionDraft>[
|
||||
AiSuggestionDraft(
|
||||
id: 'ai-1',
|
||||
personId: personId,
|
||||
kind: AiSuggestionKind.giftIdea,
|
||||
title: 'Coffee sampler',
|
||||
details: 'Small local roaster pack.',
|
||||
suggestedAt: DateTime(2026, 5, 16),
|
||||
confidence: 0.8,
|
||||
status: AiSuggestionStatus.pending,
|
||||
sourceRunId: 'run-1',
|
||||
),
|
||||
]);
|
||||
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.acceptAiSuggestionDraft('ai-1');
|
||||
|
||||
final LocalDataState after = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
expect(after.aiSuggestionDrafts.single.status, AiSuggestionStatus.accepted);
|
||||
expect(
|
||||
after.ideas.any(
|
||||
(RelationshipIdea idea) =>
|
||||
idea.personId == personId &&
|
||||
idea.type == IdeaType.gift &&
|
||||
idea.title == 'Coffee sampler',
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('dismissing draft does not create local artifacts', () async {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final LocalDataState initial = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
final String personId = initial.people.first.id;
|
||||
final int ideaCount = initial.ideas.length;
|
||||
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.saveAiSuggestionDrafts(<AiSuggestionDraft>[
|
||||
AiSuggestionDraft(
|
||||
id: 'ai-2',
|
||||
personId: personId,
|
||||
kind: AiSuggestionKind.eventIdea,
|
||||
title: 'Museum afternoon',
|
||||
details: 'Pick a quiet exhibit.',
|
||||
suggestedAt: DateTime(2026, 5, 16),
|
||||
confidence: 0.7,
|
||||
status: AiSuggestionStatus.pending,
|
||||
sourceRunId: 'run-1',
|
||||
),
|
||||
]);
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.dismissAiSuggestionDraft('ai-2');
|
||||
|
||||
final LocalDataState after = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
expect(
|
||||
after.aiSuggestionDrafts.single.status,
|
||||
AiSuggestionStatus.dismissed,
|
||||
);
|
||||
expect(after.ideas.length, ideaCount);
|
||||
});
|
||||
}
|
||||
|
||||
ProviderContainer _createContainer() {
|
||||
return ProviderContainer(
|
||||
overrides: [
|
||||
localDataStoreProvider.overrideWithValue(InMemoryLocalDataStore()),
|
||||
syncStateStoreProvider.overrideWithValue(InMemorySyncStateStore()),
|
||||
reminderSchedulerProvider.overrideWithValue(
|
||||
const NoopReminderScheduler(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user