1 // Copyright 2026 Onde Inference (Splitfire AB). All rights reserved.
2 // Use of this source code is governed by the MIT license.
3 //
4 import 'package:flutter_test/flutter_test.dart';
5 import 'package:onde_inference/onde_inference.dart';
6 import 'package:onde_inference/src/frb_generated.dart/frb_generated.dart'
7 as frb
8 show RustLibApi;
9
10 class _FakeOndeChatEngine implements OndeChatEngine {
11 @override
12 dynamic noSuchMethod(Invocation invocation) =>
13 throw UnimplementedError('Unexpected OndeChatEngine member: $invocation');
14 }
15
16 class _MockRustLibApi implements frb.RustLibApi {
17 static const _qwen2515b = GgufModelConfig(
18 modelId: 'bartowski/Qwen2.5-1.5B-Instruct-GGUF',
19 files: ['Qwen2.5-1.5B-Instruct-Q4_K_M.gguf'],
20 displayName: 'Qwen 2.5 1.5B',
21 approxMemory: '~941 MB',
22 );
23
24 static const _qwen253b = GgufModelConfig(
25 modelId: 'bartowski/Qwen2.5-3B-Instruct-GGUF',
26 files: ['Qwen2.5-3B-Instruct-Q4_K_M.gguf'],
27 displayName: 'Qwen 2.5 3B',
28 approxMemory: '~1.93 GB',
29 );
30
31 static const _qwen25Coder15b = GgufModelConfig(
32 modelId: 'bartowski/Qwen2.5-Coder-1.5B-Instruct-GGUF',
33 files: ['Qwen2.5-Coder-1.5B-Instruct-Q4_K_M.gguf'],
34 displayName: 'Qwen 2.5 Coder 1.5B',
35 approxMemory: '~941 MB',
36 );
37
38 static const _qwen25Coder3b = GgufModelConfig(
39 modelId: 'bartowski/Qwen2.5-Coder-3B-Instruct-GGUF',
40 files: ['Qwen2.5-Coder-3B-Instruct-Q4_K_M.gguf'],
41 displayName: 'Qwen 2.5 Coder 3B',
42 approxMemory: '~1.93 GB',
43 );
44
45 static final _defaultSampling = SamplingConfig(
46 temperature: 0.7,
47 topP: 0.95,
48 maxTokens: BigInt.from(512),
49 );
50
51 static final _deterministicSampling = SamplingConfig(
52 temperature: 0.0,
53 maxTokens: BigInt.from(512),
54 );
55
56 static final _mobileSampling = SamplingConfig(
57 temperature: 0.7,
58 topP: 0.95,
59 maxTokens: BigInt.from(128),
60 );
61
62 @override
63 GgufModelConfig crateApiDefaultModelConfig() => _qwen25Coder3b;
64
65 @override
66 SamplingConfig crateApiDefaultSamplingConfig() => _defaultSampling;
67
68 @override
69 SamplingConfig crateApiDeterministicSamplingConfig() =>
70 _deterministicSampling;
71
72 @override
73 SamplingConfig crateApiMobileSamplingConfig() => _mobileSampling;
74
75 @override
76 OndeChatEngine crateApiOndeChatEngineNew() => _FakeOndeChatEngine();
77
78 @override
79 GgufModelConfig crateApiQwen2515BConfig() => _qwen2515b;
80
81 @override
82 GgufModelConfig crateApiQwen253BConfig() => _qwen253b;
83
84 @override
85 GgufModelConfig crateApiQwen25Coder15BConfig() => _qwen25Coder15b;
86
87 @override
88 GgufModelConfig crateApiQwen25Coder3BConfig() => _qwen25Coder3b;
89
90 @override
91 dynamic noSuchMethod(Invocation invocation) =>
92 throw UnimplementedError('Unexpected RustLibApi member: $invocation');
93 }
94
95 void main() {
96 setUpAll(() {
97 RustLib.initMock(api: _MockRustLibApi());
98 });
99
100 tearDownAll(() {
101 RustLib.dispose();
102 });
103 group('OndeException', () {
104 test('toString includes error info', () {
105 const e = OndeException(OndeError_NoModelLoaded());
106 expect(e.toString(), contains('OndeException'));
107 });
108
109 test('equality', () {
110 const a = OndeException(OndeError_NoModelLoaded());
111 const b = OndeException(OndeError_NoModelLoaded());
112 const c = OndeException(OndeError_Cancelled());
113 expect(a, equals(b));
114 expect(a, isNot(equals(c)));
115 });
116 });
117
118 group('ChatRole', () {
119 test('name values', () {
120 expect(ChatRole.system.name, 'system');
121 expect(ChatRole.user.name, 'user');
122 expect(ChatRole.assistant.name, 'assistant');
123 });
124 });
125
126 group('ChatMessage', () {
127 test('factory constructors set correct roles', () {
128 expect(ChatMessageX.system('Hi').role, ChatRole.system);
129 expect(ChatMessageX.user('Hi').role, ChatRole.user);
130 expect(ChatMessageX.assistant('Hi').role, ChatRole.assistant);
131 });
132
133 test('equality', () {
134 final a = ChatMessageX.user('Hello');
135 final b = ChatMessageX.user('Hello');
136 final c = ChatMessageX.user('World');
137 expect(a, equals(b));
138 expect(a, isNot(equals(c)));
139 });
140 });
141
142 group('SamplingConfig', () {
143 test('defaultConfig preset', () {
144 final s = SamplingConfigX.defaultConfig();
145 expect(s.temperature, 0.7);
146 expect(s.topP, 0.95);
147 expect(s.maxTokens, BigInt.from(512));
148 });
149
150 test('deterministic preset', () {
151 final s = SamplingConfigX.deterministic();
152 expect(s.temperature, 0.0);
153 expect(s.maxTokens, BigInt.from(512));
154 expect(s.topP, isNull);
155 });
156
157 test('mobile preset', () {
158 final s = SamplingConfigX.mobile();
159 expect(s.temperature, 0.7);
160 expect(s.topP, 0.95);
161 expect(s.maxTokens, BigInt.from(128));
162 });
163
164 test('copyWith overrides single field', () {
165 final base = SamplingConfigX.defaultConfig();
166 final copy = base.copyWith(temperature: 0.3);
167 expect(copy.temperature, 0.3);
168 expect(copy.topP, base.topP);
169 expect(copy.maxTokens, base.maxTokens);
170 });
171
172 test('equality', () {
173 expect(
174 SamplingConfigX.deterministic(),
175 equals(SamplingConfigX.deterministic()),
176 );
177 expect(
178 SamplingConfigX.deterministic(),
179 isNot(equals(SamplingConfigX.mobile())),
180 );
181 });
182 });
183
184 group('GgufModelConfig', () {
185 test('factory functions return non-empty configs', () {
186 final config1_5b = OndeInference.qwen2515bConfig();
187 expect(config1_5b.modelId, contains('1.5B'));
188 expect(config1_5b.files, isNotEmpty);
189 expect(config1_5b.displayName, isNotEmpty);
190 expect(config1_5b.approxMemory, isNotEmpty);
191
192 final config3b = OndeInference.qwen253bConfig();
193 expect(config3b.modelId, contains('3B'));
194 expect(config3b.files, isNotEmpty);
195
196 final configCoder1_5b = OndeInference.qwen25Coder15bConfig();
197 expect(configCoder1_5b.modelId, contains('Coder'));
198
199 final configCoder3b = OndeInference.qwen25Coder3bConfig();
200 expect(configCoder3b.modelId, contains('Coder'));
201
202 final defaultConfig = OndeInference.defaultModelConfig();
203 expect(defaultConfig.modelId, isNotEmpty);
204 });
205
206 test('equality', () {
207 expect(
208 OndeInference.qwen2515bConfig(),
209 equals(OndeInference.qwen2515bConfig()),
210 );
211 expect(
212 OndeInference.qwen2515bConfig(),
213 isNot(equals(OndeInference.qwen253bConfig())),
214 );
215 });
216 });
217
218 group('EngineStatus', () {
219 test('isReady only true for ready', () {
220 expect(EngineStatus.ready.isReady, isTrue);
221 expect(EngineStatus.unloaded.isReady, isFalse);
222 expect(EngineStatus.loading.isReady, isFalse);
223 expect(EngineStatus.generating.isReady, isFalse);
224 expect(EngineStatus.error.isReady, isFalse);
225 });
226
227 test('name values', () {
228 expect(EngineStatus.unloaded.name, 'unloaded');
229 expect(EngineStatus.loading.name, 'loading');
230 expect(EngineStatus.ready.name, 'ready');
231 expect(EngineStatus.generating.name, 'generating');
232 expect(EngineStatus.error.name, 'error');
233 });
234 });
235
236 group('EngineInfo', () {
237 test('equality', () {
238 final a = EngineInfo(
239 status: EngineStatus.unloaded,
240 historyLength: BigInt.zero,
241 );
242 final b = EngineInfo(
243 status: EngineStatus.unloaded,
244 historyLength: BigInt.zero,
245 );
246 final c = EngineInfo(
247 status: EngineStatus.ready,
248 modelName: 'Qwen 2.5 3B',
249 approxMemory: '~1.93 GB',
250 historyLength: BigInt.from(3),
251 );
252 expect(a, equals(b));
253 expect(a, isNot(equals(c)));
254 });
255 });
256
257 group('StreamChunk', () {
258 test('equality', () {
259 const a = StreamChunk(delta: 'hello', done: false);
260 const b = StreamChunk(delta: 'hello', done: false);
261 const c = StreamChunk(delta: 'bye', done: true, finishReason: 'stop');
262 expect(a, equals(b));
263 expect(a, isNot(equals(c)));
264 });
265 });
266
267 group('InferenceResult', () {
268 test('equality', () {
269 const a = InferenceResult(
270 text: 'Hi there!',
271 durationSecs: 1.5,
272 durationDisplay: '1.5s',
273 finishReason: 'stop',
274 toolCalls: [],
275 );
276 const b = InferenceResult(
277 text: 'Hi there!',
278 durationSecs: 1.5,
279 durationDisplay: '1.5s',
280 finishReason: 'stop',
281 toolCalls: [],
282 );
283 expect(a, equals(b));
284 });
285 });
286
287 group('RustLib', () {
288 test('instance is accessible before init', () {
289 // instance is always non-null; initialization requires the native
290 // library to be compiled first.
291 expect(RustLib.instance, isNotNull);
292 });
293 });
294
295 group('OndeInference helpers', () {
296 test('config factories return valid configs', () {
297 expect(OndeInference.defaultModelConfig().modelId, isNotEmpty);
298 expect(OndeInference.qwen2515bConfig().modelId, contains('1.5B'));
299 expect(OndeInference.qwen253bConfig().modelId, contains('3B'));
300 expect(OndeInference.qwen25Coder15bConfig().modelId, contains('Coder'));
301 expect(OndeInference.qwen25Coder3bConfig().modelId, contains('Coder'));
302 });
303
304 test('sampling factories return correct presets', () {
305 expect(OndeInference.defaultSamplingConfig().temperature, 0.7);
306 expect(OndeInference.deterministicSamplingConfig().temperature, 0.0);
307 expect(OndeInference.mobileSamplingConfig().maxTokens, BigInt.from(128));
308 });
309 });
310
311 group('OndeChatEngine', () {
312 test('constructor succeeds when mock API is initialized', () {
313 expect(OndeChatEngine(), isA<OndeChatEngine>());
314 });
315 });
316 }