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
}