feat: import Chinese-localized Buzz source snapshot
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled

Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
2026-08-13 18:34:25 +08:00
parent 61c3fa1df9
commit 9dfa06ffee
3785 changed files with 1085458 additions and 2 deletions
@@ -0,0 +1,546 @@
import 'dart:async';
import 'dart:convert';
import 'package:buzz/shared/relay/relay.dart';
import 'package:buzz/shared/theme/theme.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
const local = CommunityThemePreference(
theme: 'buzz',
accent: '#3b82f6',
followSystem: true,
);
test('confirmed absence seeds exact NIP-78 coordinate', () async {
final session = _FakeSession();
final relay = _FakeSignedRelay();
final manager = _manager(session, relay);
final result = await manager.initialize();
expect(result.status, CommunityThemeRemoteStatus.absent);
manager.publish(local);
await manager.flush();
expect(relay.submissions, hasLength(1));
expect(relay.submissions.single.kind, 30078);
expect(
relay.submissions.single.tags,
containsAll(<List<String>>[
['d', 'community-theme'],
['t', 'community-theme'],
]),
);
expect(jsonDecode(relay.submissions.single.content), local.toJson());
});
test(
'live replacement closes the history-to-subscription absence gap',
() async {
const replacement = CommunityThemePreference(
theme: 'dracula',
accent: '#ef4444',
followSystem: false,
);
final applied = <CommunityThemePreference>[];
late final _FakeSession session;
session = _FakeSession(
onFetchHistory: () {
session.emit(
_event(
id: 'replacement',
createdAt: 100,
content: jsonEncode(replacement.toJson()),
),
);
},
);
final manager = _manager(
session,
_FakeSignedRelay(),
onRemote: (remote) => applied.add(remote.preference),
);
final result = await manager.initialize();
expect(session.subscribeCalls, 1);
expect(result.status, CommunityThemeRemoteStatus.valid);
expect(result.remote?.preference, replacement);
expect(applied, [replacement]);
},
);
test('invalid and unavailable records never seed', () async {
for (final session in [
_FakeSession(history: [_event(content: '{bad json')]),
_FakeSession(error: StateError('offline')),
]) {
final relay = _FakeSignedRelay();
final result = await _manager(session, relay).initialize();
expect(
result.status,
anyOf(
CommunityThemeRemoteStatus.invalid,
CommunityThemeRemoteStatus.unavailable,
),
);
expect(relay.submissions, isEmpty);
}
});
test(
'newest valid event wins with deterministic same-second ordering',
() async {
final applied = <CommunityThemePreference>[];
final session = _FakeSession(
history: [
_event(
id: 'z',
createdAt: 50,
content: jsonEncode(
const CommunityThemePreference(
theme: 'dracula',
accent: '#ef4444',
followSystem: false,
).toJson(),
),
),
_event(id: 'a', createdAt: 50, content: jsonEncode(local.toJson())),
],
);
final manager = _manager(
session,
_FakeSignedRelay(),
onRemote: (r) => applied.add(r.preference),
);
await manager.initialize();
expect(applied.single.theme, 'buzz');
session.emit(
_event(
id: 'z',
createdAt: 50,
content: jsonEncode(
const CommunityThemePreference(
theme: 'dracula',
accent: '#ef4444',
followSystem: false,
).toJson(),
),
),
);
expect(applied, hasLength(1));
},
);
test('remote hydration never cancels a newer pending local write', () async {
final relay = _FakeSignedRelay();
final session = _FakeSession();
final manager = _manager(session, relay);
await manager.initialize();
manager.cancelPending();
manager.publish(
const CommunityThemePreference(
theme: 'dracula',
accent: '#ef4444',
followSystem: false,
),
);
session.emit(
_event(id: 'remote', createdAt: 100, content: jsonEncode(local.toJson())),
);
await manager.flush();
expect(relay.submissions, hasLength(1));
expect(jsonDecode(relay.submissions.single.content)['theme'], 'dracula');
manager.publish(local);
manager.dispose();
await manager.flush();
expect(relay.submissions, hasLength(1));
});
test(
'relay CLOSED resubscribes then catches up latest replacement event',
() async {
final applied = <CommunityThemePreference>[];
final session = _FakeSession();
final manager = _manager(
session,
_FakeSignedRelay(),
onRemote: (remote) => applied.add(remote.preference),
);
await manager.initialize();
manager.cancelPending();
expect(session.subscribeCalls, 1);
const replacement = CommunityThemePreference(
theme: 'dracula',
accent: '#ef4444',
followSystem: false,
);
session.history = [
_event(
id: 'replacement',
createdAt: 100,
content: jsonEncode(replacement.toJson()),
),
];
session.closeLiveSubscription('rate-limited: quota exceeded');
await _waitUntil(() => session.subscribeCalls == 2 && applied.isNotEmpty);
expect(applied.single, replacement);
expect(session.activeListeners, 1);
manager.dispose();
},
);
test('relay CLOSED after dispose never resubscribes', () async {
final session = _FakeSession();
final manager = _manager(session, _FakeSignedRelay());
await manager.initialize();
final close = session.latestClosedCallback;
manager.dispose();
close?.call('late close');
await Future<void>.delayed(const Duration(milliseconds: 20));
expect(session.subscribeCalls, 1);
});
test('publish failure retries and acknowledges exact preference', () async {
final relay = _FakeSignedRelay(failuresRemaining: 1);
final acknowledgements = <CommunityThemePreference>[];
final manager = _manager(
_FakeSession(),
relay,
onPublished: acknowledgements.add,
);
manager.publish(local);
await manager.flush();
expect(manager.pending, local);
await _waitUntil(() => acknowledgements.length == 1);
expect(relay.attempts, 2);
expect(manager.pending, isNull);
expect(acknowledgements, [local]);
});
test('serializes in-flight publish before latest edit', () async {
final firstSubmission = Completer<void>();
final relay = _FakeSignedRelay(firstSubmissionGate: firstSubmission.future);
final manager = _manager(_FakeSession(), relay);
const latest = CommunityThemePreference(
theme: 'dracula',
accent: '#ef4444',
followSystem: false,
);
manager.publish(local);
final firstFlush = manager.flush();
await _waitUntil(() => relay.attempts == 1);
manager.publish(latest);
await manager.flush();
expect(relay.attempts, 1);
firstSubmission.complete();
await firstFlush;
await _waitUntil(() => relay.attempts == 2);
expect(relay.submittedEvents, hasLength(2));
expect(
relay.submittedEvents[1].createdAt,
greaterThan(relay.submittedEvents[0].createdAt),
);
expect(jsonDecode(relay.submissions[1].content)['theme'], 'dracula');
expect(manager.pending, isNull);
});
test(
'republishes above remote observed while publish is in flight',
() async {
final firstSubmission = Completer<void>();
final relay = _FakeSignedRelay(
firstSubmissionGate: firstSubmission.future,
);
final session = _FakeSession();
final acknowledgements = <CommunityThemePreference>[];
final manager = _manager(
session,
relay,
onPublished: acknowledgements.add,
);
await manager.initialize();
manager.publish(local);
final firstFlush = manager.flush();
await _waitUntil(() => relay.attempts == 1);
session.emit(
_event(
id: 'remote-winner',
createdAt: 2000000000,
content: jsonEncode(
const CommunityThemePreference(
theme: 'dracula',
accent: '#ef4444',
followSystem: false,
).toJson(),
),
),
);
firstSubmission.complete();
await firstFlush;
expect(acknowledgements, isEmpty);
expect(manager.pending, local);
await _waitUntil(() => relay.attempts == 2);
expect(relay.submittedEvents[1].createdAt, 2000000001);
expect(acknowledgements, [local]);
expect(manager.pending, isNull);
},
);
test('remote coordinate advances pending local publish timestamp', () async {
final relay = _FakeSignedRelay();
final session = _FakeSession();
final manager = _manager(session, relay);
await manager.initialize();
manager.publish(local);
session.emit(
_event(
id: 'remote',
createdAt: 2000000000,
content: jsonEncode(local.toJson()),
),
);
await manager.flush();
expect(relay.submittedEvents.single.createdAt, 2000000001);
});
test(
'remote replacement invalidates A to B to A no-op suppression',
() async {
final relay = _FakeSignedRelay();
final session = _FakeSession();
final manager = _manager(session, relay);
await manager.initialize();
manager.publish(local);
await manager.flush();
const remotePreference = CommunityThemePreference(
theme: 'dracula',
accent: '#ef4444',
followSystem: false,
);
session.emit(
_event(
id: 'remote',
createdAt: relay.submittedEvents.single.createdAt + 1,
content: jsonEncode(remotePreference.toJson()),
),
);
manager.publish(local);
await manager.flush();
expect(relay.submissions, hasLength(2));
expect(manager.pending, isNull);
},
);
test(
'published coordinate rejects delayed same-second initialization result',
() async {
const stale = CommunityThemePreference(
theme: 'dracula',
accent: '#ef4444',
followSystem: false,
);
final history = Completer<List<NostrEvent>>();
final session = _FakeSession(historyFuture: history.future);
final relay = _FakeSignedRelay(eventId: 'published-a');
final applied = <CommunityThemePreference>[];
final manager = _manager(
session,
relay,
onRemote: (remote) => applied.add(remote.preference),
);
final initializing = manager.initialize();
manager.publish(local);
await manager.flush();
final createdAt = relay.submittedEvents.single.createdAt;
history.complete([
_event(
id: 'published-z',
createdAt: createdAt,
content: jsonEncode(stale.toJson()),
),
]);
await initializing;
expect(applied, isEmpty);
expect(manager.pending, isNull);
},
);
}
CommunityThemeSyncManager _manager(
_FakeSession session,
_FakeSignedRelay relay, {
void Function(RemoteCommunityTheme)? onRemote,
void Function(CommunityThemePreference)? onPublished,
}) => CommunityThemeSyncManager(
pubkey: 'pk',
relaySession: session,
signedEventRelay: relay,
crypto: const CommunityThemeCrypto(encrypt: _identity, decrypt: _identity),
debounce: const Duration(days: 1),
publishRetryBase: const Duration(milliseconds: 1),
publishRetryMax: const Duration(milliseconds: 4),
subscriptionRetryBase: const Duration(milliseconds: 1),
onRemote: onRemote ?? (_) {},
onPublished: onPublished ?? (_) {},
);
String _identity(String value) => value;
NostrEvent _event({
String id = 'event',
int createdAt = 1,
required String content,
}) => NostrEvent(
id: id,
pubkey: 'pk',
createdAt: createdAt,
kind: 30078,
tags: const [
['d', 'community-theme'],
],
content: content,
sig: 'sig',
);
class _FakeSession extends RelaySessionNotifier {
_FakeSession({
List<NostrEvent> history = const [],
this.historyFuture,
this.error,
this.onFetchHistory,
}) : history = List.of(history);
List<NostrEvent> history;
final Future<List<NostrEvent>>? historyFuture;
final Object? error;
final void Function()? onFetchHistory;
int subscribeCalls = 0;
final List<void Function(NostrEvent)> _listeners = [];
final List<void Function(String)> _closedCallbacks = [];
int get activeListeners => _listeners.length;
void Function(String)? get latestClosedCallback =>
_closedCallbacks.isEmpty ? null : _closedCallbacks.last;
@override
Future<List<NostrEvent>> fetchHistory(
NostrFilter filter, {
Duration timeout = const Duration(seconds: 8),
}) async {
if (error != null) throw error!;
onFetchHistory?.call();
if (historyFuture != null) return historyFuture!;
return history;
}
@override
Future<void Function()> subscribe(
NostrFilter filter,
void Function(NostrEvent) onEvent, {
void Function(String)? onClosed,
}) async {
subscribeCalls++;
_listeners.add(onEvent);
_closedCallbacks.add(onClosed ?? (_) {});
return () {
final index = _listeners.indexOf(onEvent);
if (index < 0) return;
_listeners.removeAt(index);
_closedCallbacks.removeAt(index);
};
}
void emit(NostrEvent event) {
for (final listener in List.of(_listeners)) {
listener(event);
}
}
void closeLiveSubscription(String message) {
if (_listeners.isEmpty) return;
_listeners.removeAt(0);
_closedCallbacks.removeAt(0)(message);
}
}
Future<void> _waitUntil(
bool Function() condition, {
Duration timeout = const Duration(seconds: 2),
}) async {
final deadline = DateTime.now().add(timeout);
while (!condition()) {
if (DateTime.now().isAfter(deadline)) {
fail('condition not met within $timeout');
}
await Future<void>.delayed(const Duration(milliseconds: 1));
}
}
class _Submission {
final int kind;
final String content;
final List<List<String>> tags;
const _Submission(this.kind, this.content, this.tags);
}
class _FakeSignedRelay implements SignedEventRelay {
_FakeSignedRelay({
this.failuresRemaining = 0,
this.eventId = 'event',
this.firstSubmissionGate,
});
int failuresRemaining;
final String eventId;
final Future<void>? firstSubmissionGate;
int attempts = 0;
final submissions = <_Submission>[];
final submittedEvents = <NostrEvent>[];
@override
String? get pubkey => 'pk';
@override
Future<NostrEvent> submit({
required int kind,
required String content,
required List<List<String>> tags,
int? createdAt,
void Function(NostrEvent)? onSigned,
}) async {
attempts++;
if (attempts == 1 && firstSubmissionGate != null) {
await firstSubmissionGate;
}
if (failuresRemaining > 0) {
failuresRemaining--;
throw StateError('publish failed');
}
submissions.add(_Submission(kind, content, tags));
final event = _event(
id: eventId,
content: content,
createdAt: createdAt ?? 0,
);
onSigned?.call(event);
submittedEvents.add(event);
return event;
}
}