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,94 @@
|
||||
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/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';
|
||||
import 'package:relationship_saver/features/people/domain/person_models.dart';
|
||||
import 'package:relationship_saver/features/reminders/domain/reminder_models.dart';
|
||||
import 'package:relationship_saver/features/share_intake/domain/share_models.dart';
|
||||
|
||||
void main() {
|
||||
test('builds pseudonymous payload without identifiable profile data', () {
|
||||
final LocalDataState state = LocalDataState(
|
||||
people: <PersonProfile>[
|
||||
PersonProfile(
|
||||
id: 'p-1',
|
||||
name: 'Alice Secret',
|
||||
relationship: 'Sister',
|
||||
affinityScore: 85,
|
||||
nextMoment: DateTime(2026, 6, 1),
|
||||
tags: const <String>['coffee', 'books'],
|
||||
notes: 'Passport number and private address should stay local.',
|
||||
aliases: const <String>['Ally'],
|
||||
location: 'Zurich, Switzerland',
|
||||
lastInteractedAt: DateTime(2026, 5, 1),
|
||||
),
|
||||
],
|
||||
moments: const <RelationshipMoment>[],
|
||||
ideas: const <RelationshipIdea>[],
|
||||
reminders: const <ReminderRule>[],
|
||||
tasks: const <DashboardTask>[],
|
||||
sourceLinks: <SourceProfileLink>[
|
||||
SourceProfileLink(
|
||||
id: 'sl-1',
|
||||
sourceApp: 'whatsapp',
|
||||
normalizedDisplayName: 'Alice Secret',
|
||||
profileId: 'p-1',
|
||||
firstSeenAt: DateTime(2026, 5, 1),
|
||||
lastSeenAt: DateTime(2026, 5, 1),
|
||||
sourceUserId: 'wa:alice',
|
||||
sourceThreadId: 'thread-secret',
|
||||
sourceFingerprint: 'whatsapp|u:wa:alice',
|
||||
),
|
||||
],
|
||||
sharedMessages: <SharedMessageEntry>[
|
||||
SharedMessageEntry(
|
||||
id: 'sm-1',
|
||||
profileId: 'p-1',
|
||||
payload: SharedPayload(
|
||||
sourceApp: 'whatsapp',
|
||||
payloadType: SharedPayloadType.text,
|
||||
rawText: 'Raw private chat text',
|
||||
createdAt: DateTime(2026, 5, 1),
|
||||
receivedAt: DateTime(2026, 5, 1),
|
||||
platform: 'ios',
|
||||
sourceDisplayName: 'Alice Secret',
|
||||
sourceUserId: 'wa:alice',
|
||||
sourceThreadId: 'thread-secret',
|
||||
),
|
||||
importedAt: DateTime(2026, 5, 1),
|
||||
resolvedAutomatically: true,
|
||||
),
|
||||
],
|
||||
personFacts: <PersonFact>[
|
||||
PersonFact(
|
||||
id: 'pf-1',
|
||||
personId: 'p-1',
|
||||
type: CapturedFactType.note,
|
||||
text: 'Secret medical detail',
|
||||
sourceKind: CaptureSourceKind.manual,
|
||||
createdAt: DateTime(2026, 5, 1),
|
||||
updatedAt: DateTime(2026, 5, 1),
|
||||
isSensitive: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final AnonymizedLlmDigestContext context = AnonymizedLlmContextBuilder(
|
||||
now: () => DateTime(2026, 5, 16),
|
||||
).build(state);
|
||||
final String prompt = context.toPromptJson();
|
||||
|
||||
expect(context.tokenToPersonId, <String, String>{'person_001': 'p-1'});
|
||||
expect(prompt, contains('person_001'));
|
||||
expect(prompt, isNot(contains('Alice')));
|
||||
expect(prompt, isNot(contains('Ally')));
|
||||
expect(prompt, isNot(contains('Zurich')));
|
||||
expect(prompt, isNot(contains('Passport')));
|
||||
expect(prompt, isNot(contains('wa:alice')));
|
||||
expect(prompt, isNot(contains('thread-secret')));
|
||||
expect(prompt, isNot(contains('Raw private chat text')));
|
||||
expect(prompt, isNot(contains('Secret medical detail')));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user