mirror of
https://github.com/block/buzz.git
synced 2026-11-06 17:31:19 +00:00
An empty Bestie slot cycles still posters from Fuzzies, Gloopies and Figgies every five seconds, without creating video players. The next poster is precached; reduced motion and hidden-screen pauses are preserved. The launcher uses the neutral “Bestie” label until a selection is available, then “Chat with …”. For supported communities with no owned agents, tapping opens Create agent directly. A successful first-agent setup saves that agent as Bestie without replacing an existing preference; existing-agent and unsupported-community paths keep the Agents list. Verified Builderlab registrations can show sleeping remote agents as Online. Owner-scoped saved runtime availability now prevents a known absent or deleted runtime from returning Online after Builderlab sign-out or a fresh load, even when another community retains its registration. Validation: mobile static checks; 4,274 Flutter tests passed with hosted flows enabled; launcher → cancel → create → Home “Chat with Scout”, including encrypted persisted selection; sign-out, fresh-load, owner isolation and deletion regressions. Restoring the old carousel produced three video-player requests; removing the saved-availability guard reproduced incorrect Online after sign-out. The earlier iPhone recording below shows the original carousel and entry flow and stops before creation. The new poster-only behavior and completed creation are covered by widget tests, not a new device recording. https://github.com/user-attachments/assets/94fcff72-967a-44c2-99d5-27c715e196a2 --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
141 lines
4.8 KiB
Dart
141 lines
4.8 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:nostr/nostr.dart' as nostr;
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:buzz/shared/crypto/nip44.dart';
|
|
import 'package:buzz/shared/me/default_agent.dart';
|
|
import 'package:buzz/shared/me/default_agent_writer.dart';
|
|
import 'package:buzz/shared/me/me_sync.dart';
|
|
import 'package:buzz/shared/relay/relay.dart';
|
|
import 'me_sync_test.dart' show FakeRelay;
|
|
|
|
void main() {
|
|
const secret =
|
|
'0000000000000000000000000000000000000000000000000000000000000001';
|
|
const origin = 'https://relay.test';
|
|
final public = nostr.Keys(secret).public;
|
|
final config = RelayConfig(
|
|
baseUrl: origin,
|
|
nsec: nostr.Nip19.encode(prefix: nostr.Nip19Prefix.nsec, data: secret),
|
|
);
|
|
NostrEvent signed(
|
|
String? choice,
|
|
int time, {
|
|
String coordinate = 'main-agent',
|
|
bool missing = false,
|
|
}) => NostrEvent.fromJson(
|
|
nostr.Event.from(
|
|
kind: 30078,
|
|
content: nip44Encrypt(
|
|
getConversationKey(secret, public),
|
|
jsonEncode({'version': 1, if (!missing) 'agent': choice}),
|
|
),
|
|
secretKey: secret,
|
|
createdAt: time,
|
|
tags: [
|
|
['d', coordinate],
|
|
['t', 'main-agent'],
|
|
],
|
|
verify: false,
|
|
).toMap(),
|
|
);
|
|
|
|
test('writes the desktop main-agent wire format, including clear', () async {
|
|
final relay = FakeRelay();
|
|
final sync = MeSync(relay, config);
|
|
for (final choice in ['a' * 64, null]) {
|
|
relay.events = [];
|
|
await saveDefaultAgent(sync, choice);
|
|
final event = relay.events.single;
|
|
expect(event.kind, 30078);
|
|
expect(event.pubkey, public);
|
|
expect(event.tags, [
|
|
['d', 'main-agent'],
|
|
['t', 'main-agent'],
|
|
]);
|
|
expect(jsonDecode(nip44Decrypt(sync.key, event.content)), {
|
|
'version': 1,
|
|
'agent': choice,
|
|
});
|
|
}
|
|
});
|
|
|
|
test(
|
|
'first-agent selection never replaces an existing choice or clear',
|
|
() async {
|
|
for (final choice in ['b' * 64, null]) {
|
|
final previous = signed(choice, 1);
|
|
final relay = FakeRelay()..events = [previous];
|
|
await saveDefaultAgent(
|
|
MeSync(relay, config),
|
|
'a' * 64,
|
|
onlyIfUnset: true,
|
|
);
|
|
expect(relay.events.single.id, previous.id);
|
|
}
|
|
final relay = FakeRelay();
|
|
final sync = MeSync(relay, config);
|
|
await saveDefaultAgent(sync, 'a' * 64, onlyIfUnset: true);
|
|
expect(decodeDefaultAgent(relay.events.single, sync), 'a' * 64);
|
|
// Retrying a completed setup must be idempotent even in the same second.
|
|
await saveDefaultAgent(sync, 'a' * 64, onlyIfUnset: true);
|
|
expect(relay.events, hasLength(1));
|
|
},
|
|
);
|
|
|
|
test(
|
|
'only signed owner preferences at the desktop coordinate and exact schema are accepted',
|
|
() {
|
|
final sync = MeSync(FakeRelay(), config);
|
|
final valid = signed('a' * 64, 1);
|
|
expect(decodeDefaultAgent(valid, sync), 'a' * 64);
|
|
expect(decodeDefaultAgent(signed(null, 2), sync), isNull);
|
|
for (final invalid in [
|
|
signed('a' * 64, 1, coordinate: 'buzz-default-agent-v1:legacy'),
|
|
signed(null, 1, missing: true),
|
|
signed('invalid', 1),
|
|
NostrEvent.fromJson({...valid.toJson(), 'sig': '0' * 128}),
|
|
NostrEvent.fromJson({...valid.toJson(), 'pubkey': 'b' * 64}),
|
|
]) {
|
|
expect(() => decodeDefaultAgent(invalid, sync), throwsA(anything));
|
|
}
|
|
},
|
|
);
|
|
|
|
test(
|
|
'live choice wins over late history, persists offline, and can be cleared',
|
|
() async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final history = Completer<List<NostrEvent>>();
|
|
final relay = FakeRelay()..pendingRead = history.future;
|
|
final sync = MeSync(relay, config, prefs: prefs);
|
|
final container = ProviderContainer(
|
|
overrides: [
|
|
meSyncProvider.overrideWithValue(sync),
|
|
relaySessionProvider.overrideWith(() => relay),
|
|
],
|
|
);
|
|
addTearDown(container.dispose);
|
|
final seen = <String?>[];
|
|
container.listen(defaultAgentProvider, (_, next) {
|
|
if (next.hasValue) seen.add(next.value);
|
|
});
|
|
await Future<void>.delayed(Duration.zero);
|
|
relay.emit!(signed('b' * 64, 2));
|
|
history.complete([signed('a' * 64, 1)]);
|
|
await container.read(defaultAgentProvider.future);
|
|
await Future<void>.delayed(Duration.zero);
|
|
expect(container.read(defaultAgentProvider).value, 'b' * 64);
|
|
expect(seen, isNot(contains('a' * 64)));
|
|
expect(prefs.getString('main-agent:$origin:$public'), isNotNull);
|
|
relay.emit!(signed(null, 3));
|
|
await Future<void>.delayed(Duration.zero);
|
|
expect(container.read(defaultAgentProvider).value, isNull);
|
|
expect(relay.writes, 0);
|
|
},
|
|
);
|
|
}
|