Skip to content

Commit c9196f4

Browse files
committed
chore: update dependencies and enhance testing configuration
- Added `httpx` as a dependency for improved HTTP handling in tests. - Configured pytest to specify test paths and set asyncio mode to auto for better test execution.
1 parent 9e743c1 commit c9196f4

14 files changed

Lines changed: 989 additions & 7 deletions

agent/pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,14 @@ memory = [
2424
dev = [
2525
"pytest>=8.0",
2626
"pytest-asyncio>=0.24",
27+
"httpx>=0.28",
2728
"ruff>=0.8",
2829
]
2930

3031
[tool.ruff]
3132
line-length = 100
3233
target-version = "py311"
34+
35+
[tool.pytest.ini_options]
36+
testpaths = ["tests"]
37+
asyncio_mode = "auto"

agent/tests/__init__.py

Whitespace-only changes.

agent/tests/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Shared test fixtures for the MisakaX Agent Sidecar."""
2+
3+
import pytest
4+
from httpx import ASGITransport, AsyncClient
5+
6+
from app.main import app
7+
8+
9+
@pytest.fixture
10+
async def client():
11+
"""Async HTTP client for testing FastAPI endpoints."""
12+
transport = ASGITransport(app=app)
13+
async with AsyncClient(transport=transport, base_url="http://test") as ac:
14+
yield ac

agent/tests/test_config.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Tests for the application configuration."""
2+
3+
from app.config import Settings
4+
5+
6+
def test_default_settings():
7+
settings = Settings()
8+
assert settings.host == "127.0.0.1"
9+
assert settings.port == 9527
10+
assert settings.log_level == "info"
11+
assert settings.db_path == ""
12+
13+
14+
def test_settings_env_prefix(monkeypatch):
15+
monkeypatch.setenv("MISAKA_PORT", "8080")
16+
monkeypatch.setenv("MISAKA_LOG_LEVEL", "debug")
17+
settings = Settings()
18+
assert settings.port == 8080
19+
assert settings.log_level == "debug"
20+
21+
22+
def test_settings_custom_db_path(monkeypatch):
23+
monkeypatch.setenv("MISAKA_DB_PATH", "/tmp/test.db")
24+
settings = Settings()
25+
assert settings.db_path == "/tmp/test.db"

agent/tests/test_health.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Tests for the health check endpoint."""
2+
3+
import pytest
4+
5+
6+
@pytest.mark.asyncio
7+
async def test_health_check_returns_ok(client):
8+
response = await client.get("/health")
9+
assert response.status_code == 200
10+
data = response.json()
11+
assert data["status"] == "ok"
12+
assert data["service"] == "misaka-agent"
13+
14+
15+
@pytest.mark.asyncio
16+
async def test_health_check_is_get_only(client):
17+
response = await client.post("/health")
18+
assert response.status_code == 405
19+
20+
21+
@pytest.mark.asyncio
22+
async def test_openapi_schema_accessible(client):
23+
response = await client.get("/openapi.json")
24+
assert response.status_code == 200
25+
schema = response.json()
26+
assert schema["info"]["title"] == "MisakaX Agent"
27+
assert schema["info"]["version"] == "0.1.0"

src-tauri/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ pub mod config;
66
pub mod crypto;
77
pub mod db;
88
pub mod services;
9+
#[cfg(not(feature = "test-private"))]
910
mod sidecar;
11+
#[cfg(feature = "test-private")]
12+
pub mod sidecar;
1013

1114
use config::AppConfig;
1215
use services::llm::StreamRegistry;

src-tauri/src/sidecar.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
use std::process::{Child, Command};
22
use std::time::Duration;
33

4+
/// 构建健康检查 URL
5+
pub fn health_check_url(port: u16) -> String {
6+
format!("http://127.0.0.1:{}/health", port)
7+
}
8+
49
/// Manages the Python Agent Sidecar process lifecycle.
510
pub struct SidecarManager {
611
child: Option<Child>,
@@ -9,8 +14,7 @@ pub struct SidecarManager {
914
impl SidecarManager {
1015
/// Spawn the Python sidecar and wait for its health check.
1116
pub fn start(agent_dir: &str, port: u16) -> Result<Self, String> {
12-
// Check if already running
13-
if Self::health_check(port) {
17+
if Self::is_healthy(port) {
1418
tracing::info!("Python Sidecar already running on port {}", port);
1519
return Ok(Self { child: None });
1620
}
@@ -36,16 +40,14 @@ impl SidecarManager {
3640
)
3741
})?;
3842

39-
// Wait for health check (max 10 seconds)
4043
for _ in 0..20 {
4144
std::thread::sleep(Duration::from_millis(500));
42-
if Self::health_check(port) {
45+
if Self::is_healthy(port) {
4346
tracing::info!("Python Sidecar ready on port {}", port);
4447
return Ok(Self { child: Some(child) });
4548
}
4649
}
4750

48-
// Timeout — kill the process and return error
4951
let _ = child.kill();
5052
let _ = child.wait();
5153
Err(format!(
@@ -54,9 +56,9 @@ impl SidecarManager {
5456
))
5557
}
5658

57-
fn health_check(port: u16) -> bool {
59+
fn is_healthy(port: u16) -> bool {
5860
reqwest::blocking::Client::new()
59-
.get(format!("http://127.0.0.1:{}/health", port))
61+
.get(health_check_url(port))
6062
.timeout(Duration::from_secs(2))
6163
.send()
6264
.map(|r| r.status().is_success())
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
use misaka_x_lib::crypto;
2+
use misaka_x_lib::db::models::CreateCustomModel;
3+
use misaka_x_lib::db::repository::{CustomModelRepo, RouterConfigRepo};
4+
use misaka_x_lib::services::llm::registry::{ModelInfo, ModelRegistry};
5+
use rusqlite::Connection;
6+
7+
fn setup_test_db() -> Connection {
8+
let conn = Connection::open_in_memory().unwrap();
9+
conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
10+
misaka_x_lib::db::migrations::run_migrations(&conn).unwrap();
11+
conn
12+
}
13+
14+
fn insert_router_config(conn: &Connection, id: &str, provider: &str) {
15+
let encrypted = crypto::encrypt("sk-test-key-fake-12345678").unwrap();
16+
RouterConfigRepo::insert(
17+
conn, id, "Test Provider", provider, &encrypted,
18+
None, None, None, true, None,
19+
)
20+
.unwrap();
21+
}
22+
23+
// ─── CustomModelRepo tests ───────────────────────────────────────────
24+
25+
#[test]
26+
fn test_insert_custom_model() {
27+
let conn = setup_test_db();
28+
insert_router_config(&conn, "rc-1", "openai");
29+
30+
let model = CreateCustomModel {
31+
model_id: "ft-gpt-4o".to_string(),
32+
display_name: "Fine-tuned GPT-4o".to_string(),
33+
supports_vision: true,
34+
supports_thinking: false,
35+
max_tokens: Some(4096),
36+
context_window: Some(128000),
37+
};
38+
39+
CustomModelRepo::insert(&conn, "cm-1", "rc-1", &model).unwrap();
40+
41+
let models = ModelRegistry::available_models(&conn, "rc-1", "openai").unwrap();
42+
let custom: Vec<_> = models.iter().filter(|m| m.is_custom).collect();
43+
assert_eq!(custom.len(), 1);
44+
assert_eq!(custom[0].model_id, "ft-gpt-4o");
45+
assert_eq!(custom[0].display_name, "Fine-tuned GPT-4o");
46+
assert!(custom[0].supports_vision);
47+
assert!(!custom[0].supports_thinking);
48+
assert_eq!(custom[0].max_tokens, Some(4096));
49+
assert_eq!(custom[0].context_window, Some(128000));
50+
}
51+
52+
#[test]
53+
fn test_insert_custom_model_minimal() {
54+
let conn = setup_test_db();
55+
insert_router_config(&conn, "rc-1", "openai");
56+
57+
let model = CreateCustomModel {
58+
model_id: "my-model".to_string(),
59+
display_name: "My Model".to_string(),
60+
supports_vision: false,
61+
supports_thinking: false,
62+
max_tokens: None,
63+
context_window: None,
64+
};
65+
66+
CustomModelRepo::insert(&conn, "cm-1", "rc-1", &model).unwrap();
67+
68+
let models = ModelRegistry::available_models(&conn, "rc-1", "openai").unwrap();
69+
let custom: Vec<_> = models.iter().filter(|m| m.is_custom).collect();
70+
assert_eq!(custom.len(), 1);
71+
assert_eq!(custom[0].max_tokens, None);
72+
assert_eq!(custom[0].context_window, None);
73+
}
74+
75+
#[test]
76+
fn test_delete_custom_model() {
77+
let conn = setup_test_db();
78+
insert_router_config(&conn, "rc-1", "openai");
79+
80+
let model = CreateCustomModel {
81+
model_id: "to-delete".to_string(),
82+
display_name: "Delete Me".to_string(),
83+
supports_vision: false,
84+
supports_thinking: false,
85+
max_tokens: None,
86+
context_window: None,
87+
};
88+
89+
CustomModelRepo::insert(&conn, "cm-del", "rc-1", &model).unwrap();
90+
CustomModelRepo::delete(&conn, "cm-del").unwrap();
91+
92+
let models = ModelRegistry::available_models(&conn, "rc-1", "openai").unwrap();
93+
let custom_count = models.iter().filter(|m| m.is_custom).count();
94+
assert_eq!(custom_count, 0);
95+
}
96+
97+
#[test]
98+
fn test_delete_custom_model_not_found() {
99+
let conn = setup_test_db();
100+
let result = CustomModelRepo::delete(&conn, "nonexistent");
101+
assert!(result.is_err());
102+
}
103+
104+
#[test]
105+
fn test_custom_models_unique_constraint() {
106+
let conn = setup_test_db();
107+
insert_router_config(&conn, "rc-1", "openai");
108+
109+
let model = CreateCustomModel {
110+
model_id: "duplicate".to_string(),
111+
display_name: "First".to_string(),
112+
supports_vision: false,
113+
supports_thinking: false,
114+
max_tokens: None,
115+
context_window: None,
116+
};
117+
118+
CustomModelRepo::insert(&conn, "cm-1", "rc-1", &model).unwrap();
119+
120+
let model2 = CreateCustomModel {
121+
model_id: "duplicate".to_string(),
122+
display_name: "Second".to_string(),
123+
supports_vision: false,
124+
supports_thinking: false,
125+
max_tokens: None,
126+
context_window: None,
127+
};
128+
129+
let result = CustomModelRepo::insert(&conn, "cm-2", "rc-1", &model2);
130+
assert!(result.is_err(), "Should fail on duplicate (router_config_id, model_id)");
131+
}
132+
133+
#[test]
134+
fn test_multiple_custom_models_per_provider() {
135+
let conn = setup_test_db();
136+
insert_router_config(&conn, "rc-1", "openai");
137+
138+
for i in 0..5 {
139+
let model = CreateCustomModel {
140+
model_id: format!("model-{}", i),
141+
display_name: format!("Model {}", i),
142+
supports_vision: false,
143+
supports_thinking: false,
144+
max_tokens: None,
145+
context_window: None,
146+
};
147+
CustomModelRepo::insert(&conn, &format!("cm-{}", i), "rc-1", &model).unwrap();
148+
}
149+
150+
let models = ModelRegistry::available_models(&conn, "rc-1", "openai").unwrap();
151+
let custom_count = models.iter().filter(|m| m.is_custom).count();
152+
assert_eq!(custom_count, 5);
153+
}
154+
155+
// ─── ModelInfo structure tests ───────────────────────────────────────
156+
157+
#[test]
158+
fn test_model_info_serialize() {
159+
let info = ModelInfo::builtin("gpt-4o", "GPT-4o", true, false);
160+
let json = serde_json::to_string(&info).unwrap();
161+
assert!(json.contains("\"model_id\":\"gpt-4o\""));
162+
assert!(json.contains("\"display_name\":\"GPT-4o\""));
163+
assert!(json.contains("\"supports_vision\":true"));
164+
assert!(json.contains("\"supports_thinking\":false"));
165+
assert!(json.contains("\"is_custom\":false"));
166+
}
167+
168+
#[test]
169+
fn test_model_info_deserialize() {
170+
let json = r#"{
171+
"model_id": "custom-1",
172+
"display_name": "Custom Model",
173+
"supports_vision": true,
174+
"supports_thinking": true,
175+
"is_custom": true,
176+
"max_tokens": 8192,
177+
"context_window": null
178+
}"#;
179+
let info: ModelInfo = serde_json::from_str(json).unwrap();
180+
assert_eq!(info.model_id, "custom-1");
181+
assert!(info.supports_vision);
182+
assert!(info.supports_thinking);
183+
assert!(info.is_custom);
184+
assert_eq!(info.max_tokens, Some(8192));
185+
assert!(info.context_window.is_none());
186+
}

0 commit comments

Comments
 (0)