Merge branch 'claude/festive-einstein-384c45'

Ego-centric trust replaces the global Duniter WoT, plus Wallapop-style
ratings v1 (kind 30778). Conflicts resolved: pubspec assets (kept
seed_saving, dropped trust referents), chat_screen (avatars/no-links
from main + rating strip and simplified TrustCubit from the branch),
strings.g.dart regenerated from merged sources.
This commit is contained in:
vjrj 2026-07-11 13:25:51 +02:00
commit 456cc8154d
40 changed files with 1777 additions and 876 deletions

View file

@ -0,0 +1,124 @@
import 'package:commons_core/commons_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/i18n/strings.g.dart';
import 'package:tane/state/peer_rating_cubit.dart';
import 'package:tane/ui/rating_sheet.dart';
/// In-memory [RatingTransport] (one live rating per pair).
class FakeRatingTransport implements RatingTransport {
FakeRatingTransport(this.selfId);
final String selfId;
final Map<String, Rating> byRater = {};
@override
Future<void> rate({
required String subjectPubkey,
required int stars,
String comment = '',
}) async =>
byRater[selfId] = Rating(
rater: selfId,
subject: subjectPubkey,
stars: stars,
ratedAt: DateTime(2026),
comment: comment,
);
@override
Future<void> retract({required String subjectPubkey}) async =>
byRater.remove(selfId);
@override
Future<List<Rating>> ratingsOf(String subjectPubkey) async =>
List.of(byRater.values);
@override
Future<Rating?> myRatingOf(String subjectPubkey) async => byRater[selfId];
@override
Future<void> close() async {}
}
void main() {
const me = 'me';
const peer = 'peer';
late FakeRatingTransport transport;
late PeerRatingCubit cubit;
setUp(() async {
LocaleSettings.setLocaleSync(AppLocale.en);
transport = FakeRatingTransport(me);
cubit = PeerRatingCubit(transport, peerPubkey: peer, selfPubkey: me);
await cubit.load();
});
tearDown(() => cubit.close());
Widget host() => TranslationProvider(
child: MaterialApp(
locale: AppLocale.en.flutterLocale,
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: Scaffold(
body: Builder(
builder: (context) => Center(
child: TextButton(
onPressed: () => showRatingSheet(context, cubit),
child: const Text('open'),
),
),
),
),
),
);
testWidgets('picking stars and saving publishes the rating',
(tester) async {
await tester.pumpWidget(host());
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
expect(find.text('Rate this person'), findsOneWidget);
// Save is disabled until stars are picked.
expect(
tester.widget<FilledButton>(find.byKey(const Key('rating.save'))).enabled,
isFalse,
);
await tester.tap(find.byKey(const Key('rating.star.4')));
await tester.pump();
await tester.enterText(
find.byKey(const Key('rating.comment')), 'Great swap');
await tester.tap(find.byKey(const Key('rating.save')));
await tester.pumpAndSettle();
final mine = transport.byRater[me]!;
expect(mine.stars, 4);
expect(mine.comment, 'Great swap');
expect(find.text('Rating saved'), findsOneWidget); // snackbar
});
testWidgets('opens pre-filled when you already rated; retract removes',
(tester) async {
await cubit.rate(3, comment: 'Fine');
await tester.pumpWidget(host());
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
expect(find.text('Edit your rating'), findsOneWidget);
expect(find.text('Fine'), findsOneWidget); // comment pre-filled
await tester.tap(find.byKey(const Key('rating.retract')));
await tester.pumpAndSettle();
expect(transport.byRater, isEmpty);
});
}

View file

