1 """Tests for onde-inference Python bindings.
2
3 These tests verify the UniFFI-generated Python bindings work correctly.
4 They test the public API surface without requiring model downloads or
5 GPU hardware — focusing on construction, configuration, and error handling.
6 """
7 import pytest
8
9 # All imports should come from the public package
10 from onde_inference import (
11 OndeChatEngine,
12 ChatMessage,
13 ChatRole,
14 SamplingConfig,
15 GgufModelConfig,
16 EngineInfo,
17 EngineStatus,
18 InferenceError,
19 StreamChunk,
20 # Free functions
21 default_model_config,
22 qwen25_1_5b_config,
23 qwen25_3b_config,
24 default_sampling_config,
25 deterministic_sampling_config,
26 mobile_sampling_config,
27 system_message,
28 user_message,
29 assistant_message,
30 # Version
31 __version__,
32 )
33
34
35 class TestVersion:
36 def test_version_is_string(self):
37 assert isinstance(__version__, str)
38
39 def test_version_is_semver(self):
40 parts = __version__.split(".")
41 assert len(parts) == 3
42 assert all(p.isdigit() for p in parts)
43
44
45 class TestModelConfigs:
46 def test_default_model_config_not_empty(self):
47 cfg = default_model_config()
48 assert isinstance(cfg, GgufModelConfig)
49 assert cfg.model_id != ""
50 assert len(cfg.files) > 0
51 assert cfg.display_name != ""
52
53 def test_qwen25_1_5b_config(self):
54 cfg = qwen25_1_5b_config()
55 assert "1.5B" in cfg.model_id
56 assert len(cfg.files) == 1
57 assert cfg.files[0].endswith(".gguf")
58
59 def test_qwen25_3b_config(self):
60 cfg = qwen25_3b_config()
61 assert "3B" in cfg.model_id
62 assert len(cfg.files) == 1
63 assert cfg.files[0].endswith(".gguf")
64
65
66 class TestSamplingConfigs:
67 def test_default_sampling(self):
68 s = default_sampling_config()
69 assert isinstance(s, SamplingConfig)
70 assert s.temperature == 0.7
71 assert s.top_p == 0.95
72 assert s.max_tokens == 512
73
74 def test_deterministic_sampling(self):
75 s = deterministic_sampling_config()
76 assert s.temperature == 0.0
77 assert s.top_p is None
78
79 def test_mobile_sampling(self):
80 s = mobile_sampling_config()
81 assert s.max_tokens == 128
82
83
84 class TestMessageHelpers:
85 def test_system_message(self):
86 m = system_message("You are helpful.")
87 assert isinstance(m, ChatMessage)
88 assert m.role == ChatRole.SYSTEM
89 assert m.content == "You are helpful."
90
91 def test_user_message(self):
92 m = user_message("Hello")
93 assert m.role == ChatRole.USER
94 assert m.content == "Hello"
95
96 def test_assistant_message(self):
97 m = assistant_message("Hi!")
98 assert m.role == ChatRole.ASSISTANT
99 assert m.content == "Hi!"
100
101
102 class TestChatRole:
103 def test_enum_variants_exist(self):
104 assert ChatRole.SYSTEM is not None
105 assert ChatRole.USER is not None
106 assert ChatRole.ASSISTANT is not None
107
108
109 class TestEngineStatus:
110 def test_enum_variants_exist(self):
111 assert EngineStatus.UNLOADED is not None
112 assert EngineStatus.LOADING is not None
113 assert EngineStatus.READY is not None
114 assert EngineStatus.GENERATING is not None
115 assert EngineStatus.ERROR is not None
116
117
118 @pytest.mark.asyncio
119 class TestOndeChatEngine:
120 async def test_new_engine(self):
121 engine = OndeChatEngine()
122 assert engine is not None
123
124 async def test_engine_not_loaded_initially(self):
125 engine = OndeChatEngine()
126 assert await engine.is_loaded() is False
127
128 async def test_engine_info_initial_state(self):
129 engine = OndeChatEngine()
130 info = await engine.info()
131 assert isinstance(info, EngineInfo)
132 assert info.status == EngineStatus.UNLOADED
133 assert info.history_length == 0
134 assert info.model_name is None
135
136 async def test_send_message_without_model_raises(self):
137 engine = OndeChatEngine()
138 with pytest.raises(InferenceError):
139 await engine.send_message("hello")
140
141 async def test_history_empty_initially(self):
142 engine = OndeChatEngine()
143 history = await engine.history()
144 assert history == []
145
146 async def test_clear_history_returns_zero(self):
147 engine = OndeChatEngine()
148 removed = await engine.clear_history()
149 assert removed == 0
150
151 async def test_unload_when_none_returns_none(self):
152 engine = OndeChatEngine()
153 result = await engine.unload_model()
154 assert result is None