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,82 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:relationship_saver/features/local/local_models.dart';
|
||||
import 'package:relationship_saver/features/share_intake/domain/share_capture_draft_suggester.dart';
|
||||
|
||||
void main() {
|
||||
test('suggests gift idea draft from gift-oriented text', () {
|
||||
final CapturedFactDraft draft =
|
||||
ShareCaptureDraftSuggester.suggestForPayload(
|
||||
SharedPayload(
|
||||
sourceApp: 'whatsapp',
|
||||
payloadType: SharedPayloadType.text,
|
||||
rawText: 'Gift idea: blue scarf she mentioned last week',
|
||||
createdAt: DateTime(2026, 4, 17),
|
||||
receivedAt: DateTime(2026, 4, 17),
|
||||
platform: 'android',
|
||||
),
|
||||
);
|
||||
|
||||
expect(draft.type, CapturedFactType.giftIdea);
|
||||
expect(draft.label, 'Gift idea');
|
||||
expect(draft.needsReview, isTrue);
|
||||
});
|
||||
|
||||
test(
|
||||
'suggests important date draft when birthday and explicit date are present',
|
||||
() {
|
||||
final CapturedFactDraft draft =
|
||||
ShareCaptureDraftSuggester.suggestForPayload(
|
||||
SharedPayload(
|
||||
sourceApp: 'notes',
|
||||
payloadType: SharedPayloadType.text,
|
||||
rawText: 'Her birthday is 2026-09-14',
|
||||
createdAt: DateTime(2026, 4, 17),
|
||||
receivedAt: DateTime(2026, 4, 17),
|
||||
platform: 'ios',
|
||||
),
|
||||
);
|
||||
|
||||
expect(draft.type, CapturedFactType.importantDate);
|
||||
expect(draft.label, 'Birthday');
|
||||
expect(draft.dateValue, DateTime(2026, 9, 14));
|
||||
},
|
||||
);
|
||||
|
||||
test('suggests place idea for shared maps links', () {
|
||||
final CapturedFactDraft draft =
|
||||
ShareCaptureDraftSuggester.suggestForPayload(
|
||||
SharedPayload(
|
||||
sourceApp: 'share_sheet',
|
||||
payloadType: SharedPayloadType.url,
|
||||
rawText: '',
|
||||
url: 'https://maps.apple.com/?address=1%20Infinite%20Loop',
|
||||
createdAt: DateTime(2026, 4, 17),
|
||||
receivedAt: DateTime(2026, 4, 17),
|
||||
platform: 'ios',
|
||||
),
|
||||
);
|
||||
|
||||
expect(draft.type, CapturedFactType.placeIdea);
|
||||
expect(draft.label, 'Place idea');
|
||||
});
|
||||
|
||||
test(
|
||||
'suggests dislike when text contains strong negative preference signal',
|
||||
() {
|
||||
final CapturedFactDraft draft =
|
||||
ShareCaptureDraftSuggester.suggestForPayload(
|
||||
SharedPayload(
|
||||
sourceApp: 'signal',
|
||||
payloadType: SharedPayloadType.text,
|
||||
rawText: "She doesn't like crowded bars",
|
||||
createdAt: DateTime(2026, 4, 17),
|
||||
receivedAt: DateTime(2026, 4, 17),
|
||||
platform: 'android',
|
||||
),
|
||||
);
|
||||
|
||||
expect(draft.type, CapturedFactType.dislike);
|
||||
expect(draft.label, 'Preference');
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'package:relationship_saver/features/local/storage/local_data_store_in_me
|
||||
import 'package:relationship_saver/features/local/storage/local_data_store_provider.dart';
|
||||
import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler.dart';
|
||||
import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler_provider.dart';
|
||||
import 'package:relationship_saver/features/share_intake/share_capture_models.dart';
|
||||
import 'package:relationship_saver/features/share_intake/share_inbox_view.dart';
|
||||
import 'package:relationship_saver/features/sync/storage/sync_state_store_in_memory.dart';
|
||||
import 'package:relationship_saver/features/sync/storage/sync_state_store_provider.dart';
|
||||
@@ -66,7 +67,7 @@ void main() {
|
||||
|
||||
expect(find.text('Share Inbox'), findsOneWidget);
|
||||
expect(find.text('Ambiguous match'), findsOneWidget);
|
||||
expect(find.widgetWithText(FilledButton, 'Review & Match'), findsOneWidget);
|
||||
expect(find.widgetWithText(FilledButton, 'Review & Save'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.widgetWithText(TextButton, 'Dismiss Item').first);
|
||||
await tester.pumpAndSettle();
|
||||
@@ -116,7 +117,12 @@ void main() {
|
||||
|
||||
expect(find.text('Near match conflict'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Review & Match').first);
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Review & Save').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Review Capture Mapping'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Use Mapping'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Review Match'), findsOneWidget);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:relationship_saver/features/local/local_models.dart';
|
||||
import 'package:relationship_saver/features/share_intake/share_payload_parser.dart';
|
||||
|
||||
void main() {
|
||||
test('parses url-only payload', () {
|
||||
final SharedPayload payload = SharePayloadParser.parse(
|
||||
rawText: 'https://example.com/date-night',
|
||||
sourceApp: 'share_sheet',
|
||||
platform: 'android',
|
||||
);
|
||||
|
||||
expect(payload.payloadType, SharedPayloadType.url);
|
||||
expect(payload.url, 'https://example.com/date-night');
|
||||
expect(payload.rawText, isEmpty);
|
||||
});
|
||||
|
||||
test('parses text plus url payload', () {
|
||||
final SharedPayload payload = SharePayloadParser.parse(
|
||||
rawText: 'Could be a good anniversary place https://example.com',
|
||||
sourceApp: 'share_sheet',
|
||||
platform: 'android',
|
||||
);
|
||||
|
||||
expect(payload.payloadType, SharedPayloadType.textWithUrl);
|
||||
expect(payload.url, 'https://example.com');
|
||||
expect(payload.rawText, 'Could be a good anniversary place');
|
||||
});
|
||||
|
||||
test('preserves WhatsApp sender extraction when pattern matches', () {
|
||||
final SharedPayload payload = SharePayloadParser.parse(
|
||||
rawText: 'Ava Hart: loves cozy Italian restaurants',
|
||||
sourceApp: 'whatsapp',
|
||||
platform: 'ios',
|
||||
);
|
||||
|
||||
expect(payload.sourceDisplayName, 'Ava Hart');
|
||||
expect(payload.sourceUserId, 'wa:ava hart');
|
||||
expect(payload.rawText, 'loves cozy Italian restaurants');
|
||||
});
|
||||
|
||||
test('uses explicit signal source app hint and extracts sender', () {
|
||||
final SharedPayload payload = SharePayloadParser.parse(
|
||||
rawText: 'Marek: this concert looks promising',
|
||||
sourceApp: 'signal',
|
||||
platform: 'android',
|
||||
);
|
||||
|
||||
expect(payload.sourceApp, 'signal');
|
||||
expect(payload.sourceDisplayName, 'Marek');
|
||||
expect(payload.sourceUserId, 'signal:marek');
|
||||
expect(payload.rawText, 'this concert looks promising');
|
||||
});
|
||||
|
||||
test('normalizes notes hint without trying to invent sender metadata', () {
|
||||
final SharedPayload payload = SharePayloadParser.parse(
|
||||
rawText: 'Dinner idea from Notes https://example.com',
|
||||
sourceApp: 'notes',
|
||||
platform: 'ios',
|
||||
);
|
||||
|
||||
expect(payload.sourceApp, 'notes');
|
||||
expect(payload.sourceDisplayName, isNull);
|
||||
expect(payload.payloadType, SharedPayloadType.textWithUrl);
|
||||
expect(payload.rawText, 'Dinner idea from Notes');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user