chore: restore product scaffold to rollback baseline

This commit is contained in:
Edison Jwa
2026-05-29 14:02:04 +09:00
parent 2896f14ec9
commit fe6e07353e
434 changed files with 27278 additions and 63230 deletions
@@ -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);
});
}