@ -0,0 +1,176 @@
import 'package:commons_core/commons_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/i18n/strings.g.dart';
import 'package:tane/services/profile_cache.dart';
import 'package:tane/services/social_connection.dart';
import 'package:tane/services/social_service.dart';
import 'package:tane/services/social_settings.dart';
import 'package:tane/ui/your_people_screen.dart';
import '../support/test_support.dart';
/// In-memory [TrustTransport] with a mutable certification graph.
class FakeTrustTransport implements TrustTransport {
FakeTrustTransport(this.selfId);
final String selfId;
final List<Certification> certs = [];
void addCert(String issuer, String subject) => certs.add(Certification(
issuer: issuer,
subject: subject,
issuedAt: DateTime(2026),
));
@override
Future<List<Certification>> allCertifications() async => List.of(certs);
@override
Future<Set<String>> certifiersOf(String subjectPubkey) async => {
for (final c in certs)
if (c.subject == subjectPubkey) c.issuer,
};
@override
Future<void> certify({
required String subjectPubkey,
Duration validity = const Duration(days: 365),
String note = '',
}) async =>
addCert(selfId, subjectPubkey);
@override
Future<void> revoke({required String subjectPubkey}) async => certs
.removeWhere((c) => c.issuer == selfId && c.subject == subjectPubkey);
@override
Future<void> close() async {}
}
/// A [SocialSession] stand-in that only carries the trust transport the
/// screen under test never touches the other transports.
class FakeSession implements SocialSession {
FakeSession(this.trust);
@override
final TrustTransport trust;
@override
OfferTransport get offers => throw UnimplementedError();
@override
MessageTransport get messages => throw UnimplementedError();
@override
RatingTransport get ratings => throw UnimplementedError();
@override
ProfileTransport get profile => throw UnimplementedError();
@override
SyncTransport get sync => throw UnimplementedError();
@override
Future<void> close() async {}
}
void main() {
const seedHex =
'000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f';
final peerA = 'aa' * 32;
final peerB = 'bb' * 32;
late SocialService social;
setUp(() async {
social = await SocialService.fromRootSeedHex(seedHex);
LocaleSettings.setLocaleSync(AppLocale.en);
});
Widget app(SocialConnection connection, {ProfileCache? cache}) =>
TranslationProvider(
child: MaterialApp(
locale: AppLocale.en.flutterLocale,
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: YourPeopleScreen(
social: social,
connection: connection,
profileCache: cache,
),
),
);
SocialConnection online(TrustTransport trust) => SocialConnection(
social: social,
settings: SocialSettings(InMemorySecretStore()),
open: (_) async => FakeSession(trust) as SocialSession,
);
testWidgets('offline shows the friendly note', (tester) async {
final settings = SocialSettings(InMemorySecretStore());
await settings.setRelayUrls(const []); // offline: don't hit the network
final connection = SocialConnection(social: social, settings: settings);
await tester.pumpWidget(app(connection));
await tester.pumpAndSettle();
expect(
find.text("You're offline — try again when you're connected"),
findsOneWidget,
);
});
testWidgets('lists who you vouch for and who vouches for you, with names',
(tester) async {
final trust = FakeTrustTransport(social.publicKeyHex)
..addCert(social.publicKeyHex, peerA) // you vouch for A
..addCert(peerB, social.publicKeyHex) // B vouches for you
..addCert(peerA, peerB); // unrelated edge, must not show
final cache = ProfileCache(InMemorySecretStore());
await cache.setName(peerA, 'Alice');
await tester.pumpWidget(app(online(trust), cache: cache));
await tester.pumpAndSettle();
expect(find.text('You vouch for'), findsOneWidget);
expect(find.text('They vouch for you'), findsOneWidget);
expect(find.text('Alice'), findsOneWidget); // cached name wins
expect(find.text(shortPubkey(peerB)), findsOneWidget); // fallback
expect(find.byKey(Key('yourPeople.given.$peerA')), findsOneWidget);
expect(find.byKey(Key('yourPeople.received.$peerB')), findsOneWidget);
});
testWidgets('empty states show gentle notes', (tester) async {
await tester
.pumpWidget(app(online(FakeTrustTransport(social.publicKeyHex))));
await tester.pumpAndSettle();
expect(
find.textContaining("You don't vouch for anyone yet"),
findsOneWidget,
);
expect(find.text('No one vouches for you yet'), findsOneWidget);
});
testWidgets('revoking removes the vouch after confirming', (tester) async {
final trust = FakeTrustTransport(social.publicKeyHex)
..addCert(social.publicKeyHex, peerA);
await tester.pumpWidget(app(online(trust)));
await tester.pumpAndSettle();
await tester.tap(find.text('Stop vouching'));
await tester.pumpAndSettle();
expect(find.text('Stop vouching for this person?'), findsOneWidget);
await tester.tap(find.text('Stop vouching').last); // dialog action
await tester.pumpAndSettle();
expect(trust.certs, isEmpty);
expect(
find.textContaining("You don't vouch for anyone yet"),
findsOneWidget,
);
});
}