@ondeinference / onde / commits / 2626e39

Add Dart SDK CI job and mock RustLibApi for unit tests

- Adds a Dart/Flutter SDK validation job to GitHub Actions CI - Mocks RustLibApi in Dart tests to enable unit testing without native library - Updates .gitignore to exclude Dart build outputs - Refactors Dart tests to use the mock API and verify config factories

Seto Elkahfi committed May 14, 2026 at 16:50 UTC 2626e398422d453577212acc804978437bfdf847
3 files changed +127 -6
.github/workflows/ci.yml
+27
@@ -13,6 +13,33 @@ permissions:
13 contents: read
14
15 jobs:
16 + # ── Dart / Flutter SDK validation ────────────────────────────────────────
17 + dart-sdk:
18 + name: Dart SDK (Flutter)
19 + runs-on: ubuntu-latest
20 + steps:
21 + - uses: actions/checkout@v6
22 + - uses: subosito/flutter-action@v2
23 + with:
24 + flutter-version-file: sdk/dart/.flutter-version
25 + channel: stable
26 + cache: true
27 + - name: Install Dart dependencies
28 + working-directory: sdk/dart
29 + run: flutter pub get
30 + - name: Analyze Dart package
31 + working-directory: sdk/dart
32 + run: flutter analyze --no-fatal-infos
33 + - name: Run Dart package tests
34 + working-directory: sdk/dart
35 + run: flutter test
36 + - name: Restore generated files modified by flutter pub get
37 + working-directory: sdk/dart
38 + run: git checkout -- . 2>/dev/null || true
39 + - name: Dry-run Dart publish
40 + working-directory: sdk/dart
41 + run: flutter pub publish --dry-run
42 +
43 # ── Formatting ────────────────────────────────────────────────────────────
44 fmt:
45 name: Format
sdk/dart/.gitignore
+3
@@ -17,6 +17,9 @@ macos/*.a
17 **/*.dll
18 **/*.lib
19
20 +# Flutter / Dart build outputs
21 +build/
22 +
23 # Flutter example app build outputs
24 example/build/
25 example/.dart_tool/
sdk/dart/test/dart_test.dart
+97 -6
@@ -3,8 +3,103 @@
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());
@@ -198,10 +293,6 @@ void main() {
293 });
294
295 group('OndeInference helpers', () {
201 - test('init() delegates to RustLib.init()', () async {
202 - await expectLater(OndeInference.init(), completes);
203 - });
204 -
296 test('config factories return valid configs', () {
297 expect(OndeInference.defaultModelConfig().modelId, isNotEmpty);
298 expect(OndeInference.qwen2515bConfig().modelId, contains('1.5B'));
@@ -218,8 +309,8 @@ void main() {
309 });
310
311 group('OndeChatEngine', () {
221 - test('constructor throws when library not initialized', () {
222 - expect(() => OndeChatEngine(), throwsA(anything));
312 + test('constructor succeeds when mock API is initialized', () {
313 + expect(OndeChatEngine(), isA<OndeChatEngine>());
314 });
315 });
316 }