kofdai commited on
Commit
9febfad
·
verified ·
1 Parent(s): bfa56c5

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. __init__.py +1 -1
  2. conftest.py +31 -0
  3. test_null_ai.py +345 -0
__init__.py CHANGED
@@ -1 +1 @@
1
- # Backend package
 
1
+ # NullAI Test Suite
conftest.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pytest configuration for NullAI tests
3
+ """
4
+ import pytest
5
+ import os
6
+ import sys
7
+ import tempfile
8
+
9
+ # プロジェクトルートをパスに追加
10
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
11
+
12
+
13
+ @pytest.fixture(scope="session")
14
+ def project_root():
15
+ """プロジェクトルートパスを返す"""
16
+ return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17
+
18
+
19
+ @pytest.fixture
20
+ def temp_config_dir(tmp_path):
21
+ """一時的な設定ディレクトリを作成"""
22
+ return str(tmp_path)
23
+
24
+
25
+ @pytest.fixture(scope="session")
26
+ def suppress_deprecation_warnings():
27
+ """非推奨警告を抑制(テスト時のノイズ削減)"""
28
+ import warnings
29
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
30
+ yield
31
+ warnings.resetwarnings()
test_null_ai.py ADDED
@@ -0,0 +1,345 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NullAI テストスイート
3
+
4
+ null_ai モジュールのユニットテストと統合テスト
5
+ """
6
+ import pytest
7
+ import asyncio
8
+ import os
9
+ import sys
10
+ import json
11
+ from unittest.mock import Mock, patch, AsyncMock
12
+ from datetime import datetime
13
+
14
+ # プロジェクトルートをパスに追加
15
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
16
+
17
+ from null_ai.config import (
18
+ ModelProvider,
19
+ ModelConfig,
20
+ DomainConfig,
21
+ NullAIConfig,
22
+ ConfigManager,
23
+ DEFAULT_MODELS,
24
+ DEFAULT_DOMAINS
25
+ )
26
+
27
+
28
+ class TestModelProvider:
29
+ """ModelProvider Enum テスト"""
30
+
31
+ def test_huggingface_provider_exists(self):
32
+ """HuggingFaceプロバイダーが存在する"""
33
+ assert ModelProvider.HUGGINGFACE.value == "huggingface"
34
+
35
+ def test_huggingface_api_provider_exists(self):
36
+ """HuggingFace APIプロバイダーが存在する"""
37
+ assert ModelProvider.HUGGINGFACE_API.value == "huggingface_api"
38
+
39
+ def test_local_provider_exists(self):
40
+ """Localプロバイダーが存在する"""
41
+ assert ModelProvider.LOCAL.value == "local"
42
+
43
+ def test_gguf_provider_exists(self):
44
+ """GGUFプロバイダーが存在する"""
45
+ assert ModelProvider.GGUF.value == "gguf"
46
+
47
+ def test_no_external_api_providers(self):
48
+ """外部APIプロバイダー(OpenAI/Anthropic/Ollama)が存在しない"""
49
+ provider_values = [p.value for p in ModelProvider]
50
+ assert "openai" not in provider_values
51
+ assert "anthropic" not in provider_values
52
+ # Ollamaは下位互換性のために残る場合があるが、推奨されない
53
+ # assert "ollama" not in provider_values
54
+
55
+
56
+ class TestModelConfig:
57
+ """ModelConfig テスト"""
58
+
59
+ def test_create_huggingface_model(self):
60
+ """HuggingFaceモデル設定を作成できる"""
61
+ config = ModelConfig(
62
+ model_id="test-model",
63
+ display_name="Test Model",
64
+ provider=ModelProvider.HUGGINGFACE,
65
+ model_name="test-org/test-model"
66
+ )
67
+ assert config.model_id == "test-model"
68
+ assert config.provider == ModelProvider.HUGGINGFACE
69
+
70
+ def test_model_to_dict(self):
71
+ """ModelConfigを辞書に変換できる"""
72
+ config = ModelConfig(
73
+ model_id="test-model",
74
+ display_name="Test Model",
75
+ provider=ModelProvider.HUGGINGFACE,
76
+ model_name="test-org/test-model"
77
+ )
78
+ d = config.to_dict()
79
+ assert d["model_id"] == "test-model"
80
+ assert d["provider"] == "huggingface"
81
+
82
+ def test_model_from_dict(self):
83
+ """辞書からModelConfigを作成できる"""
84
+ data = {
85
+ "model_id": "test-model",
86
+ "display_name": "Test Model",
87
+ "provider": "huggingface",
88
+ "model_name": "test-org/test-model"
89
+ }
90
+ config = ModelConfig.from_dict(data)
91
+ assert config.model_id == "test-model"
92
+ assert config.provider == ModelProvider.HUGGINGFACE
93
+
94
+
95
+ class TestDefaultModels:
96
+ """DEFAULT_MODELS テスト"""
97
+
98
+ def test_default_models_exist(self):
99
+ """デフォルトモデルが存在する"""
100
+ assert len(DEFAULT_MODELS) > 0
101
+
102
+ def test_default_models_use_huggingface(self):
103
+ """デフォルトモデルはHuggingFaceプロバイダーを使用"""
104
+ for model in DEFAULT_MODELS:
105
+ assert model.provider in [
106
+ ModelProvider.HUGGINGFACE,
107
+ ModelProvider.HUGGINGFACE_API,
108
+ ModelProvider.LOCAL,
109
+ ModelProvider.GGUF
110
+ ], f"Model {model.model_id} uses unsupported provider {model.provider}"
111
+
112
+ def test_has_default_model(self):
113
+ """デフォルトモデルが設定されている"""
114
+ default_models = [m for m in DEFAULT_MODELS if m.is_default]
115
+ assert len(default_models) >= 1
116
+
117
+ def test_deepseek_models_exist(self):
118
+ """DeepSeekモデルが存在する"""
119
+ deepseek_models = [m for m in DEFAULT_MODELS if "deepseek" in m.model_id.lower()]
120
+ assert len(deepseek_models) > 0
121
+
122
+
123
+ class TestDefaultDomains:
124
+ """DEFAULT_DOMAINS テスト"""
125
+
126
+ def test_default_domains_exist(self):
127
+ """デフォルトドメインが存在する"""
128
+ assert len(DEFAULT_DOMAINS) > 0
129
+
130
+ def test_medical_domain_exists(self):
131
+ """医療ドメインが存在する"""
132
+ medical = [d for d in DEFAULT_DOMAINS if d.domain_id == "medical"]
133
+ assert len(medical) == 1
134
+
135
+ def test_legal_domain_exists(self):
136
+ """法律ドメインが存在する"""
137
+ legal = [d for d in DEFAULT_DOMAINS if d.domain_id == "legal"]
138
+ assert len(legal) == 1
139
+
140
+ def test_general_domain_exists(self):
141
+ """一般ドメインが存在する"""
142
+ general = [d for d in DEFAULT_DOMAINS if d.domain_id == "general"]
143
+ assert len(general) == 1
144
+
145
+
146
+ class TestNullAIConfig:
147
+ """NullAIConfig テスト"""
148
+
149
+ def test_default_config(self):
150
+ """デフォルト設定を作成できる"""
151
+ config = NullAIConfig()
152
+ assert config.system_name == "NullAI"
153
+ assert config.version == "1.0.0"
154
+
155
+ def test_config_to_dict(self):
156
+ """設定を辞書に変換できる"""
157
+ config = NullAIConfig()
158
+ d = config.to_dict()
159
+ assert "system_name" in d
160
+ assert "version" in d
161
+ assert "default_model_id" in d
162
+
163
+
164
+ class TestConfigManager:
165
+ """ConfigManager テスト"""
166
+
167
+ def test_create_config_manager(self, tmp_path):
168
+ """ConfigManagerを作成できる"""
169
+ manager = ConfigManager(config_dir=str(tmp_path))
170
+ assert manager is not None
171
+
172
+ def test_list_models(self, tmp_path):
173
+ """モデル一覧を取得できる"""
174
+ manager = ConfigManager(config_dir=str(tmp_path))
175
+ models = manager.list_models()
176
+ assert len(models) > 0
177
+
178
+ def test_list_domains(self, tmp_path):
179
+ """ドメイン一覧を取得できる"""
180
+ manager = ConfigManager(config_dir=str(tmp_path))
181
+ domains = manager.list_domains()
182
+ assert len(domains) > 0
183
+
184
+ def test_get_default_model(self, tmp_path):
185
+ """デフォルトモデルを取得できる"""
186
+ manager = ConfigManager(config_dir=str(tmp_path))
187
+ default = manager.get_default_model()
188
+ assert default is not None
189
+ assert default.is_default
190
+
191
+ def test_get_model_by_id(self, tmp_path):
192
+ """IDでモデルを取得できる"""
193
+ manager = ConfigManager(config_dir=str(tmp_path))
194
+ # デフォルトモデルIDで取得
195
+ model = manager.get_model("deepseek-r1-32b")
196
+ assert model is not None
197
+ assert model.model_id == "deepseek-r1-32b"
198
+
199
+ def test_get_domain_by_id(self, tmp_path):
200
+ """IDでドメインを取得できる"""
201
+ manager = ConfigManager(config_dir=str(tmp_path))
202
+ domain = manager.get_domain("medical")
203
+ assert domain is not None
204
+ assert domain.domain_id == "medical"
205
+
206
+
207
+ class TestModelRouter:
208
+ """ModelRouter テスト(モックを使用)"""
209
+
210
+ @pytest.fixture
211
+ def mock_config_manager(self, tmp_path):
212
+ """モックConfigManager"""
213
+ return ConfigManager(config_dir=str(tmp_path))
214
+
215
+ def test_create_model_router(self, mock_config_manager):
216
+ """ModelRouterを作成できる"""
217
+ from null_ai.model_router import ModelRouter
218
+ router = ModelRouter(mock_config_manager)
219
+ assert router is not None
220
+
221
+ def test_get_model_for_domain(self, mock_config_manager):
222
+ """ドメインに対応するモデルを取得できる"""
223
+ from null_ai.model_router import ModelRouter
224
+ router = ModelRouter(mock_config_manager)
225
+ model = router.get_model_for_domain("medical")
226
+ assert model is not None
227
+
228
+ def test_get_provider_info(self, mock_config_manager):
229
+ """プロバイダー情報を取得できる"""
230
+ from null_ai.model_router import ModelRouter
231
+ router = ModelRouter(mock_config_manager)
232
+ info = router.get_provider_info()
233
+
234
+ assert "supported_providers" in info
235
+ assert "unsupported_providers" in info
236
+
237
+ # サポートされているプロバイダーを確認
238
+ supported_ids = [p["id"] for p in info["supported_providers"]]
239
+ assert "huggingface" in supported_ids
240
+ assert "huggingface_api" in supported_ids
241
+ assert "gguf" in supported_ids
242
+
243
+ # サポートされていないプロバイダーを確認
244
+ unsupported_ids = [p["id"] for p in info["unsupported_providers"]]
245
+ assert "openai" in unsupported_ids
246
+ assert "anthropic" in unsupported_ids
247
+
248
+ @pytest.mark.asyncio
249
+ async def test_list_available_models(self, mock_config_manager):
250
+ """利用可能なモデル一覧を取得できる"""
251
+ from null_ai.model_router import ModelRouter
252
+ router = ModelRouter(mock_config_manager)
253
+ models = await router.list_available_models()
254
+ assert len(models) > 0
255
+
256
+
257
+ class TestWebSearchEnrichment:
258
+ """WebSearchEnrichment テスト"""
259
+
260
+ def test_create_enrichment_engine(self):
261
+ """WebSearchEnrichmentを作成できる"""
262
+ from null_ai.web_search_enrichment import WebSearchEnrichment
263
+ enricher = WebSearchEnrichment()
264
+ assert enricher is not None
265
+
266
+ def test_generate_search_queries(self):
267
+ """検索クエリを生成できる"""
268
+ from null_ai.web_search_enrichment import WebSearchEnrichment
269
+ enricher = WebSearchEnrichment()
270
+ queries = enricher.generate_search_queries("medical", count=5)
271
+ assert len(queries) > 0
272
+ assert len(queries) <= 5
273
+
274
+ def test_duckduckgo_provider_always_available(self):
275
+ """DuckDuckGoプロバイダーが常に利用可能"""
276
+ from null_ai.web_search_enrichment import WebSearchEnrichment
277
+ enricher = WebSearchEnrichment()
278
+ provider_names = [p.__class__.__name__ for p in enricher.search_providers]
279
+ assert "DuckDuckGoSearch" in provider_names
280
+
281
+
282
+ class TestSystemAPI:
283
+ """System API テスト"""
284
+
285
+ @pytest.fixture
286
+ def client(self):
287
+ """FastAPIテストクライアント"""
288
+ from fastapi.testclient import TestClient
289
+ from backend.app.main import app
290
+ return TestClient(app)
291
+
292
+ def test_system_status_endpoint(self, client):
293
+ """/api/system/status エンドポイントが動作する"""
294
+ response = client.get("/api/system/status")
295
+ assert response.status_code == 200
296
+ data = response.json()
297
+ assert "status" in data
298
+ assert "version" in data
299
+ assert "services" in data
300
+ assert "models" in data
301
+
302
+ def test_system_health_endpoint(self, client):
303
+ """/api/system/health エンドポイントが動作する"""
304
+ response = client.get("/api/system/health")
305
+ assert response.status_code == 200
306
+ data = response.json()
307
+ assert "status" in data
308
+ assert data["status"] == "ok"
309
+
310
+ def test_providers_endpoint(self, client):
311
+ """/api/system/providers エンドポイントが動作する"""
312
+ response = client.get("/api/system/providers")
313
+ assert response.status_code == 200
314
+ data = response.json()
315
+ assert "supported" in data
316
+ assert "unsupported" in data
317
+
318
+
319
+ # 統合テスト(オプション - 実際のモデルが必要)
320
+ class TestIntegration:
321
+ """統合テスト(実行には実際のモデルが必要)"""
322
+
323
+ @pytest.mark.skip(reason="Requires actual model download")
324
+ @pytest.mark.asyncio
325
+ async def test_inference_with_huggingface(self, tmp_path):
326
+ """HuggingFaceモデルで推論が実行できる"""
327
+ from null_ai.config import ConfigManager
328
+ from null_ai.model_router import ModelRouter
329
+
330
+ manager = ConfigManager(config_dir=str(tmp_path))
331
+ router = ModelRouter(manager)
332
+
333
+ result = await router.infer(
334
+ prompt="Hello, how are you?",
335
+ domain_id="general",
336
+ save_to_memory=False,
337
+ max_tokens=50
338
+ )
339
+
340
+ assert "response" in result
341
+ assert len(result["response"]) > 0
342
+
343
+
344
+ if __name__ == "__main__":
345
+ pytest.main([__file__, "-v"])