From efe04288ea907adfe91a38ae916c0d364c315cca Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Tue, 29 Sep 2026 12:21:14 -0400 Subject: [PATCH] feat(mobile): add the contextual identity-name resolver (#7894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿค– ## Summary When two identities share a display name, Buzz mobile shows the same label for both. A reader cannot tell a person from an agent, or one person's agent from another's. For example, a channel can show three agents and one human all named "murderbot". This PR adds the naming rules that fix this, without changing any screen yet. It ports the contextual identity-name contract (v1) from the desktop app to Dart. Given the identities shown together, the resolver gives each public key a distinct label: - A human keeps the plain name. Agents that share it get a qualifier. - The viewer's own agents are named before other people's agents. - An agent is qualified by its owner's name first ("Wes's Honey"). A short public-key suffix is added only if that is still ambiguous ("baxen's murderbot ยท xa8v"). It also adds two providers that screens will use: - `channelIdentityNamesProvider` compares names against a channel's members, plus any referenced non-member such as an old author or an outside mention. - `watchIdentityNames` compares a set of identities shown together outside a channel, and loads their profiles. The next PRs in this stack connect these to the screens. Part 1 of 3: #7894 (resolver) โ†’ #7895 (conversations) โ†’ #7896 (lists, Search, Pulse). ### Related issue Related to #2910 (default "Fizz" agents from different users collide in one channel). ### Testing - The contract's 31 portable fixture cases run against the production resolver. The fixture file is copied unchanged from the contract, so reviewers can skip it. - Unit tests cover owner and suffix qualification, the viewer's own identities, and invalid public keys. - Provider tests check that a channel keeps its names while its roster reloads, that an offline cached bot still counts as an agent, and that changing relay or account never shows the old roster. - `flutter analyze` is clean. No UI change, so there are no screenshots. --------- Signed-off-by: Logan Johnson Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> --- .../channel_identity_names_provider.dart | 83 + .../channels/channel_management_provider.dart | 18 + .../identity_names/identity_name_policy.dart | 239 +++ .../shared/identity_names/identity_names.dart | 169 ++ .../identity_names_provider.dart | 66 + .../channel_identity_names_provider_test.dart | 233 +++ .../identity-names.fixtures.json | 1787 +++++++++++++++++ .../identity_name_policy_test.dart | 129 ++ .../identity_names/identity_names_test.dart | 100 + 9 files changed, 2824 insertions(+) create mode 100644 mobile/lib/features/channels/channel_identity_names_provider.dart create mode 100644 mobile/lib/shared/identity_names/identity_name_policy.dart create mode 100644 mobile/lib/shared/identity_names/identity_names.dart create mode 100644 mobile/lib/shared/identity_names/identity_names_provider.dart create mode 100644 mobile/test/features/channels/channel_identity_names_provider_test.dart create mode 100644 mobile/test/shared/identity_names/identity-names.fixtures.json create mode 100644 mobile/test/shared/identity_names/identity_name_policy_test.dart create mode 100644 mobile/test/shared/identity_names/identity_names_test.dart diff --git a/mobile/lib/features/channels/channel_identity_names_provider.dart b/mobile/lib/features/channels/channel_identity_names_provider.dart new file mode 100644 index 0000000000..4d25a4c17d --- /dev/null +++ b/mobile/lib/features/channels/channel_identity_names_provider.dart @@ -0,0 +1,83 @@ +import 'dart:collection'; + +import 'package:flutter/foundation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../shared/identity_names/identity_names.dart'; +import '../../shared/identity_names/identity_names_provider.dart'; +import '../../shared/mentions/agent_identity_provider.dart'; +import 'channel_management_provider.dart'; +import 'channels_provider.dart'; + +/// Contextual identity labels for one channel, thread, or DM. +/// +/// The comparison context is the channel's members; a referenced non-member +/// (historical author, outside mention) is compared against the members plus +/// itself. Watch one label with +/// `ref.watch(channelIdentityNamesProvider(id).select((n) => n.labelFor(pk)))`. +final channelIdentityNamesProvider = Provider.autoDispose + .family((ref, channelId) { + final sources = ref.watch(identityNameSourcesProvider); + final members = + watchLatestChannelMembers(ref, channelId) ?? + (ref.watch(channelsProvider).asData == null + ? const [] + : ref + .read(channelsProvider.notifier) + .cachedMembersForChannel(channelId)); + final names = sources.scope( + [for (final member in members) member.pubkey], + // The roster's own bot roles stay available offline, when the + // relay-backed bot lookup and agent directory are empty. + agentPubkeys: { + ...ref.watch(agentMentionPubkeysProvider(channelId)), + for (final member in members) + if (member.isBot) member.pubkey, + }, + fallbackNames: { + for (final member in members) member.pubkey: ?member.displayName, + }, + ); + loadIdentityNameOwners(ref, sources, names.candidates); + return names; + }); + +/// Watches one identity's contextual label in [channelId]. +String watchChannelIdentityLabel( + WidgetRef ref, + String channelId, + String pubkey, +) => ref.watch( + channelIdentityNamesProvider( + channelId, + ).select((names) => names.labelFor(pubkey)), +); + +/// Watches contextual labels for [pubkeys] in [channelId], keyed by +/// lowercase pubkey. Rebuilds only when one of these labels changes. +Map watchChannelIdentityLabels( + WidgetRef ref, + String channelId, + Iterable pubkeys, +) { + final keys = {for (final pubkey in pubkeys) pubkey.toLowerCase()}; + if (keys.isEmpty) return const {}; + return ref.watch( + channelIdentityNamesProvider(channelId).select( + (names) => _LabelMap({for (final key in keys) key: names.labelFor(key)}), + ), + ); +} + +class _LabelMap extends UnmodifiableMapView { + _LabelMap(super.labels); + + @override + bool operator ==(Object other) => + other is Map && mapEquals(this, other); + + @override + int get hashCode => Object.hashAllUnordered( + entries.map((entry) => Object.hash(entry.key, entry.value)), + ); +} diff --git a/mobile/lib/features/channels/channel_management_provider.dart b/mobile/lib/features/channels/channel_management_provider.dart index 36e640b411..9b90b5163b 100644 --- a/mobile/lib/features/channels/channel_management_provider.dart +++ b/mobile/lib/features/channels/channel_management_provider.dart @@ -586,6 +586,24 @@ final channelMembersProvider = FutureProvider.autoDispose return members; }); +/// The latest roster for [channelId] in the current relay, account, and +/// channel scope, or null if none has loaded in that scope yet. +/// +/// While [channelMembersProvider] reloads (for example after a kind:39002 +/// update), this keeps the roster from the last completed load in the same +/// scope instead of dropping it. Another relay or account never sees it. +List? watchLatestChannelMembers(Ref ref, String channelId) { + final loaded = ref.watch(channelMembersProvider(channelId)).asData?.value; + if (loaded != null) return loaded; + return ref + .read(_channelMembersSnapshotCacheProvider) + .read( + relayBaseUrl: ref.watch(relayConfigProvider).baseUrl, + pubkey: ref.watch(myPubkeyProvider)?.toLowerCase(), + channelId: channelId, + ); +} + /// Channel canvas (kind:40100 for the channel). final channelCanvasProvider = FutureProvider.family(( ref, diff --git a/mobile/lib/shared/identity_names/identity_name_policy.dart b/mobile/lib/shared/identity_names/identity_name_policy.dart new file mode 100644 index 0000000000..d3afaf82e4 --- /dev/null +++ b/mobile/lib/shared/identity_names/identity_name_policy.dart @@ -0,0 +1,239 @@ +import 'package:flutter/foundation.dart'; + +import '../utils/string_utils.dart'; + +/// Pinned version of the portable identity-name contract implemented here. +/// +/// Spec and fixtures: +/// https://github.com/block/buzz-app/blob/92c2fb2/src/bundled/identity-naming/README.md +const identityNamePolicyVersion = 1; + +/// One naming fact for an identity. [pubkey] and [ownerPubkey] must be valid +/// 64-character hex public keys (either case); callers choose names and +/// fallbacks first. +@immutable +class NamingIdentity { + final String pubkey; + final String name; + final bool isAgent; + final String? ownerPubkey; + + const NamingIdentity({ + required this.pubkey, + required this.name, + this.isAgent = false, + this.ownerPubkey, + }); + + @override + bool operator ==(Object other) => + other is NamingIdentity && + other.pubkey == pubkey && + other.name == name && + other.isAgent == isAgent && + other.ownerPubkey == ownerPubkey; + + @override + int get hashCode => Object.hash(pubkey, name, isAgent, ownerPubkey); +} + +/// A resolved display label. [qualifier] is only the key suffix, when added. +@immutable +class ResolvedIdentityName { + final String name; + final String? qualifier; + + const ResolvedIdentityName(this.name, [this.qualifier]); + + @override + bool operator ==(Object other) => + other is ResolvedIdentityName && + other.name == name && + other.qualifier == qualifier; + + @override + int get hashCode => Object.hash(name, qualifier); + + @override + String toString() => 'ResolvedIdentityName($name, $qualifier)'; +} + +// ECMAScript String.prototype.trim code points, as the contract requires. +// Dart's String.trim also removes U+0085, which the contract keeps. +bool _isContractWhitespace(int unit) => + (unit >= 0x09 && unit <= 0x0D) || + unit == 0x20 || + unit == 0xA0 || + unit == 0x1680 || + (unit >= 0x2000 && unit <= 0x200A) || + unit == 0x2028 || + unit == 0x2029 || + unit == 0x202F || + unit == 0x205F || + unit == 0x3000 || + unit == 0xFEFF; + +/// Trims [value] exactly as the identity-name contract specifies. +String trimIdentityName(String value) { + var start = 0; + var end = value.length; + while (start < end && _isContractWhitespace(value.codeUnitAt(start))) { + start++; + } + while (end > start && _isContractWhitespace(value.codeUnitAt(end - 1))) { + end--; + } + return value.substring(start, end); +} + +final RegExp _hexKey = RegExp(r'^[0-9a-fA-F]{64}$'); + +void _requireKey(String key, String name) { + if (!_hexKey.hasMatch(key)) { + throw ArgumentError.value(key, name, 'not a 64-character hex public key'); + } +} + +class _Row { + final String key; + final NamingIdentity identity; + final String original; + final int priority; + final bool mine; + String base; + String label; + int length = 0; + String? suffix; + + _Row({ + required this.key, + required this.identity, + required this.original, + required this.priority, + required this.mine, + }) : base = original, + label = original; +} + +/// Resolves distinct display labels for the selected identities. +/// +/// Display policy only: a readable owner or key suffix grants no authority. +/// Implements contextual identity names v1 (see [identityNamePolicyVersion]). +/// Omitting [candidates] selects every supplied fact; an empty set selects +/// none. +/// +/// Every supplied key โ€” each fact's [NamingIdentity.pubkey] and +/// [NamingIdentity.ownerPubkey], [viewer], and each candidate โ€” must be a +/// valid public key. The contract leaves invalid keys to the caller, so this +/// throws [ArgumentError] before resolving anything rather than returning a +/// label for a value that is not an identity. +Map resolveIdentityNames( + List identities, { + String? viewer, + Iterable? candidates, +}) { + for (final identity in identities) { + _requireKey(identity.pubkey, 'pubkey'); + if (identity.ownerPubkey case final owner?) { + _requireKey(owner, 'ownerPubkey'); + } + } + if (viewer != null) _requireKey(viewer, 'viewer'); + final selected = candidates?.map((key) { + _requireKey(key, 'candidates'); + return key.toLowerCase(); + }).toSet(); + final normalizedViewer = viewer?.toLowerCase(); + // Last fact per key: the preferred alias and the owner-lookup name. + final preferred = { + for (final identity in identities) identity.pubkey.toLowerCase(): identity, + }; + // One working row per (key, trimmed name); the last fact supplies metadata. + final aliases = <(String, String), NamingIdentity>{}; + for (final identity in identities) { + final key = ( + identity.pubkey.toLowerCase(), + trimIdentityName(identity.name), + ); + aliases[key] = identity; + } + final rows = <_Row>[ + for (final MapEntry(key: (key, name), value: identity) in aliases.entries) + if (selected == null || selected.contains(key)) + () { + final owner = identity.ownerPubkey?.toLowerCase(); + final mine = + normalizedViewer != null && + (key == normalizedViewer || owner == normalizedViewer); + return _Row( + key: key, + identity: identity, + original: name, + mine: mine, + priority: identity.isAgent + ? (mine ? 2 : 3) + : (key == normalizedViewer ? 0 : 1), + ); + }(), + ]; + final npubs = {}; + String npubFor(String key) => npubs[key] ??= fullNpub(key)!; + + while (true) { + final groups = >{}; + for (final row in rows) { + (groups[row.label] ??= []).add(row); + } + final collisions = [ + for (final group in groups.values) + if (group.map((row) => row.key).toSet().length > 1) group, + ]; + if (collisions.isEmpty) break; + for (final group in collisions) { + final best = group + .map((row) => row.priority) + .reduce((a, b) => a < b ? a : b); + final winners = { + for (final row in group) + if (row.priority == best) row.key, + }; + final changing = [ + for (final row in group) + if (winners.length != 1 || !winners.contains(row.key)) row, + ]; + final groupHasHuman = group.any((row) => !row.identity.isAgent); + var qualified = false; + for (final row in changing) { + if (!row.identity.isAgent || row.length != 0) continue; + final ownerKey = row.identity.ownerPubkey?.toLowerCase(); + final ownerFact = ownerKey == null ? null : preferred[ownerKey]; + final owner = ownerFact == null ? '' : trimIdentityName(ownerFact.name); + final readable = !row.mine && owner.isNotEmpty + ? '$owner\u2019s ${row.original}' + : groupHasHuman + ? '${row.original} (agent)' + : row.base; + if (readable != row.base) { + row.base = readable; + row.label = readable; + qualified = true; + } + } + if (qualified) continue; + for (final row in changing) { + row.length = row.length == 0 ? 4 : row.length + 1; + final npub = npubFor(row.key); + row.suffix = row.length <= npub.length + ? npub.substring(npub.length - row.length) + : '$npub \u00b7 ${row.length - npub.length}'; + row.label = '${row.base} \u00b7 ${row.suffix}'; + } + } + } + + return { + for (final row in rows) + if (row.original == trimIdentityName(preferred[row.key]!.name)) + row.key: ResolvedIdentityName(row.label, row.suffix), + }; +} diff --git a/mobile/lib/shared/identity_names/identity_names.dart b/mobile/lib/shared/identity_names/identity_names.dart new file mode 100644 index 0000000000..c573eb64d6 --- /dev/null +++ b/mobile/lib/shared/identity_names/identity_names.dart @@ -0,0 +1,169 @@ +import 'package:flutter/foundation.dart'; + +import '../profile/user_profile.dart'; +import '../utils/string_utils.dart'; +import 'identity_name_policy.dart'; + +final RegExp _hexPubkey = RegExp(r'^[0-9a-f]{64}$'); + +/// Immutable naming facts known to this client, independent of any view. +/// +/// Mobile's owner hint is the verified NIP-OA owner from the profile `auth` +/// tag (or agent directory). It is display ranking only, never authority. +@immutable +class IdentityNameSources { + final Map profiles; + final Set agentPubkeys; + final Map agentDisplayNames; + final Map agentOwners; + final String? viewer; + + const IdentityNameSources({ + this.profiles = const {}, + this.agentPubkeys = const {}, + this.agentDisplayNames = const {}, + this.agentOwners = const {}, + this.viewer, + }); + + static const empty = IdentityNameSources(); + + /// The naming fact for [pubkey], or null when it is not a valid key. + /// + /// Name: non-blank profile display name, then agent-directory name, then + /// [fallbackName], then the compact npub. The resolver trims the name. + NamingIdentity? factFor( + String pubkey, { + String? fallbackName, + bool isAgent = false, + }) { + final key = pubkey.toLowerCase(); + if (!_hexPubkey.hasMatch(key)) return null; + final profile = profiles[key]; + final owner = (profile?.ownerPubkey ?? agentOwners[key])?.toLowerCase(); + return NamingIdentity( + pubkey: key, + name: + _nonBlank(profile?.displayName) ?? + _nonBlank(agentDisplayNames[key]) ?? + _nonBlank(fallbackName) ?? + shortPubkey(key), + isAgent: isAgent || owner != null || agentPubkeys.contains(key), + ownerPubkey: owner != null && _hexPubkey.hasMatch(owner) ? owner : null, + ); + } + + /// Owner keys of [candidates] whose profile is not cached yet. Callers load + /// them so readable owner prefixes can appear; none are invented meanwhile. + Set missingOwnerProfiles(Iterable candidates) => candidates + .map((key) => factFor(key)?.ownerPubkey) + .nonNulls + .where((owner) => !profiles.containsKey(owner)) + .toSet(); + + /// Owner lookup fact: only a real, non-blank owner profile name. + NamingIdentity? _ownerFact(String owner) { + final name = _nonBlank(profiles[owner]?.displayName); + return name == null ? null : NamingIdentity(pubkey: owner, name: name); + } + + /// Resolves labels for one view. [candidates] is the view's comparison + /// context (for example channel members); [agentPubkeys] adds view-local + /// agent roles such as channel bots; [fallbackNames] supplies view-local + /// names used only when no profile or directory name exists. + IdentityNames scope( + Iterable candidates, { + Set agentPubkeys = const {}, + Map fallbackNames = const {}, + }) => IdentityNames._( + this, + { + for (final key in candidates) + if (_hexPubkey.hasMatch(key.toLowerCase())) key.toLowerCase(), + }, + {for (final key in agentPubkeys) key.toLowerCase()}, + { + for (final MapEntry(:key, :value) in fallbackNames.entries) + key.toLowerCase(): value, + }, + ); +} + +/// Contextual display labels for one view's comparison context. +/// +/// Labels are presentation only: keep the exact key for navigation, mentions, +/// and every identity-bound action. A key outside the context is resolved +/// against the context plus that key, as the contract requires for +/// historical or non-member references. +class IdentityNames { + final IdentityNameSources _sources; + final Set _candidates; + final Set _agentPubkeys; + final Map _fallbackNames; + final Map _outside = {}; + late final Map _members = _resolve(_candidates); + + IdentityNames._( + this._sources, + this._candidates, + this._agentPubkeys, + this._fallbackNames, + ); + + /// The comparison context's lowercase keys. + Set get candidates => _candidates; + + /// The resolved label and qualifier for [pubkey], or null for an invalid key. + ResolvedIdentityName? resolve(String pubkey) { + final key = pubkey.toLowerCase(); + if (_candidates.contains(key)) return _members[key]; + return _outside.putIfAbsent(key, () { + if (!_hexPubkey.hasMatch(key)) return null; + return _resolve({..._candidates, key})[key]; + }); + } + + /// The display label for [pubkey]. A malformed key is outside the naming + /// contract: it keeps its plain known name and is never disambiguated. + String labelFor(String pubkey) => + resolve(pubkey)?.name ?? _plainName(pubkey.toLowerCase()); + + String _plainName(String key) => + _nonBlank(_sources.profiles[key]?.displayName)?.trim() ?? + _nonBlank(_sources.agentDisplayNames[key])?.trim() ?? + _nonBlank(_fallbackNames[key])?.trim() ?? + shortPubkey(key); + + Map _resolve(Set candidates) { + final facts = []; + final owners = {}; + for (final key in candidates) { + final fact = _sources.factFor( + key, + fallbackName: _fallbackNames[key], + isAgent: _agentPubkeys.contains(key), + ); + if (fact == null) continue; + facts.add(fact); + if (fact.ownerPubkey case final owner? when !candidates.contains(owner)) { + owners.add(owner); + } + } + // Owners outside the context are lookup facts, not collision candidates; + // placing them first keeps a candidate's own fact preferred. + final ownerFacts = [ + for (final owner in owners) ?_sources._ownerFact(owner), + ]; + return resolveIdentityNames( + [...ownerFacts, ...facts], + viewer: _sources.viewer, + candidates: candidates, + ); + } +} + +String? _nonBlank(String? value) { + if (value == null) return null; + final trimmed = trimIdentityName(value); + return trimmed.isEmpty ? null : value; +} diff --git a/mobile/lib/shared/identity_names/identity_names_provider.dart b/mobile/lib/shared/identity_names/identity_names_provider.dart new file mode 100644 index 0000000000..2f07546d73 --- /dev/null +++ b/mobile/lib/shared/identity_names/identity_names_provider.dart @@ -0,0 +1,66 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../mentions/agent_identity_provider.dart'; +import '../profile/user_cache_provider.dart'; +import '../relay/relay.dart'; +import 'identity_names.dart'; + +/// Client-wide naming facts. Views choose their own comparison context with +/// [IdentityNameSources.scope]; never resolve against this whole cache. +final identityNameSourcesProvider = Provider((ref) { + return IdentityNameSources( + profiles: ref.watch(userCacheProvider), + agentPubkeys: ref.watch(knownAgentPubkeysProvider), + agentDisplayNames: ref.watch(agentDirectoryDisplayNamesProvider), + agentOwners: ref.watch(agentOwnersProvider).asData?.value ?? const {}, + viewer: ref.watch(myPubkeyProvider)?.toLowerCase(), + ); +}); + +/// Requests missing owner profiles for [names] after the current build. +void loadIdentityNameOwners( + Ref ref, + IdentityNameSources sources, + Iterable candidates, +) { + final missing = sources.missingOwnerProfiles(candidates); + if (missing.isEmpty) return; + Future.microtask(() { + if (ref.mounted) { + ref.read(userCacheProvider.notifier).preload(missing.toList()); + } + }); +} + +/// Labels for a displayed collection with no channel context, such as search +/// results, a Pulse timeline, or a picker's choices. [candidates] is that +/// collection; keys outside it resolve against it plus themselves. +IdentityNames watchIdentityNames( + WidgetRef ref, + Iterable candidates, { + Set agentPubkeys = const {}, + Map fallbackNames = const {}, +}) { + final sources = ref.watch(identityNameSourcesProvider); + final names = sources.scope( + candidates, + agentPubkeys: agentPubkeys, + fallbackNames: fallbackNames, + ); + // A displayed identity's profile carries its owner hint, so load uncached + // candidates as well as their owners; channels load member profiles + // elsewhere, but a displayed collection has no other loader. + final missing = { + for (final key in names.candidates) + if (!sources.profiles.containsKey(key)) key, + ...sources.missingOwnerProfiles(names.candidates), + }; + if (missing.isNotEmpty) { + Future.microtask(() { + if (ref.context.mounted) { + ref.read(userCacheProvider.notifier).preload(missing.toList()); + } + }); + } + return names; +} diff --git a/mobile/test/features/channels/channel_identity_names_provider_test.dart b/mobile/test/features/channels/channel_identity_names_provider_test.dart new file mode 100644 index 0000000000..b4f8c6d5ab --- /dev/null +++ b/mobile/test/features/channels/channel_identity_names_provider_test.dart @@ -0,0 +1,233 @@ +import 'dart:async'; + +import 'package:buzz/features/channels/channel.dart'; +import 'package:buzz/features/channels/channel_identity_names_provider.dart'; +import 'package:buzz/features/channels/channels_provider.dart'; +import 'package:buzz/shared/profile/user_cache_provider.dart'; +import 'package:buzz/shared/profile/user_profile.dart'; +import 'package:buzz/shared/relay/relay.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +const _channelId = '11111111-1111-4111-8111-111111111111'; +final _human = 'c' * 64; +final _bot = 'd' * 64; + +void main() { + late _MembershipRelaySession session; + late ProviderContainer container; + + setUp(() { + session = _MembershipRelaySession(); + container = ProviderContainer( + retry: (_, _) => null, + overrides: [ + relaySessionProvider.overrideWith(() => session), + relayConfigProvider.overrideWith(_TestRelayConfigNotifier.new), + myPubkeyProvider.overrideWith((ref) => ref.watch(_accountProvider)), + channelsProvider.overrideWith(_EmptyChannelsNotifier.new), + userCacheProvider.overrideWith( + () => _FixedUserCacheNotifier({ + _human: UserProfile(pubkey: _human, displayName: 'Honey'), + _bot: UserProfile(pubkey: _bot, displayName: 'Honey'), + }), + ), + ], + ); + addTearDown(container.dispose); + }); + + ProviderSubscription listenNames() { + final subscription = container.listen( + channelIdentityNamesProvider(_channelId), + (_, _) {}, + ); + addTearDown(subscription.close); + return subscription; + } + + List labels() { + final names = container.read(channelIdentityNamesProvider(_channelId)); + return [names.labelFor(_human), names.labelFor(_bot)]; + } + + Future settle() async { + for (var i = 0; i < 5; i++) { + await container.pump(); + await Future.delayed(Duration.zero); + } + } + + test('keeps the same-scope roster while membership reloads', () async { + listenNames(); + await settle(); + expect(labels(), ['Honey', 'Honey (agent)']); + expect(session.membershipEvents, isNotNull); + + // A live kind:39002 update reloads the roster. Hold that reload open. + final reload = session.holdMemberFetches(); + session.membershipEvents!(_membersEvent()); + await settle(); + expect(session.heldMemberFetches, isPositive); + expect(labels(), ['Honey', 'Honey (agent)']); + + reload.complete(); + await settle(); + expect(labels(), ['Honey', 'Honey (agent)']); + }); + + test('uses cached bot roles when the relay is offline', () async { + final subscription = listenNames(); + await settle(); + expect(labels(), ['Honey', 'Honey (agent)']); + + // Unmount, go offline, and mount again from the cached roster. The + // relay-backed bot lookup and agent directory are empty while offline. + subscription.close(); + await settle(); + session.setStatus(SessionStatus.disconnected); + await settle(); + listenNames(); + await settle(); + + final names = container.read(channelIdentityNamesProvider(_channelId)); + expect(names.candidates, {_human, _bot}); + expect(labels(), ['Honey', 'Honey (agent)']); + }); + + for (final reset in _scopeResets.entries) { + test('drops the roster when the ${reset.key} changes', () async { + listenNames(); + await settle(); + expect( + container.read(channelIdentityNamesProvider(_channelId)).candidates, + {_human, _bot}, + ); + + final reload = session.holdMemberFetches(); + reset.value(container); + await settle(); + expect(session.heldMemberFetches, isPositive); + expect( + container.read(channelIdentityNamesProvider(_channelId)).candidates, + isEmpty, + ); + reload.complete(); + await settle(); + }); + } +} + +final _scopeResets = { + 'relay': (container) => container + .read(relayConfigProvider.notifier) + .update(baseUrl: 'https://second-community.example'), + 'account': (container) => + container.read(_accountProvider.notifier).set('e' * 64), +}; + +NostrEvent _membersEvent() => NostrEvent( + id: 'members', + pubkey: 'owner', + createdAt: 1, + kind: 39002, + tags: [ + const ['d', _channelId], + ['p', _human, '', 'member'], + ['p', _bot, '', 'bot'], + ], + content: '', + sig: 'sig', +); + +final _accountProvider = NotifierProvider<_AccountNotifier, String?>( + _AccountNotifier.new, +); + +class _AccountNotifier extends Notifier { + @override + String? build() => 'a' * 64; + + void set(String pubkey) => state = pubkey; +} + +class _TestRelayConfigNotifier extends RelayConfigNotifier { + @override + RelayConfig build() => + const RelayConfig(baseUrl: 'https://first-community.example'); + + @override + void update({required String baseUrl, String? nsec}) { + state = RelayConfig(baseUrl: baseUrl, nsec: nsec); + } +} + +class _EmptyChannelsNotifier extends ChannelsNotifier { + @override + Future> build() async => const []; +} + +class _FixedUserCacheNotifier extends UserCacheNotifier { + _FixedUserCacheNotifier(this._users); + + final Map _users; + + @override + Map build() => _users; + + @override + Future preload(List pubkeys) async => true; +} + +/// Serves the channel's kind:39002 roster, can hold member fetches open, and +/// exposes the live membership subscription's event callback. +class _MembershipRelaySession extends RelaySessionNotifier { + void Function(NostrEvent)? membershipEvents; + Completer? _hold; + int heldMemberFetches = 0; + + @override + SessionState build() => const SessionState(status: SessionStatus.connected); + + void setStatus(SessionStatus status) => state = SessionState(status: status); + + Completer holdMemberFetches() => _hold = Completer(); + + @override + Future> fetchHistory( + NostrFilter filter, { + Duration timeout = const Duration(seconds: 8), + }) async { + if (!(filter.kinds.contains(39002))) return const []; + if (_hold case final hold?) { + heldMemberFetches++; + await hold.future; + } + return [_membersEvent()]; + } + + @override + Future> queryRelay( + List filters, { + Duration timeout = const Duration(seconds: 8), + }) async => const []; + + @override + Future subscribe( + NostrFilter filter, + void Function(NostrEvent) onEvent, { + void Function(String message)? onClosed, + }) async => () {}; + + @override + Future subscribeWithStatus( + NostrFilter filter, + void Function(NostrEvent) onEvent, { + void Function(String message)? onClosed, + required void Function(RelaySubscriptionStatus status) onStatusChanged, + }) async { + if (filter.kinds.contains(39002)) membershipEvents = onEvent; + onStatusChanged(RelaySubscriptionStatus.ready); + return () {}; + } +} diff --git a/mobile/test/shared/identity_names/identity-names.fixtures.json b/mobile/test/shared/identity_names/identity-names.fixtures.json new file mode 100644 index 0000000000..7bf01af9e0 --- /dev/null +++ b/mobile/test/shared/identity_names/identity-names.fixtures.json @@ -0,0 +1,1787 @@ +{ + "$comment": "Contextual identity names v1: semantic inputs, no signed events. Spec: README.md. Preserve identities order; compare expected as a key-indexed map. null qualifier means no key suffix. Expected outputs are literal, not generated by the policy under test.", + "version": 1, + "cases": [ + { + "name": "unique_foreign_agent", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "2222222222222222222222222222222222222222222222222222222222222222" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey", + "qualifier": null + } + } + }, + { + "name": "human_before_mine_before_others", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "name": "Honey" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "2222222222222222222222222222222222222222222222222222222222222222" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc": { + "name": "Honey", + "qualifier": null + }, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey (agent)", + "qualifier": null + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Wesโ€™s Honey", + "qualifier": null + } + } + }, + { + "name": "mine_before_other_agent", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "2222222222222222222222222222222222222222222222222222222222222222" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey", + "qualifier": null + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Wesโ€™s Honey", + "qualifier": null + } + } + }, + { + "name": "viewer_changes_labels", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "2222222222222222222222222222222222222222222222222222222222222222" + } + ], + "viewer": "2222222222222222222222222222222222222222222222222222222222222222", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Loganโ€™s Honey", + "qualifier": null + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Honey", + "qualifier": null + } + } + }, + { + "name": "my_same_owner_duplicates", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey ยท rcaj", + "qualifier": "rcaj" + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Honey ยท 04hu", + "qualifier": "04hu" + } + } + }, + { + "name": "other_same_owner_duplicates", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "2222222222222222222222222222222222222222222222222222222222222222" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "2222222222222222222222222222222222222222222222222222222222222222" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Wesโ€™s Honey ยท rcaj", + "qualifier": "rcaj" + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Wesโ€™s Honey ยท 04hu", + "qualifier": "04hu" + } + } + }, + { + "name": "human_and_my_duplicates", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "name": "Honey" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc": { + "name": "Honey", + "qualifier": null + }, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey (agent) ยท rcaj", + "qualifier": "rcaj" + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Honey (agent) ยท 04hu", + "qualifier": "04hu" + } + } + }, + { + "name": "viewer_wins_human_collision", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Honey" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "1111111111111111111111111111111111111111111111111111111111111111", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "1111111111111111111111111111111111111111111111111111111111111111": { + "name": "Honey", + "qualifier": null + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Honey ยท 04hu", + "qualifier": "04hu" + } + } + }, + { + "name": "equal_priority_humans", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey ยท rcaj", + "qualifier": "rcaj" + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Honey ยท 04hu", + "qualifier": "04hu" + } + } + }, + { + "name": "literal_owner_prefix", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "2222222222222222222222222222222222222222222222222222222222222222" + }, + { + "pubkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "name": "Wesโ€™s Honey" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey", + "qualifier": null + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Wesโ€™s Honey ยท 04hu", + "qualifier": "04hu" + }, + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc": { + "name": "Wesโ€™s Honey", + "qualifier": null + } + } + }, + { + "name": "literal_agent_marker", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "name": "Honey" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey (agent)" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc": { + "name": "Honey", + "qualifier": null + }, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey (agent) ยท rcaj", + "qualifier": "rcaj" + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Honey (agent)", + "qualifier": null + } + } + }, + { + "name": "defer_suffix_when_another_row_qualifies", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "2222222222222222222222222222222222222222222222222222222222222222" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey", + "qualifier": null + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Wesโ€™s Honey", + "qualifier": null + } + } + }, + { + "name": "unknown_owners", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey ยท rcaj", + "qualifier": "rcaj" + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Honey ยท 04hu", + "qualifier": "04hu" + } + } + }, + { + "name": "unknown_viewer_is_not_mine", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "2222222222222222222222222222222222222222222222222222222222222222" + } + ], + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey", + "qualifier": null + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Wesโ€™s Honey", + "qualifier": null + } + } + }, + { + "name": "missing_owner_name", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "3333333333333333333333333333333333333333333333333333333333333333" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "3333333333333333333333333333333333333333333333333333333333333333" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey ยท rcaj", + "qualifier": "rcaj" + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Honey ยท 04hu", + "qualifier": "04hu" + } + } + }, + { + "name": "duplicate_owner_names", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "3333333333333333333333333333333333333333333333333333333333333333", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "2222222222222222222222222222222222222222222222222222222222222222" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "3333333333333333333333333333333333333333333333333333333333333333" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Wesโ€™s Honey ยท rcaj", + "qualifier": "rcaj" + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Wesโ€™s Honey ยท 04hu", + "qualifier": "04hu" + } + } + }, + { + "name": "extend_only_colliding_suffixes", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "3ee51d04715939ef0e492f7b87bf1dcaf2c0b4a16b753f19d7a92e96ba01b8db", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "55e2be15c0fb4ba8231701c4ba65d545715b7c79761952fd8a7cc75cf6afb602", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "3ee51d04715939ef0e492f7b87bf1dcaf2c0b4a16b753f19d7a92e96ba01b8db", + "55e2be15c0fb4ba8231701c4ba65d545715b7c79761952fd8a7cc75cf6afb602", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "expected": { + "3ee51d04715939ef0e492f7b87bf1dcaf2c0b4a16b753f19d7a92e96ba01b8db": { + "name": "Honey ยท suscycq", + "qualifier": "suscycq" + }, + "55e2be15c0fb4ba8231701c4ba65d545715b7c79761952fd8a7cc75cf6afb602": { + "name": "Honey ยท quscycq", + "qualifier": "quscycq" + }, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey ยท rcaj", + "qualifier": "rcaj" + } + } + }, + { + "name": "literal_suffix_keeps_human_priority", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "name": "Honey ยท rcaj" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey ยท mrcaj", + "qualifier": "mrcaj" + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Honey ยท 04hu", + "qualifier": "04hu" + }, + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc": { + "name": "Honey ยท rcaj", + "qualifier": null + } + } + }, + { + "name": "trim_but_no_case_fold", + "identities": [ + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "๏ปฟ Honeyย " + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "honey" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey", + "qualifier": null + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "honey", + "qualifier": null + } + } + }, + { + "name": "trim_creates_collision", + "identities": [ + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": " Honey " + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey ยท rcaj", + "qualifier": "rcaj" + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Honey ยท 04hu", + "qualifier": "04hu" + } + } + }, + { + "name": "no_unicode_normalization", + "identities": [ + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "รฉ" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "eฬ" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "รฉ", + "qualifier": null + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "eฬ", + "qualifier": null + } + } + }, + { + "name": "normalize_keys_and_deduplicate", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "name": "Viewer" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + }, + { + "pubkey": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "2222222222222222222222222222222222222222222222222222222222222222" + } + ], + "viewer": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", + "candidates": [ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey", + "qualifier": null + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Wesโ€™s Honey", + "qualifier": null + } + } + }, + { + "name": "unselected_namesake_does_not_compete", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey", + "qualifier": null + } + } + }, + { + "name": "empty_candidate_set", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [], + "expected": {} + }, + { + "name": "absent_candidate_fact", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": {} + }, + { + "name": "omitted_candidates_selects_all", + "identities": [ + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey ยท rcaj", + "qualifier": "rcaj" + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Honey ยท 04hu", + "qualifier": "04hu" + } + } + }, + { + "name": "all_aliases_compete_last_alias_returned", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Juniper", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Juniper", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Juniper ยท rcaj", + "qualifier": "rcaj" + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Juniper ยท 04hu", + "qualifier": "04hu" + } + } + }, + { + "name": "one_key_aliases_are_not_competitors", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Juniper", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey", + "qualifier": null + } + } + }, + { + "name": "nonreturned_literal_alias_still_competes", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "name": "Honey ยท rcaj" + }, + { + "pubkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "name": "Juniper" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey ยท mrcaj", + "qualifier": "mrcaj" + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Honey ยท 04hu", + "qualifier": "04hu" + }, + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc": { + "name": "Juniper", + "qualifier": null + } + } + }, + { + "name": "preferred_owner_alias_is_raw", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "James" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "2222222222222222222222222222222222222222222222222222222222222222" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey", + "qualifier": null + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Jamesโ€™s Honey", + "qualifier": null + } + } + }, + { + "name": "full_key_and_counters_occupied", + "identities": [ + { + "pubkey": "1111111111111111111111111111111111111111111111111111111111111111", + "name": "Logan" + }, + { + "pubkey": "2222222222222222222222222222222222222222222222222222222222222222", + "name": "Wes" + }, + { + "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "name": "Honey", + "isAgent": true, + "ownerPubkey": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000010", + "name": "Honey ยท rcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000011", + "name": "Honey ยท mrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000012", + "name": "Honey ยท amrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000013", + "name": "Honey ยท qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000014", + "name": "Honey ยท 4qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000015", + "name": "Honey ยท 24qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000016", + "name": "Honey ยท 424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000017", + "name": "Honey ยท 2424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000018", + "name": "Honey ยท 42424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000019", + "name": "Honey ยท 242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000001a", + "name": "Honey ยท 4242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000001b", + "name": "Honey ยท 24242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000001c", + "name": "Honey ยท 424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000001d", + "name": "Honey ยท 2424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000001e", + "name": "Honey ยท 42424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000001f", + "name": "Honey ยท 242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000020", + "name": "Honey ยท 4242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000021", + "name": "Honey ยท 24242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000022", + "name": "Honey ยท 424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000023", + "name": "Honey ยท 2424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000024", + "name": "Honey ยท 42424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000025", + "name": "Honey ยท 242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000026", + "name": "Honey ยท 4242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000027", + "name": "Honey ยท 24242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000028", + "name": "Honey ยท 424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000029", + "name": "Honey ยท 2424242424242424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000002a", + "name": "Honey ยท 42424242424242424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000002b", + "name": "Honey ยท 242424242424242424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000002c", + "name": "Honey ยท 4242424242424242424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000002d", + "name": "Honey ยท 24242424242424242424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000002e", + "name": "Honey ยท 424242424242424242424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000002f", + "name": "Honey ยท 2424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000030", + "name": "Honey ยท 42424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000031", + "name": "Honey ยท 242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000032", + "name": "Honey ยท 4242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000033", + "name": "Honey ยท 24242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000034", + "name": "Honey ยท 424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000035", + "name": "Honey ยท 2424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000036", + "name": "Honey ยท 42424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000037", + "name": "Honey ยท 242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000038", + "name": "Honey ยท 4242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000039", + "name": "Honey ยท 24242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000003a", + "name": "Honey ยท 424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000003b", + "name": "Honey ยท 2424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000003c", + "name": "Honey ยท 42424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000003d", + "name": "Honey ยท 242424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000003e", + "name": "Honey ยท 4242424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000003f", + "name": "Honey ยท 24242424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000040", + "name": "Honey ยท 424242424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000041", + "name": "Honey ยท 2424242424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000042", + "name": "Honey ยท 42424242424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000043", + "name": "Honey ยท 242424242424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000044", + "name": "Honey ยท 4242424242424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000045", + "name": "Honey ยท 24242424242424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000046", + "name": "Honey ยท 424242424242424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000047", + "name": "Honey ยท 1424242424242424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000048", + "name": "Honey ยท b1424242424242424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "0000000000000000000000000000000000000000000000000000000000000049", + "name": "Honey ยท ub1424242424242424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000004a", + "name": "Honey ยท pub1424242424242424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000004b", + "name": "Honey ยท npub1424242424242424242424242424242424242424242424242424qamrcaj" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000004c", + "name": "Honey ยท npub1424242424242424242424242424242424242424242424242424qamrcaj ยท 1" + }, + { + "pubkey": "000000000000000000000000000000000000000000000000000000000000004d", + "name": "Honey ยท npub1424242424242424242424242424242424242424242424242424qamrcaj ยท 2" + } + ], + "viewer": "1111111111111111111111111111111111111111111111111111111111111111", + "candidates": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "0000000000000000000000000000000000000000000000000000000000000010", + "0000000000000000000000000000000000000000000000000000000000000011", + "0000000000000000000000000000000000000000000000000000000000000012", + "0000000000000000000000000000000000000000000000000000000000000013", + "0000000000000000000000000000000000000000000000000000000000000014", + "0000000000000000000000000000000000000000000000000000000000000015", + "0000000000000000000000000000000000000000000000000000000000000016", + "0000000000000000000000000000000000000000000000000000000000000017", + "0000000000000000000000000000000000000000000000000000000000000018", + "0000000000000000000000000000000000000000000000000000000000000019", + "000000000000000000000000000000000000000000000000000000000000001a", + "000000000000000000000000000000000000000000000000000000000000001b", + "000000000000000000000000000000000000000000000000000000000000001c", + "000000000000000000000000000000000000000000000000000000000000001d", + "000000000000000000000000000000000000000000000000000000000000001e", + "000000000000000000000000000000000000000000000000000000000000001f", + "0000000000000000000000000000000000000000000000000000000000000020", + "0000000000000000000000000000000000000000000000000000000000000021", + "0000000000000000000000000000000000000000000000000000000000000022", + "0000000000000000000000000000000000000000000000000000000000000023", + "0000000000000000000000000000000000000000000000000000000000000024", + "0000000000000000000000000000000000000000000000000000000000000025", + "0000000000000000000000000000000000000000000000000000000000000026", + "0000000000000000000000000000000000000000000000000000000000000027", + "0000000000000000000000000000000000000000000000000000000000000028", + "0000000000000000000000000000000000000000000000000000000000000029", + "000000000000000000000000000000000000000000000000000000000000002a", + "000000000000000000000000000000000000000000000000000000000000002b", + "000000000000000000000000000000000000000000000000000000000000002c", + "000000000000000000000000000000000000000000000000000000000000002d", + "000000000000000000000000000000000000000000000000000000000000002e", + "000000000000000000000000000000000000000000000000000000000000002f", + "0000000000000000000000000000000000000000000000000000000000000030", + "0000000000000000000000000000000000000000000000000000000000000031", + "0000000000000000000000000000000000000000000000000000000000000032", + "0000000000000000000000000000000000000000000000000000000000000033", + "0000000000000000000000000000000000000000000000000000000000000034", + "0000000000000000000000000000000000000000000000000000000000000035", + "0000000000000000000000000000000000000000000000000000000000000036", + "0000000000000000000000000000000000000000000000000000000000000037", + "0000000000000000000000000000000000000000000000000000000000000038", + "0000000000000000000000000000000000000000000000000000000000000039", + "000000000000000000000000000000000000000000000000000000000000003a", + "000000000000000000000000000000000000000000000000000000000000003b", + "000000000000000000000000000000000000000000000000000000000000003c", + "000000000000000000000000000000000000000000000000000000000000003d", + "000000000000000000000000000000000000000000000000000000000000003e", + "000000000000000000000000000000000000000000000000000000000000003f", + "0000000000000000000000000000000000000000000000000000000000000040", + "0000000000000000000000000000000000000000000000000000000000000041", + "0000000000000000000000000000000000000000000000000000000000000042", + "0000000000000000000000000000000000000000000000000000000000000043", + "0000000000000000000000000000000000000000000000000000000000000044", + "0000000000000000000000000000000000000000000000000000000000000045", + "0000000000000000000000000000000000000000000000000000000000000046", + "0000000000000000000000000000000000000000000000000000000000000047", + "0000000000000000000000000000000000000000000000000000000000000048", + "0000000000000000000000000000000000000000000000000000000000000049", + "000000000000000000000000000000000000000000000000000000000000004a", + "000000000000000000000000000000000000000000000000000000000000004b", + "000000000000000000000000000000000000000000000000000000000000004c", + "000000000000000000000000000000000000000000000000000000000000004d" + ], + "expected": { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "name": "Honey ยท npub1424242424242424242424242424242424242424242424242424qamrcaj ยท 3", + "qualifier": "npub1424242424242424242424242424242424242424242424242424qamrcaj ยท 3" + }, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "name": "Honey ยท 04hu", + "qualifier": "04hu" + }, + "0000000000000000000000000000000000000000000000000000000000000010": { + "name": "Honey ยท rcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000011": { + "name": "Honey ยท mrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000012": { + "name": "Honey ยท amrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000013": { + "name": "Honey ยท qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000014": { + "name": "Honey ยท 4qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000015": { + "name": "Honey ยท 24qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000016": { + "name": "Honey ยท 424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000017": { + "name": "Honey ยท 2424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000018": { + "name": "Honey ยท 42424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000019": { + "name": "Honey ยท 242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000001a": { + "name": "Honey ยท 4242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000001b": { + "name": "Honey ยท 24242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000001c": { + "name": "Honey ยท 424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000001d": { + "name": "Honey ยท 2424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000001e": { + "name": "Honey ยท 42424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000001f": { + "name": "Honey ยท 242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000020": { + "name": "Honey ยท 4242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000021": { + "name": "Honey ยท 24242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000022": { + "name": "Honey ยท 424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000023": { + "name": "Honey ยท 2424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000024": { + "name": "Honey ยท 42424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000025": { + "name": "Honey ยท 242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000026": { + "name": "Honey ยท 4242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000027": { + "name": "Honey ยท 24242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000028": { + "name": "Honey ยท 424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000029": { + "name": "Honey ยท 2424242424242424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000002a": { + "name": "Honey ยท 42424242424242424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000002b": { + "name": "Honey ยท 242424242424242424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000002c": { + "name": "Honey ยท 4242424242424242424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000002d": { + "name": "Honey ยท 24242424242424242424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000002e": { + "name": "Honey ยท 424242424242424242424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000002f": { + "name": "Honey ยท 2424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000030": { + "name": "Honey ยท 42424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000031": { + "name": "Honey ยท 242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000032": { + "name": "Honey ยท 4242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000033": { + "name": "Honey ยท 24242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000034": { + "name": "Honey ยท 424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000035": { + "name": "Honey ยท 2424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000036": { + "name": "Honey ยท 42424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000037": { + "name": "Honey ยท 242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000038": { + "name": "Honey ยท 4242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000039": { + "name": "Honey ยท 24242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000003a": { + "name": "Honey ยท 424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000003b": { + "name": "Honey ยท 2424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000003c": { + "name": "Honey ยท 42424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000003d": { + "name": "Honey ยท 242424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000003e": { + "name": "Honey ยท 4242424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000003f": { + "name": "Honey ยท 24242424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000040": { + "name": "Honey ยท 424242424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000041": { + "name": "Honey ยท 2424242424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000042": { + "name": "Honey ยท 42424242424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000043": { + "name": "Honey ยท 242424242424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000044": { + "name": "Honey ยท 4242424242424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000045": { + "name": "Honey ยท 24242424242424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000046": { + "name": "Honey ยท 424242424242424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000047": { + "name": "Honey ยท 1424242424242424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000048": { + "name": "Honey ยท b1424242424242424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "0000000000000000000000000000000000000000000000000000000000000049": { + "name": "Honey ยท ub1424242424242424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000004a": { + "name": "Honey ยท pub1424242424242424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000004b": { + "name": "Honey ยท npub1424242424242424242424242424242424242424242424242424qamrcaj", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000004c": { + "name": "Honey ยท npub1424242424242424242424242424242424242424242424242424qamrcaj ยท 1", + "qualifier": null + }, + "000000000000000000000000000000000000000000000000000000000000004d": { + "name": "Honey ยท npub1424242424242424242424242424242424242424242424242424qamrcaj ยท 2", + "qualifier": null + } + } + } + ] +} diff --git a/mobile/test/shared/identity_names/identity_name_policy_test.dart b/mobile/test/shared/identity_names/identity_name_policy_test.dart new file mode 100644 index 0000000000..5c93d785cc --- /dev/null +++ b/mobile/test/shared/identity_names/identity_name_policy_test.dart @@ -0,0 +1,129 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:buzz/shared/identity_names/identity_name_policy.dart'; +import 'package:flutter_test/flutter_test.dart'; + +// Vendored, unmodified copy of the portable fixtures pinned in +// identity_name_policy.dart. Expected results are stored literals. +const _fixturePath = 'test/shared/identity_names/identity-names.fixtures.json'; + +void main() { + final conformance = + jsonDecode(File(_fixturePath).readAsStringSync()) as Map; + final cases = (conformance['cases'] as List).cast>(); + + test('validates the portable fixture version and non-empty case list', () { + expect(conformance['version'], identityNamePolicyVersion); + expect(cases, isNotEmpty); + }); + + for (final fixture in cases) { + test('conforms to the portable naming contract: ${fixture['name']}', () { + final identities = [ + for (final raw + in (fixture['identities'] as List).cast>()) + NamingIdentity( + pubkey: raw['pubkey'] as String, + name: raw['name'] as String, + isAgent: raw['isAgent'] as bool? ?? false, + ownerPubkey: raw['ownerPubkey'] as String?, + ), + ]; + final actual = resolveIdentityNames( + identities, + viewer: fixture['viewer'] as String?, + candidates: (fixture['candidates'] as List?)?.cast(), + ); + expect({ + for (final MapEntry(:key, :value) in actual.entries) + key: {'name': value.name, 'qualifier': value.qualifier}, + }, fixture['expected']); + }); + } + + test('trims only the contract whitespace set', () { + const key = + '1111111111111111111111111111111111111111111111111111111111111111'; + // U+0085 is not ECMAScript whitespace, unlike Dart String.trim. + final result = resolveIdentityNames([ + const NamingIdentity(pubkey: key, name: '\u0085Honey\u3000'), + ]); + expect(result[key], const ResolvedIdentityName('\u0085Honey')); + }); + + group('rejects an invalid key before resolving', () { + const valid = + '1111111111111111111111111111111111111111111111111111111111111111'; + + test('for a fact that needs no suffix', () { + expect( + () => resolveIdentityNames(const [ + NamingIdentity(pubkey: 'not-a-key', name: 'Unique'), + ]), + throwsArgumentError, + ); + }); + + test('for a colliding fact', () { + expect( + () => resolveIdentityNames(const [ + NamingIdentity(pubkey: 'not-a-key', name: 'Honey'), + NamingIdentity(pubkey: valid, name: 'Honey'), + ]), + throwsArgumentError, + ); + }); + + test('for an owner key', () { + expect( + () => resolveIdentityNames(const [ + NamingIdentity( + pubkey: valid, + name: 'Scout', + isAgent: true, + ownerPubkey: 'not-a-key', + ), + ]), + throwsArgumentError, + ); + }); + + test('for the viewer', () { + expect( + () => resolveIdentityNames(const [ + NamingIdentity(pubkey: valid, name: 'Honey'), + ], viewer: 'not-a-key'), + throwsArgumentError, + ); + }); + + test('for a candidate', () { + expect( + () => resolveIdentityNames( + const [NamingIdentity(pubkey: valid, name: 'Honey')], + candidates: const [valid, 'not-a-key'], + ), + throwsArgumentError, + ); + }); + + test('for a 64-character value that is not hex', () { + expect( + () => resolveIdentityNames([ + NamingIdentity(pubkey: 'g' * 64, name: 'Unique'), + ]), + throwsArgumentError, + ); + }); + + test('but accepts an upper-case hex key', () { + expect( + resolveIdentityNames([ + NamingIdentity(pubkey: valid.replaceAll('1', 'A'), name: 'Honey'), + ]), + {valid.replaceAll('1', 'a'): const ResolvedIdentityName('Honey')}, + ); + }); + }); +} diff --git a/mobile/test/shared/identity_names/identity_names_test.dart b/mobile/test/shared/identity_names/identity_names_test.dart new file mode 100644 index 0000000000..1c10277525 --- /dev/null +++ b/mobile/test/shared/identity_names/identity_names_test.dart @@ -0,0 +1,100 @@ +import 'package:buzz/shared/identity_names/identity_names.dart'; +import 'package:buzz/shared/profile/user_profile.dart'; +import 'package:buzz/shared/utils/string_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; + +String _key(String digit) => digit * 64; + +final _logan = _key('1'); +final _wes = _key('2'); +final _human = _key('c'); +final _mine = _key('a'); +final _wesAgent = _key('b'); + +IdentityNameSources _sources() => IdentityNameSources( + viewer: _logan, + profiles: { + _logan: UserProfile(pubkey: _logan, displayName: 'Logan'), + _wes: UserProfile(pubkey: _wes, displayName: 'Wes'), + _human: UserProfile(pubkey: _human, displayName: 'Honey'), + _mine: UserProfile( + pubkey: _mine, + displayName: 'Honey', + ownerPubkey: _logan, + ), + _wesAgent: UserProfile( + pubkey: _wesAgent, + displayName: 'Honey', + ownerPubkey: _wes, + ), + }, +); + +void main() { + test('channel context: human, then mine, then owner-qualified others', () { + // Wes is not a member: his profile is only an owner lookup fact. + final names = _sources().scope([_logan, _human, _mine, _wesAgent]); + expect(names.labelFor(_human), 'Honey'); + expect(names.labelFor(_mine), 'Honey (agent)'); + expect(names.labelFor(_wesAgent), 'Wesโ€™s Honey'); + expect(names.labelFor(_logan), 'Logan'); + }); + + test('a non-member reference is compared with the members plus itself', () { + final names = _sources().scope([_human]); + // Alone, Logan's agent is plain; against the human it is marked. + expect(_sources().scope([_mine]).labelFor(_mine), 'Honey'); + expect(names.labelFor(_mine), 'Honey (agent)'); + expect(names.labelFor(_human), 'Honey'); + }); + + test('profiles outside the context do not create collisions', () { + final names = _sources().scope([_wesAgent]); + expect(names.labelFor(_wesAgent), 'Honey'); + }); + + test('view-local agent roles and fallback names become facts', () { + final bot = _key('d'); + final names = IdentityNameSources( + profiles: {_human: UserProfile(pubkey: _human, displayName: 'Honey')}, + ).scope([_human, bot], agentPubkeys: {bot}, fallbackNames: {bot: 'Honey'}); + expect(names.labelFor(bot), 'Honey (agent)'); + expect(names.labelFor(_human), 'Honey'); + }); + + test( + 'blank names fall back to the compact npub; invalid keys stay plain', + () { + final names = IdentityNameSources( + profiles: { + _human: UserProfile(pubkey: _human, displayName: ' '), + 'legacy': const UserProfile(pubkey: 'legacy', displayName: 'Honey'), + }, + ).scope([_human, 'not-a-key', 'legacy']); + expect(names.candidates, {_human}); + expect(names.labelFor(_human), shortPubkey(_human)); + expect(names.labelFor('not-a-key'), unknownIdentityLabel); + // A malformed key is not an identity fact: plain name, no qualifier. + expect(names.labelFor('legacy'), 'Honey'); + }, + ); + + test('reports owner profiles to load, without inventing owner names', () { + final sources = IdentityNameSources( + viewer: _logan, + profiles: { + _human: UserProfile(pubkey: _human, displayName: 'Honey'), + _wesAgent: UserProfile( + pubkey: _wesAgent, + displayName: 'Honey', + ownerPubkey: _wes, + ), + }, + ); + expect(sources.missingOwnerProfiles([_human, _wesAgent]), {_wes}); + expect( + sources.scope([_human, _wesAgent]).labelFor(_wesAgent), + 'Honey (agent)', + ); + }); +}