Refine backendless share intake
This commit is contained in:
@@ -21,10 +21,7 @@ void main() {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final LocalDataState initial = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
final String personId = initial.people.first.id;
|
||||
final String personId = await _addTestPerson(container);
|
||||
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
@@ -65,10 +62,10 @@ void main() {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final String personId = await _addTestPerson(container);
|
||||
final LocalDataState initial = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
final String personId = initial.people.first.id;
|
||||
final int ideaCount = initial.ideas.length;
|
||||
|
||||
await container
|
||||
@@ -112,3 +109,19 @@ ProviderContainer _createContainer() {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _addTestPerson(ProviderContainer container) async {
|
||||
await container.read(localRepositoryProvider.future);
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.addPerson(
|
||||
name: 'Taylor Quinn',
|
||||
relationship: 'Friend',
|
||||
notes: '',
|
||||
tags: const <String>[],
|
||||
);
|
||||
final LocalDataState state = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
return state.people.single.id;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:relationship_saver/app/state/local_data_state.dart';
|
||||
import 'package:relationship_saver/features/ai_digest/application/anonymized_llm_context_builder.dart';
|
||||
import 'package:relationship_saver/features/ai_digest/domain/ai_digest_models.dart';
|
||||
import 'package:relationship_saver/features/dashboard/domain/dashboard_models.dart';
|
||||
import 'package:relationship_saver/features/ideas/domain/idea_models.dart';
|
||||
import 'package:relationship_saver/features/moments/domain/moment_models.dart';
|
||||
@@ -73,6 +74,19 @@ void main() {
|
||||
isSensitive: true,
|
||||
),
|
||||
],
|
||||
aiSuggestionDrafts: <AiSuggestionDraft>[
|
||||
AiSuggestionDraft(
|
||||
id: 'ai-1',
|
||||
personId: 'p-1',
|
||||
kind: AiSuggestionKind.eventIdea,
|
||||
title: 'Jazz concert downtown',
|
||||
details: 'Should not need to repeat the details.',
|
||||
suggestedAt: DateTime(2026, 5, 10),
|
||||
confidence: 0.8,
|
||||
status: AiSuggestionStatus.dismissed,
|
||||
sourceRunId: 'digest-old',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final AnonymizedLlmDigestContext context = AnonymizedLlmContextBuilder(
|
||||
@@ -90,5 +104,9 @@ void main() {
|
||||
expect(prompt, isNot(contains('thread-secret')));
|
||||
expect(prompt, isNot(contains('Raw private chat text')));
|
||||
expect(prompt, isNot(contains('Secret medical detail')));
|
||||
expect(prompt, contains('priorSuggestions'));
|
||||
expect(prompt, contains('jazz concert downtown'));
|
||||
expect(prompt, contains('dismissed'));
|
||||
expect(prompt, isNot(contains('Should not need to repeat the details.')));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,6 +48,15 @@ void main() {
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(localRepositoryProvider.future);
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.addPerson(
|
||||
name: 'Taylor Quinn',
|
||||
relationship: 'Friend',
|
||||
notes: '',
|
||||
tags: const <String>['tea'],
|
||||
);
|
||||
await container.read(llmDigestConfigProvider.notifier).setEnabled(true);
|
||||
|
||||
final LlmDigestRunResult result = await container
|
||||
|
||||
@@ -40,7 +40,7 @@ void main() {
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Sign In'));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 450));
|
||||
|
||||
expect(find.text('Dashboard'), findsWidgets);
|
||||
expect(find.text('Capture'), findsWidgets);
|
||||
|
||||
await tester.tap(find.text('Settings').first);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 450));
|
||||
|
||||
@@ -47,7 +47,7 @@ void main() {
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Sign In'));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
|
||||
expect(find.text('Dashboard'), findsWidgets);
|
||||
expect(find.text('Capture'), findsWidgets);
|
||||
|
||||
await tester.tap(find.text('People').last);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
@@ -91,13 +91,5 @@ void main() {
|
||||
}),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
await tester.tap(find.byTooltip('More'));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
await tester.tap(find.text('Sync').last);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
|
||||
expect(find.text('Queue Status'), findsOneWidget);
|
||||
expect(find.text('Pending changes'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,13 +25,23 @@ void main() {
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(localRepositoryProvider.future);
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.addPerson(
|
||||
name: 'Ava Hart',
|
||||
relationship: 'Friend',
|
||||
notes: '',
|
||||
tags: const <String>[],
|
||||
);
|
||||
final LocalDataState seeded = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
final String personId = seeded.people.first.id;
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.upsertInferredPreferenceSignalObservation(
|
||||
personId: seeded.people.first.id,
|
||||
personId: personId,
|
||||
key: 'drink:tea',
|
||||
label: 'Tea',
|
||||
category: 'drink',
|
||||
@@ -58,7 +68,7 @@ void main() {
|
||||
expect(find.text('Explore'), findsOneWidget);
|
||||
|
||||
final Finder avaNode = find.byKey(
|
||||
const ValueKey<String>('graph-node-p-ava'),
|
||||
ValueKey<String>('graph-node-$personId'),
|
||||
);
|
||||
expect(avaNode, findsOneWidget);
|
||||
|
||||
|
||||
@@ -17,15 +17,17 @@ void main() {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
});
|
||||
|
||||
test('seeds local data on first load', () async {
|
||||
test('starts with empty local data on first load', () async {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final state = await container.read(localRepositoryProvider.future);
|
||||
|
||||
expect(state.people, isNotEmpty);
|
||||
expect(state.moments, isNotEmpty);
|
||||
expect(state.tasks, isNotEmpty);
|
||||
expect(state.people, isEmpty);
|
||||
expect(state.moments, isEmpty);
|
||||
expect(state.ideas, isEmpty);
|
||||
expect(state.reminders, isEmpty);
|
||||
expect(state.tasks, isEmpty);
|
||||
});
|
||||
|
||||
test('adds and removes person with persistence state update', () async {
|
||||
@@ -62,8 +64,8 @@ void main() {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final String personId = await _addTestPerson(container);
|
||||
final state = await container.read(localRepositoryProvider.future);
|
||||
final String personId = state.people.first.id;
|
||||
final int beforeCount = state.moments.length;
|
||||
|
||||
await container
|
||||
@@ -86,10 +88,7 @@ void main() {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final LocalDataState state = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
final String personId = state.people.first.id;
|
||||
final String personId = await _addTestPerson(container);
|
||||
|
||||
final PersonPreferenceSignal first = await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
@@ -161,7 +160,7 @@ void main() {
|
||||
});
|
||||
|
||||
test(
|
||||
'ingestSharedMessage auto-creates profile and reuses source link for follow-up messages',
|
||||
'ingestSharedMessage queues unknown identity, then reuses source link after manual creation',
|
||||
() async {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
@@ -184,22 +183,35 @@ void main() {
|
||||
final LocalDataState afterFirst = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
expect(first.createdProfile, isTrue);
|
||||
expect(afterFirst.people.length, before.people.length + 1);
|
||||
final SourceProfileLink createdLink = afterFirst.sourceLinks.firstWhere(
|
||||
(SourceProfileLink link) => link.profileId == first.profileId,
|
||||
expect(first.isQueuedForResolution, isTrue);
|
||||
expect(first.createdProfile, isFalse);
|
||||
expect(afterFirst.people.length, before.people.length);
|
||||
expect(
|
||||
afterFirst.sharedInbox.single.reason,
|
||||
SharedInboxReason.missingIdentity,
|
||||
);
|
||||
|
||||
final SharedMessageIngestResult created = await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.resolveSharedInboxByCreatingProfile(
|
||||
inboxEntryId: first.inboxEntryId!,
|
||||
name: 'Taylor Quinn',
|
||||
relationship: 'Friend',
|
||||
);
|
||||
|
||||
final LocalDataState afterCreate = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
expect(created.createdProfile, isTrue);
|
||||
expect(afterCreate.people.length, before.people.length + 1);
|
||||
final SourceProfileLink createdLink = afterCreate.sourceLinks.firstWhere(
|
||||
(SourceProfileLink link) => link.profileId == created.profileId,
|
||||
);
|
||||
expect(createdLink.sourceFingerprint, 'whatsapp|u:wa:taylor quinn');
|
||||
expect(
|
||||
afterFirst.sourceLinks.any(
|
||||
(SourceProfileLink link) => link.profileId == first.profileId,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
afterFirst.moments.any(
|
||||
afterCreate.moments.any(
|
||||
(RelationshipMoment moment) =>
|
||||
moment.personId == first.profileId && moment.type == 'whatsapp',
|
||||
moment.personId == created.profileId && moment.type == 'whatsapp',
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
@@ -219,11 +231,11 @@ void main() {
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
expect(second.createdProfile, isFalse);
|
||||
expect(second.profileId, first.profileId);
|
||||
expect(afterSecond.people.length, afterFirst.people.length);
|
||||
expect(second.profileId, created.profileId);
|
||||
expect(afterSecond.people.length, afterCreate.people.length);
|
||||
expect(
|
||||
afterSecond.sharedMessages.length,
|
||||
afterFirst.sharedMessages.length + 1,
|
||||
afterCreate.sharedMessages.length + 1,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -234,7 +246,7 @@ void main() {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(localRepositoryProvider.future);
|
||||
await _addTestPerson(container, name: 'Nora Diaz');
|
||||
|
||||
final SharedMessageIngestResult result = await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
@@ -285,10 +297,7 @@ void main() {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final LocalDataState state = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
final String personId = state.people.first.id;
|
||||
final String personId = await _addTestPerson(container);
|
||||
final SharedPayload payload = SharePayloadParser.parse(
|
||||
rawText: 'She loves vegan sushi and rooftop dinners.',
|
||||
sourceApp: 'share_sheet',
|
||||
@@ -378,6 +387,35 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'captureSharedPayloadByCreatingProfile requires an explicit person name',
|
||||
() async {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(localRepositoryProvider.future);
|
||||
final SharedPayload payload = SharePayloadParser.parse(
|
||||
rawText: 'Alex: Kaffee klingt gut',
|
||||
sourceApp: 'whatsapp',
|
||||
platform: 'ios',
|
||||
);
|
||||
|
||||
expect(payload.sourceDisplayName, 'Alex');
|
||||
expect(
|
||||
() => container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.captureSharedPayloadByCreatingProfile(payload: payload),
|
||||
throwsArgumentError,
|
||||
);
|
||||
|
||||
final LocalDataState after = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
expect(after.people, isEmpty);
|
||||
expect(after.sharedMessages, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'queues near-match conflict and then uses fingerprint mapping for follow-up shares',
|
||||
() async {
|
||||
@@ -689,10 +727,10 @@ void main() {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final String personId = await _addTestPerson(container);
|
||||
final LocalDataState state = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
final String personId = state.people.first.id;
|
||||
final int beforeCount = state.ideas.length;
|
||||
|
||||
await container
|
||||
@@ -740,10 +778,10 @@ void main() {
|
||||
final ProviderContainer container = _createContainer();
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final String personId = await _addTestPerson(container);
|
||||
final LocalDataState state = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
final String personId = state.people.first.id;
|
||||
final int beforeCount = state.reminders.length;
|
||||
|
||||
await container
|
||||
@@ -852,6 +890,28 @@ ProviderContainer _createContainer({ReminderScheduler? scheduler}) {
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _addTestPerson(
|
||||
ProviderContainer container, {
|
||||
String name = 'Test Person',
|
||||
String relationship = 'Friend',
|
||||
}) async {
|
||||
await container.read(localRepositoryProvider.future);
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.addPerson(
|
||||
name: name,
|
||||
relationship: relationship,
|
||||
notes: '',
|
||||
tags: const <String>[],
|
||||
);
|
||||
final LocalDataState state = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
return state.people
|
||||
.firstWhere((PersonProfile person) => person.name == name)
|
||||
.id;
|
||||
}
|
||||
|
||||
class RecordingReminderScheduler implements ReminderScheduler {
|
||||
final List<List<ReminderRule>> snapshots = <List<ReminderRule>>[];
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ void main() {
|
||||
addTearDown(container.dispose);
|
||||
|
||||
final initial = await container.read(localRepositoryProvider.future);
|
||||
expect(initial.people, isNotEmpty);
|
||||
expect(initial.people, isEmpty);
|
||||
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
|
||||
@@ -30,6 +30,15 @@ void main() {
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(localRepositoryProvider.future);
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.addPerson(
|
||||
name: 'Taylor Quinn',
|
||||
relationship: 'Friend',
|
||||
notes: '',
|
||||
tags: const <String>[],
|
||||
);
|
||||
final LocalDataState initial = await container.read(
|
||||
localRepositoryProvider.future,
|
||||
);
|
||||
|
||||
@@ -65,7 +65,7 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Share Inbox'), findsOneWidget);
|
||||
expect(find.text('Capture'), findsOneWidget);
|
||||
expect(find.text('Ambiguous match'), findsOneWidget);
|
||||
expect(find.widgetWithText(FilledButton, 'Review & Save'), findsOneWidget);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'package:relationship_saver/features/sync/sync_coordinator.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'triggers sync when reachability transitions from offline to online',
|
||||
'does not trigger sync when background sync is disabled by default',
|
||||
(WidgetTester tester) async {
|
||||
final _TestReachability reachability = _TestReachability(initial: false);
|
||||
addTearDown(reachability.dispose);
|
||||
@@ -39,56 +39,56 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 60));
|
||||
|
||||
// Startup trigger always runs once when background sync is enabled.
|
||||
expect(syncCalls, 1);
|
||||
expect(syncCalls, 0);
|
||||
|
||||
reachability.emit(false);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 40));
|
||||
expect(syncCalls, 1);
|
||||
expect(syncCalls, 0);
|
||||
|
||||
reachability.emit(true);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 60));
|
||||
expect(syncCalls, 2);
|
||||
expect(syncCalls, 0);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('does not trigger extra sync while connectivity stays online', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final _TestReachability reachability = _TestReachability(initial: true);
|
||||
addTearDown(reachability.dispose);
|
||||
testWidgets(
|
||||
'does not trigger sync while connectivity stays online by default',
|
||||
(WidgetTester tester) async {
|
||||
final _TestReachability reachability = _TestReachability(initial: true);
|
||||
addTearDown(reachability.dispose);
|
||||
|
||||
int syncCalls = 0;
|
||||
int syncCalls = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
networkReachabilityProvider.overrideWithValue(reachability),
|
||||
syncCoordinatorProvider.overrideWith(
|
||||
(Ref ref) => _TestSyncCoordinator(
|
||||
ref,
|
||||
onSyncNow: () async {
|
||||
syncCalls += 1;
|
||||
return _okResult();
|
||||
},
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
networkReachabilityProvider.overrideWithValue(reachability),
|
||||
syncCoordinatorProvider.overrideWith(
|
||||
(Ref ref) => _TestSyncCoordinator(
|
||||
ref,
|
||||
onSyncNow: () async {
|
||||
syncCalls += 1;
|
||||
return _okResult();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
child: const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: SyncBackgroundRunner(child: SizedBox()),
|
||||
),
|
||||
],
|
||||
child: const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: SyncBackgroundRunner(child: SizedBox()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 60));
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 60));
|
||||
|
||||
expect(syncCalls, 1);
|
||||
expect(syncCalls, 0);
|
||||
|
||||
reachability.emit(true);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 60));
|
||||
reachability.emit(true);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 60));
|
||||
|
||||
expect(syncCalls, 1);
|
||||
});
|
||||
expect(syncCalls, 0);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _TestSyncCoordinator extends SyncCoordinator {
|
||||
|
||||
@@ -111,6 +111,29 @@ void main() {
|
||||
);
|
||||
expect(queueState.cursor, 'cursor-remote-1');
|
||||
});
|
||||
|
||||
test('default fake pull is inert and does not seed people', () async {
|
||||
final ProviderContainer container = ProviderContainer(
|
||||
overrides: [
|
||||
backendGatewayProvider.overrideWithValue(BackendGatewayFake()),
|
||||
localDataStoreProvider.overrideWithValue(InMemoryLocalDataStore()),
|
||||
syncStateStoreProvider.overrideWithValue(InMemorySyncStateStore()),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(localRepositoryProvider.future);
|
||||
|
||||
final SyncRunResult result = await container
|
||||
.read(syncCoordinatorProvider)
|
||||
.pullRemote();
|
||||
|
||||
expect(result.success, isTrue);
|
||||
expect(result.pulled, 0);
|
||||
|
||||
final localState = await container.read(localRepositoryProvider.future);
|
||||
expect(localState.people, isEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
class _TestGateway extends BackendGatewayFake {
|
||||
|
||||
@@ -23,6 +23,15 @@ void main() {
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await container.read(localRepositoryProvider.future);
|
||||
await container
|
||||
.read(localRepositoryProvider.notifier)
|
||||
.addPerson(
|
||||
name: 'Repair Person',
|
||||
relationship: 'Friend',
|
||||
notes: '',
|
||||
tags: const <String>[],
|
||||
);
|
||||
final localBefore = await container.read(localRepositoryProvider.future);
|
||||
final person = localBefore.people.first;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user