main
dart 139 lines 4.93 KB
Raw
1 import 'package:dio/dio.dart';
2 import 'package:flutter_riverpod/flutter_riverpod.dart';
3 import 'package:flutter_test/flutter_test.dart';
4 import 'package:localmind/core/models/enums.dart';
5 import 'package:localmind/core/providers/storage_providers.dart';
6 import 'package:localmind/features/chat/providers/chat_providers.dart';
7 import 'package:localmind/features/on_device/data/models/on_device_model.dart';
8 import 'package:localmind/features/on_device/data/on_device_chat_service.dart';
9 import 'package:localmind/features/on_device/data/on_device_gemma_service.dart';
10 import 'package:localmind/features/on_device/data/on_device_llama_service.dart';
11 import 'package:localmind/features/on_device/data/on_device_mlx_service.dart';
12 import 'package:localmind/features/on_device/providers/on_device_providers.dart';
13 import 'package:localmind/features/servers/data/models/server.dart';
14 import 'package:localmind/features/servers/providers/server_providers.dart';
15 import 'package:shared_preferences/shared_preferences.dart';
16
17 void main() {
18 group('createChatServiceForServer', () {
19 test('routes imported GGUF models to llama.cpp service', () {
20 final service = createChatServiceForServer(
21 server: _onDeviceServer(),
22 dio: Dio(),
23 onDeviceGemmaService: OnDeviceGemmaService(),
24 onDeviceLlamaService: OnDeviceLlamaService(),
25 loadedOnDeviceRuntime: OnDeviceModelRuntime.llamaCpp,
26 );
27
28 expect(service, isA<OnDeviceLlamaChatService>());
29 });
30
31 test('routes curated on-device models to Gemma service', () {
32 final service = createChatServiceForServer(
33 server: _onDeviceServer(),
34 dio: Dio(),
35 onDeviceGemmaService: OnDeviceGemmaService(),
36 onDeviceLlamaService: OnDeviceLlamaService(),
37 loadedOnDeviceRuntime: OnDeviceModelRuntime.gemma,
38 );
39
40 expect(service, isA<OnDeviceChatService>());
41 });
42
43 test('routes an MLX runtime to the MLX chat adapter', () {
44 final service = createChatServiceForServer(
45 server: _onDeviceServer(),
46 dio: Dio(),
47 onDeviceGemmaService: OnDeviceGemmaService(),
48 onDeviceLlamaService: OnDeviceLlamaService(),
49 onDeviceMlxService: OnDeviceMlxService(Dio()),
50 loadedOnDeviceRuntime: OnDeviceModelRuntime.mlx,
51 );
52
53 expect(service, isA<OnDeviceMlxChatService>());
54 });
55 });
56
57 test('exposes MLX downloads on Apple platforms (iOS, iPadOS, macOS)', () {
58 expect(
59 availableCuratedMlxModels(isIOS: true),
60 OnDeviceModel.curatedMlxModels,
61 );
62 expect(
63 availableCuratedMlxModels(isMacOS: true),
64 OnDeviceModel.curatedMlxModels,
65 );
66 expect(
67 availableCuratedMlxModels(
68 isApplePlatform: false,
69 isIOS: false,
70 isMacOS: false,
71 ),
72 isEmpty,
73 );
74 });
75
76 group('chatServiceProvider', () {
77 test(
78 'only recreates when loadedRuntime changes, not on every status change',
79 () async {
80 SharedPreferences.setMockInitialValues({});
81 final prefs = await SharedPreferences.getInstance();
82 final container = ProviderContainer(
83 overrides: [
84 sharedPreferencesProvider.overrideWithValue(prefs),
85 activeServerProvider.overrideWith(_StubActiveServerNotifier.new),
86 onDeviceGemmaServiceProvider.overrideWithValue(
87 _FakeOnDeviceGemmaService(),
88 ),
89 ],
90 );
91 addTearDown(container.dispose);
92
93 final initialService = container.read(chatServiceProvider);
94 expect(initialService, isA<OnDeviceChatService>());
95
96 // Toggle status without changing runtime — the service instance must be
97 // preserved so we don't drop an in-flight chat.
98 container.read(onDeviceEngineProvider.notifier).state =
99 const OnDeviceEngineState(status: OnDeviceEngineStatus.loading);
100 final stillSameService = container.read(chatServiceProvider);
101 expect(identical(initialService, stillSameService), isTrue);
102
103 // Switching the runtime rebuilds the service so it can pick up the
104 // llama.cpp adapter.
105 container
106 .read(onDeviceEngineProvider.notifier)
107 .state = const OnDeviceEngineState(
108 status: OnDeviceEngineStatus.loaded,
109 loadedRuntime: OnDeviceModelRuntime.llamaCpp,
110 );
111 final llamaService = container.read(chatServiceProvider);
112 expect(llamaService, isA<OnDeviceLlamaChatService>());
113 },
114 );
115 });
116 }
117
118 class _StubActiveServerNotifier extends ActiveServerNotifier {
119 @override
120 Server? build() => _onDeviceServer();
121 }
122
123 class _FakeOnDeviceGemmaService extends OnDeviceGemmaService {
124 @override
125 Future<List<String>> getInstalledModelIds() async => const [];
126 }
127
128 Server _onDeviceServer() {
129 return Server(
130 id: 'on-device',
131 name: 'On-Device',
132 type: ServerType.onDevice,
133 host: '',
134 port: 0,
135 createdAt: DateTime.utc(2026, 6, 21),
136 lastConnectedAt: DateTime.utc(2026, 6, 21),
137 status: ConnectionStatus.connected,
138 );
139 }