chore: restore product scaffold to rollback baseline
This commit is contained in:
@@ -21,6 +21,69 @@ void main() {
|
||||
expect(ConnectionPhase.connected.canDisconnect, isTrue);
|
||||
});
|
||||
|
||||
test('server-reachable phases show loading until snapshot is available', () {
|
||||
expect(
|
||||
ConnectionPhase.connecting.shouldShowSnapshotLoading(hasSnapshot: false),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
ConnectionPhase.synchronizing.shouldShowSnapshotLoading(
|
||||
hasSnapshot: false,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
ConnectionPhase.connected.shouldShowSnapshotLoading(hasSnapshot: false),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
ConnectionPhase.reconnecting.shouldShowSnapshotLoading(
|
||||
hasSnapshot: false,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
ConnectionPhase.synchronizing.shouldShowSnapshotLoading(
|
||||
hasSnapshot: true,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('chat only opens after a snapshot is available', () {
|
||||
expect(
|
||||
ConnectionPhase.synchronizing.canOpenChatWithSnapshot(hasSnapshot: false),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
ConnectionPhase.synchronizing.canOpenChatWithSnapshot(hasSnapshot: true),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
ConnectionPhase.connected.canOpenChatWithSnapshot(hasSnapshot: true),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
ConnectionPhase.reconnecting.canOpenChatWithSnapshot(hasSnapshot: true),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('snapshot application completes synchronizing phase', () {
|
||||
expect(
|
||||
phaseAfterSnapshotApplied(ConnectionPhase.synchronizing),
|
||||
ConnectionPhase.connected,
|
||||
);
|
||||
expect(
|
||||
phaseAfterSnapshotApplied(ConnectionPhase.connected),
|
||||
ConnectionPhase.connected,
|
||||
);
|
||||
expect(
|
||||
phaseAfterSnapshotApplied(ConnectionPhase.reconnecting),
|
||||
ConnectionPhase.reconnecting,
|
||||
);
|
||||
});
|
||||
|
||||
test('connection status text maps all phases', () {
|
||||
expect(
|
||||
connectionStatusText(phase: ConnectionPhase.idle, l10n: l10n),
|
||||
@@ -78,4 +141,22 @@ void main() {
|
||||
expect(ConnectionPhase.reconnecting.tokens(scheme).icon, Icons.restart_alt);
|
||||
expect(ConnectionPhase.disconnected.tokens(scheme).icon, Icons.cloud_off);
|
||||
});
|
||||
|
||||
test('connected event preserves connected phase when snapshot exists', () {
|
||||
expect(
|
||||
phaseAfterConnectedEvent(ConnectionPhase.connected, hasSnapshot: true),
|
||||
ConnectionPhase.connected,
|
||||
);
|
||||
});
|
||||
|
||||
test('connected event synchronizes when snapshot is missing', () {
|
||||
expect(
|
||||
phaseAfterConnectedEvent(ConnectionPhase.connected, hasSnapshot: false),
|
||||
ConnectionPhase.synchronizing,
|
||||
);
|
||||
expect(
|
||||
phaseAfterConnectedEvent(ConnectionPhase.reconnecting, hasSnapshot: true),
|
||||
ConnectionPhase.synchronizing,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:chanora_flutter/services/prefetch_debouncer.dart';
|
||||
|
||||
void main() {
|
||||
test('debounces host edits and prefetches latest trimmed host', () async {
|
||||
final calls = <String>[];
|
||||
final debouncer = PrefetchDebouncer(
|
||||
delay: const Duration(milliseconds: 20),
|
||||
onPrefetch: (host) async => calls.add(host),
|
||||
);
|
||||
|
||||
debouncer.schedule(' first.example.com ');
|
||||
debouncer.schedule(' second.example.com ');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 35));
|
||||
|
||||
expect(calls, ['second.example.com']);
|
||||
debouncer.dispose();
|
||||
});
|
||||
|
||||
test('skips empty hosts', () async {
|
||||
final calls = <String>[];
|
||||
final debouncer = PrefetchDebouncer(
|
||||
delay: const Duration(milliseconds: 10),
|
||||
onPrefetch: (host) async => calls.add(host),
|
||||
);
|
||||
|
||||
debouncer.schedule(' ');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 25));
|
||||
|
||||
expect(calls, isEmpty);
|
||||
debouncer.dispose();
|
||||
});
|
||||
|
||||
test('dispose cancels pending prefetch', () async {
|
||||
final calls = <String>[];
|
||||
final debouncer = PrefetchDebouncer(
|
||||
delay: const Duration(milliseconds: 30),
|
||||
onPrefetch: (host) async => calls.add(host),
|
||||
);
|
||||
|
||||
debouncer.schedule('example.com');
|
||||
debouncer.dispose();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 45));
|
||||
|
||||
expect(calls, isEmpty);
|
||||
});
|
||||
}
|
||||
@@ -18,7 +18,6 @@ void main() {
|
||||
rust.BridgeClient client({
|
||||
BigInt? id,
|
||||
BigInt? channelId,
|
||||
String uid = '',
|
||||
int talkPower = 0,
|
||||
bool talkPowerGranted = false,
|
||||
bool inputMuted = false,
|
||||
@@ -26,7 +25,6 @@ void main() {
|
||||
}) {
|
||||
return rust.BridgeClient(
|
||||
id: id ?? BigInt.from(7),
|
||||
uid: uid,
|
||||
channel: channelId ?? BigInt.one,
|
||||
name: 'Me',
|
||||
inputMuted: inputMuted,
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:chanora_flutter/services/startup_dependency_check.dart';
|
||||
|
||||
void main() {
|
||||
tearDown(() {
|
||||
debugResetStartupDependencyCheck();
|
||||
});
|
||||
|
||||
test('non-linux hosts skip startup dependency issues', () async {
|
||||
debugResetStartupDependencyCheck(platformIsLinux: () => false);
|
||||
|
||||
final result = await checkStartupDependencies();
|
||||
|
||||
expect(result.issues, isEmpty);
|
||||
});
|
||||
|
||||
test('missing Linux audio runtimes are reported as recommended', () async {
|
||||
debugResetStartupDependencyCheck(
|
||||
platformIsLinux: () => true,
|
||||
libraryProbe: (candidate) => false,
|
||||
fileExists: (_) async => false,
|
||||
osReleaseProvider: () async => 'ID=ubuntu\nID_LIKE=debian\n',
|
||||
resolvedExecutableProvider: () => '/opt/chanora/chanora_flutter',
|
||||
currentDirectoryProvider: () => '/tmp',
|
||||
);
|
||||
|
||||
final result = await checkStartupDependencies();
|
||||
|
||||
expect(result.hasIssues, isTrue);
|
||||
final pipewire = result.issues.firstWhere(
|
||||
(issue) => issue.id == 'linux-pipewire-runtime',
|
||||
);
|
||||
final pulse = result.issues.firstWhere(
|
||||
(issue) => issue.id == 'linux-pulseaudio-runtime',
|
||||
);
|
||||
expect(pipewire.isRequired, isFalse);
|
||||
expect(pulse.isRequired, isFalse);
|
||||
expect(
|
||||
pipewire.installHints.single.command,
|
||||
'sudo apt install libpipewire-0.3-0',
|
||||
);
|
||||
expect(pulse.installHints.single.command, 'sudo apt install libpulse0');
|
||||
});
|
||||
|
||||
test('distro detection parses quoted ID_LIKE lists', () async {
|
||||
debugResetStartupDependencyCheck(
|
||||
platformIsLinux: () => true,
|
||||
libraryProbe: (candidate) => false,
|
||||
fileExists: (_) async => true,
|
||||
osReleaseProvider: () async => 'ID=rocky\nID_LIKE="fedora rhel"\n',
|
||||
resolvedExecutableProvider: () => '/opt/chanora/chanora_flutter',
|
||||
currentDirectoryProvider: () => '/tmp',
|
||||
);
|
||||
|
||||
final result = await checkStartupDependencies();
|
||||
|
||||
expect(result.platformLabel, 'Fedora');
|
||||
final pipewire = result.issues.singleWhere(
|
||||
(issue) => issue.id == 'linux-pipewire-runtime',
|
||||
);
|
||||
expect(
|
||||
pipewire.installHints.single.command,
|
||||
'sudo dnf install pipewire-libs',
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'missing ONNX runtime is reported as recommended when Linux audio runtimes are present',
|
||||
() async {
|
||||
debugResetStartupDependencyCheck(
|
||||
platformIsLinux: () => true,
|
||||
libraryProbe: (candidate) =>
|
||||
candidate.contains('pipewire') || candidate.contains('pulse'),
|
||||
fileExists: (_) async => false,
|
||||
osReleaseProvider: () async => 'ID=fedora\n',
|
||||
resolvedExecutableProvider: () => '/opt/chanora/chanora_flutter',
|
||||
currentDirectoryProvider: () => '/tmp',
|
||||
currentAbiProvider: () => Abi.linuxX64,
|
||||
);
|
||||
|
||||
final result = await checkStartupDependencies();
|
||||
|
||||
expect(result.issues, hasLength(1));
|
||||
final ort = result.issues.single;
|
||||
expect(ort.id, 'linux-onnxruntime');
|
||||
expect(ort.isRequired, isFalse);
|
||||
expect(
|
||||
ort.installHints.any(
|
||||
(hint) =>
|
||||
hint.command ==
|
||||
'https://github.com/microsoft/onnxruntime/releases',
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
ort.installHints.any(
|
||||
(hint) => hint.command == 'onnxruntime-linux-x64-<version>.tgz',
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
ort.installHints.any((hint) => hint.command.contains('ORT_DYLIB_PATH')),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
ort.details,
|
||||
contains(
|
||||
'This machine needs the Linux x64 CPU archive (onnxruntime-linux-x64-<version>.tgz).',
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('bundle-local ONNX runtime clears the recommendation', () async {
|
||||
debugResetStartupDependencyCheck(
|
||||
platformIsLinux: () => true,
|
||||
libraryProbe: (candidate) =>
|
||||
candidate.contains('pipewire') || candidate.contains('pulse'),
|
||||
fileExists: (path) async => path == '/opt/chanora/lib/libonnxruntime.so',
|
||||
resolvedExecutableProvider: () => '/opt/chanora/chanora_flutter',
|
||||
currentDirectoryProvider: () => '/tmp',
|
||||
);
|
||||
|
||||
final result = await checkStartupDependencies();
|
||||
|
||||
expect(result.issues, isEmpty);
|
||||
});
|
||||
|
||||
test('logging missing startup dependencies appends a log line', () async {
|
||||
final tempDir = await Directory.systemTemp.createTemp(
|
||||
'chanora-startup-log',
|
||||
);
|
||||
addTearDown(() => tempDir.delete(recursive: true));
|
||||
final logFile = File('${tempDir.path}/chanora.log');
|
||||
|
||||
debugResetStartupDependencyCheck(logFilePathProvider: () => logFile.path);
|
||||
|
||||
const result = StartupDependencyCheckResult(
|
||||
platformLabel: 'Fedora',
|
||||
issues: [
|
||||
StartupDependencyIssue(
|
||||
id: 'linux-pipewire-runtime',
|
||||
title: 'PipeWire runtime is missing',
|
||||
summary: 'Primary Linux audio needs PipeWire.',
|
||||
details: ['Install PipeWire and recheck.'],
|
||||
severity: StartupDependencySeverity.recommended,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
await logStartupDependencyIssues(result);
|
||||
|
||||
final text = await logFile.readAsString();
|
||||
expect(text, contains('[startup_dependency_check]'));
|
||||
expect(text, contains('platform="Fedora"'));
|
||||
expect(
|
||||
text,
|
||||
contains(
|
||||
'issues=[linux-pipewire-runtime:recommended:PipeWire runtime is missing]',
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -16,6 +16,7 @@ void main() {
|
||||
|
||||
expect(settings.host, isEmpty);
|
||||
expect(settings.nickname, isEmpty);
|
||||
expect(settings.themeMode, UiThemeMode.system);
|
||||
});
|
||||
|
||||
test('saves and loads host and nickname independently', () async {
|
||||
@@ -40,78 +41,30 @@ void main() {
|
||||
expect(await service.hasExplainedPermissions(), isTrue);
|
||||
});
|
||||
|
||||
test('stores per-user playback preferences per server host', () async {
|
||||
await service.saveClientPlaybackPreference(
|
||||
serverHost: 'Example.COM',
|
||||
userUid: 'user-a',
|
||||
volume: 0.5,
|
||||
muted: true,
|
||||
);
|
||||
await service.saveClientPlaybackPreference(
|
||||
serverHost: 'other.example',
|
||||
userUid: 'user-a',
|
||||
volume: 2.0,
|
||||
muted: false,
|
||||
);
|
||||
test('saves and loads theme mode independently', () async {
|
||||
await service.saveSettings(host: 'example.com', nickname: 'Chanora');
|
||||
await service.saveThemeMode(UiThemeMode.dark);
|
||||
|
||||
final examplePrefs = await service.loadClientPlaybackPreferencesForServer(
|
||||
'example.com',
|
||||
);
|
||||
final otherPrefs = await service.loadClientPlaybackPreferencesForServer(
|
||||
'other.example',
|
||||
);
|
||||
var settings = await service.loadSettings();
|
||||
|
||||
expect(examplePrefs['user-a']?.volume, 0.5);
|
||||
expect(examplePrefs['user-a']?.muted, isTrue);
|
||||
expect(otherPrefs['user-a']?.volume, 2.0);
|
||||
expect(otherPrefs['user-a']?.muted, isFalse);
|
||||
expect(settings.host, 'example.com');
|
||||
expect(settings.nickname, 'Chanora');
|
||||
expect(settings.themeMode, UiThemeMode.dark);
|
||||
|
||||
await service.saveThemeMode(UiThemeMode.light);
|
||||
settings = await service.loadSettings();
|
||||
|
||||
expect(settings.host, 'example.com');
|
||||
expect(settings.nickname, 'Chanora');
|
||||
expect(settings.themeMode, UiThemeMode.light);
|
||||
});
|
||||
|
||||
test(
|
||||
'drops default playback preferences instead of persisting them',
|
||||
() async {
|
||||
await service.saveClientPlaybackPreference(
|
||||
serverHost: 'example.com',
|
||||
userUid: 'user-a',
|
||||
volume: 0.2,
|
||||
muted: true,
|
||||
);
|
||||
await service.saveClientPlaybackPreference(
|
||||
serverHost: 'example.com',
|
||||
userUid: 'user-a',
|
||||
volume: 1.0,
|
||||
muted: false,
|
||||
);
|
||||
test('falls back to system theme mode for invalid stored values', () async {
|
||||
SharedPreferences.setMockInitialValues({'ui.theme_mode': 'sepia'});
|
||||
service = const UiPreferencesService();
|
||||
|
||||
final prefs = await service.loadClientPlaybackPreferencesForServer(
|
||||
'example.com',
|
||||
);
|
||||
expect(prefs, isEmpty);
|
||||
},
|
||||
);
|
||||
final settings = await service.loadSettings();
|
||||
|
||||
test('ignores corrupt playback preference JSON', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'audio.client_playback_prefs': '{not-json',
|
||||
});
|
||||
|
||||
final prefs = await service.loadClientPlaybackPreferencesForServer(
|
||||
'example.com',
|
||||
);
|
||||
|
||||
expect(prefs, isEmpty);
|
||||
});
|
||||
|
||||
test('clamps invalid playback volumes to unity', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'audio.client_playback_prefs':
|
||||
'{"example.com|user-a":{"volume":"invalid","muted":false}}',
|
||||
});
|
||||
|
||||
final prefs = await service.loadClientPlaybackPreferencesForServer(
|
||||
'example.com',
|
||||
);
|
||||
|
||||
expect(prefs['user-a']?.volume, 1.0);
|
||||
expect(settings.themeMode, UiThemeMode.system);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user