mirror of
https://github.com/block/buzz.git
synced 2026-11-06 01:11:33 +00:00
## Summary - add foreground mobile Huddles on Android and iOS with native Opus capture/playback, mute, speaker routing, participants, lifecycle, and minimized drawer UI - keep mobile Huddle cards and roster state live, including ended rooms, relay-resolved profiles, and agents - broadcast desktop agent TTS through the existing Huddle audio protocol ## Scope Foreground human-to-human voice MVP only. Agent setup/transcripts, background calling, recording, and advanced device controls remain out of scope. ## Validation - `just mobile-check` - `just mobile-test` — 1,500 passed - `just desktop-check` and `just desktop-test` — 4,957 passed - desktop typecheck, strict Clippy, and Tauri tests — 2,445 passed, 15 ignored - mobile worktree identity contract checks - physical Pixel/iPhone behavior reviewed during development --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: leader <71e9f2c44a6932b6772caaaccda1911d010463c3e2c6c40410b8329956046801@buzz.block.builderlab.xyz> Co-authored-by: Tom Brow <tomb@block.xyz> Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>
291 lines
9.3 KiB
Dart
291 lines
9.3 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:buzz/shared/community/community.dart';
|
|
import 'package:buzz/shared/community/community_provider.dart';
|
|
import 'package:buzz/shared/community/community_storage.dart';
|
|
|
|
import 'community_storage_test.dart';
|
|
|
|
void main() {
|
|
late FakeSecureStorage fakeSecure;
|
|
late CommunityStorage communityStorage;
|
|
late ProviderContainer container;
|
|
|
|
setUp(() {
|
|
fakeSecure = FakeSecureStorage();
|
|
communityStorage = CommunityStorage(secure: fakeSecure);
|
|
});
|
|
|
|
tearDown(() => container.dispose());
|
|
|
|
ProviderContainer createContainer() {
|
|
return ProviderContainer(
|
|
overrides: [communityStorageProvider.overrideWithValue(communityStorage)],
|
|
);
|
|
}
|
|
|
|
group('CommunityListNotifier', () {
|
|
test('loads empty list initially', () async {
|
|
container = createContainer();
|
|
final communities = await container.read(communityListProvider.future);
|
|
expect(communities, isEmpty);
|
|
});
|
|
|
|
test('addCommunity adds to list', () async {
|
|
container = createContainer();
|
|
await container.read(communityListProvider.future);
|
|
|
|
final ws = Community.create(
|
|
name: 'Test',
|
|
relayUrl: 'https://test.example.com',
|
|
);
|
|
await container.read(communityListProvider.notifier).addCommunity(ws);
|
|
|
|
final communities = await container.read(communityListProvider.future);
|
|
expect(communities, hasLength(1));
|
|
expect(communities.first.name, 'Test');
|
|
});
|
|
|
|
test('removeCommunity removes from list', () async {
|
|
container = createContainer();
|
|
await container.read(communityListProvider.future);
|
|
|
|
final ws = Community.create(
|
|
name: 'Test',
|
|
relayUrl: 'https://test.example.com',
|
|
);
|
|
await container.read(communityListProvider.notifier).addCommunity(ws);
|
|
await container
|
|
.read(communityListProvider.notifier)
|
|
.removeCommunity(ws.id);
|
|
|
|
final communities = await container.read(communityListProvider.future);
|
|
expect(communities, isEmpty);
|
|
});
|
|
|
|
test(
|
|
'waits for transition teardown before removing active community',
|
|
() async {
|
|
container = createContainer();
|
|
await container.read(communityListProvider.future);
|
|
|
|
final ws1 = Community.create(
|
|
name: 'One',
|
|
relayUrl: 'https://one.example.com',
|
|
);
|
|
final ws2 = Community.create(
|
|
name: 'Two',
|
|
relayUrl: 'https://two.example.com',
|
|
);
|
|
final notifier = container.read(communityListProvider.notifier);
|
|
await notifier.addCommunity(ws1);
|
|
await notifier.addCommunity(ws2);
|
|
await notifier.switchCommunity(ws1.id);
|
|
final teardown = Completer<void>();
|
|
container
|
|
.read(communityTransitionProvider)
|
|
.register(() => teardown.future);
|
|
|
|
final removing = notifier.removeCommunity(ws1.id);
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
expect(await communityStorage.loadActiveId(), ws1.id);
|
|
expect(
|
|
(await communityStorage.loadAll()).map((community) => community.id),
|
|
contains(ws1.id),
|
|
);
|
|
teardown.complete();
|
|
await removing;
|
|
expect(await communityStorage.loadActiveId(), ws2.id);
|
|
expect(
|
|
(await communityStorage.loadAll()).map((community) => community.id),
|
|
isNot(contains(ws1.id)),
|
|
);
|
|
},
|
|
);
|
|
|
|
test('renameCommunity updates name', () async {
|
|
container = createContainer();
|
|
await container.read(communityListProvider.future);
|
|
|
|
final ws = Community.create(
|
|
name: 'Original',
|
|
relayUrl: 'https://test.example.com',
|
|
);
|
|
await container.read(communityListProvider.notifier).addCommunity(ws);
|
|
await container
|
|
.read(communityListProvider.notifier)
|
|
.renameCommunity(ws.id, 'Renamed');
|
|
|
|
final communities = await container.read(communityListProvider.future);
|
|
expect(communities.first.name, 'Renamed');
|
|
});
|
|
|
|
test('waits for transition teardown before updating active ID', () async {
|
|
container = createContainer();
|
|
await container.read(communityListProvider.future);
|
|
|
|
final ws1 = Community.create(
|
|
name: 'One',
|
|
relayUrl: 'https://one.example.com',
|
|
);
|
|
final ws2 = Community.create(
|
|
name: 'Two',
|
|
relayUrl: 'https://two.example.com',
|
|
);
|
|
final notifier = container.read(communityListProvider.notifier);
|
|
await notifier.addCommunity(ws1);
|
|
await notifier.addCommunity(ws2);
|
|
await notifier.switchCommunity(ws1.id);
|
|
final teardown = Completer<void>();
|
|
container
|
|
.read(communityTransitionProvider)
|
|
.register(() => teardown.future);
|
|
|
|
final switching = notifier.switchCommunity(ws2.id);
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
expect(await communityStorage.loadActiveId(), ws1.id);
|
|
teardown.complete();
|
|
await switching;
|
|
expect(await communityStorage.loadActiveId(), ws2.id);
|
|
});
|
|
|
|
test('overlapping transitions share one callback run', () async {
|
|
container = createContainer();
|
|
final coordinator = container.read(communityTransitionProvider);
|
|
final teardown = Completer<void>();
|
|
var calls = 0;
|
|
coordinator.register(() {
|
|
calls++;
|
|
return teardown.future;
|
|
});
|
|
|
|
var firstCompleted = false;
|
|
var secondCompleted = false;
|
|
final first = coordinator.run().then((_) => firstCompleted = true);
|
|
final second = coordinator.run().then((_) => secondCompleted = true);
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
expect(calls, 1);
|
|
expect(firstCompleted, isFalse);
|
|
expect(secondCompleted, isFalse);
|
|
teardown.complete();
|
|
await Future.wait([first, second]);
|
|
expect(firstCompleted, isTrue);
|
|
expect(secondCompleted, isTrue);
|
|
});
|
|
|
|
test('a later transition starts a new callback run', () async {
|
|
container = createContainer();
|
|
final coordinator = container.read(communityTransitionProvider);
|
|
var calls = 0;
|
|
coordinator.register(() async => calls++);
|
|
|
|
await coordinator.run();
|
|
await coordinator.run();
|
|
|
|
expect(calls, 2);
|
|
});
|
|
|
|
test(
|
|
'transition callback failure does not block the active ID update',
|
|
() async {
|
|
container = createContainer();
|
|
await container.read(communityListProvider.future);
|
|
|
|
final ws1 = Community.create(
|
|
name: 'One',
|
|
relayUrl: 'https://one.example.com',
|
|
);
|
|
final ws2 = Community.create(
|
|
name: 'Two',
|
|
relayUrl: 'https://two.example.com',
|
|
);
|
|
final notifier = container.read(communityListProvider.notifier);
|
|
await notifier.addCommunity(ws1);
|
|
await notifier.addCommunity(ws2);
|
|
await notifier.switchCommunity(ws1.id);
|
|
container.read(communityTransitionProvider).register(() async {
|
|
throw StateError('native cleanup failed');
|
|
});
|
|
|
|
await notifier.switchCommunity(ws2.id);
|
|
|
|
expect(await communityStorage.loadActiveId(), ws2.id);
|
|
},
|
|
);
|
|
|
|
test('switchCommunity updates active ID', () async {
|
|
container = createContainer();
|
|
await container.read(communityListProvider.future);
|
|
|
|
final ws1 = Community.create(
|
|
name: 'One',
|
|
relayUrl: 'https://one.example.com',
|
|
);
|
|
final ws2 = Community.create(
|
|
name: 'Two',
|
|
relayUrl: 'https://two.example.com',
|
|
);
|
|
|
|
final notifier = container.read(communityListProvider.notifier);
|
|
await notifier.addCommunity(ws1);
|
|
await notifier.addCommunity(ws2);
|
|
await notifier.switchCommunity(ws2.id);
|
|
|
|
final activeId = await communityStorage.loadActiveId();
|
|
expect(activeId, ws2.id);
|
|
});
|
|
});
|
|
|
|
group('activeCommunityProvider', () {
|
|
test('returns null when no communities', () async {
|
|
container = createContainer();
|
|
final active = await container.read(activeCommunityProvider.future);
|
|
expect(active, isNull);
|
|
});
|
|
|
|
test('returns community matching active ID', () async {
|
|
container = createContainer();
|
|
await container.read(communityListProvider.future);
|
|
|
|
final ws = Community.create(
|
|
name: 'Test',
|
|
relayUrl: 'https://test.example.com',
|
|
);
|
|
final notifier = container.read(communityListProvider.notifier);
|
|
await notifier.addCommunity(ws);
|
|
await notifier.switchCommunity(ws.id);
|
|
|
|
final active = await container.read(activeCommunityProvider.future);
|
|
expect(active, isNotNull);
|
|
expect(active!.id, ws.id);
|
|
expect(active.name, 'Test');
|
|
});
|
|
|
|
test('falls back to first community if active ID is invalid', () async {
|
|
container = createContainer();
|
|
await container.read(communityListProvider.future);
|
|
|
|
final ws = Community.create(
|
|
name: 'Fallback',
|
|
relayUrl: 'https://test.example.com',
|
|
);
|
|
final notifier = container.read(communityListProvider.notifier);
|
|
await notifier.addCommunity(ws);
|
|
|
|
// Set an invalid active ID.
|
|
await communityStorage.saveActiveId('nonexistent-id');
|
|
|
|
// Re-read — should fall back.
|
|
container.invalidate(activeCommunityProvider);
|
|
final active = await container.read(activeCommunityProvider.future);
|
|
expect(active, isNotNull);
|
|
expect(active!.id, ws.id);
|
|
});
|
|
});
|
|
}
|