Skip to content

Commit 880d001

Browse files
committed
test(coverage): more local_ai + voice gains
- local_ai/ollama_admin: diagnostics against mock Ollama (unreachable + missing models + all models present), list_models happy/error paths. - voice/dictation_listener: start_if_enabled early-returns for disabled/ empty-hotkey/unparseable-hotkey; normalize_hotkey_for_rdev coverage for Shift+Alt, lowercase, function keys, whitespace trimming.
1 parent 665cca0 commit 880d001

2 files changed

Lines changed: 192 additions & 1 deletion

File tree

‎src/openhuman/local_ai/service/ollama_admin.rs‎

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,7 @@ impl LocalAiService {
668668
async fn list_models(&self) -> Result<Vec<OllamaModelTag>, String> {
669669
let response = self
670670
.http
671-
.get(format!("{OLLAMA_BASE_URL}/api/tags"))
671+
.get(format!("{}/api/tags", ollama_base_url()))
672672
.timeout(std::time::Duration::from_secs(5))
673673
.send()
674674
.await
@@ -936,4 +936,147 @@ mod tests {
936936
std::env::remove_var("OPENHUMAN_OLLAMA_BASE_URL");
937937
}
938938
}
939+
940+
#[tokio::test]
941+
async fn diagnostics_reports_server_unreachable_when_url_unbound() {
942+
let _guard = crate::openhuman::local_ai::LOCAL_AI_TEST_MUTEX
943+
.lock()
944+
.expect("local ai mutex");
945+
946+
unsafe {
947+
std::env::set_var("OPENHUMAN_OLLAMA_BASE_URL", "http://127.0.0.1:1");
948+
}
949+
let config = Config::default();
950+
let service = LocalAiService::new(&config);
951+
let diag = service.diagnostics(&config).await.expect("diagnostics");
952+
assert_eq!(diag["ollama_running"], false);
953+
let issues = diag["issues"].as_array().cloned().unwrap_or_default();
954+
assert!(
955+
!issues.is_empty(),
956+
"unreachable server must surface an issue"
957+
);
958+
assert!(issues
959+
.iter()
960+
.any(|v| v.as_str().unwrap_or("").contains("not running")));
961+
unsafe {
962+
std::env::remove_var("OPENHUMAN_OLLAMA_BASE_URL");
963+
}
964+
}
965+
966+
#[tokio::test]
967+
async fn diagnostics_with_running_server_but_missing_models_flags_issues() {
968+
let _guard = crate::openhuman::local_ai::LOCAL_AI_TEST_MUTEX
969+
.lock()
970+
.expect("local ai mutex");
971+
972+
let app = Router::new().route("/api/tags", get(|| async { Json(json!({ "models": [] })) }));
973+
let base = spawn_mock(app).await;
974+
unsafe {
975+
std::env::set_var("OPENHUMAN_OLLAMA_BASE_URL", &base);
976+
}
977+
978+
let config = Config::default();
979+
let service = LocalAiService::new(&config);
980+
let diag = service.diagnostics(&config).await.expect("diagnostics");
981+
assert_eq!(diag["ollama_running"], true);
982+
// No models are installed → expected chat model issue surfaces.
983+
let issues = diag["issues"].as_array().cloned().unwrap_or_default();
984+
assert!(!issues.is_empty());
985+
unsafe {
986+
std::env::remove_var("OPENHUMAN_OLLAMA_BASE_URL");
987+
}
988+
}
989+
990+
#[tokio::test]
991+
async fn diagnostics_ok_when_expected_models_are_present() {
992+
let _guard = crate::openhuman::local_ai::LOCAL_AI_TEST_MUTEX
993+
.lock()
994+
.expect("local ai mutex");
995+
996+
let config = Config::default();
997+
let chat = crate::openhuman::local_ai::model_ids::effective_chat_model_id(&config);
998+
let chat_tag = format!("{}:latest", chat);
999+
let app = Router::new().route(
1000+
"/api/tags",
1001+
get(move || {
1002+
let chat_tag = chat_tag.clone();
1003+
async move {
1004+
Json(json!({
1005+
"models": [
1006+
{ "name": chat_tag, "modified_at": "", "size": 1u64, "digest": "d" }
1007+
]
1008+
}))
1009+
}
1010+
}),
1011+
);
1012+
let base = spawn_mock(app).await;
1013+
unsafe {
1014+
std::env::set_var("OPENHUMAN_OLLAMA_BASE_URL", &base);
1015+
}
1016+
1017+
let service = LocalAiService::new(&config);
1018+
let diag = service.diagnostics(&config).await.expect("diagnostics");
1019+
assert_eq!(diag["ollama_running"], true);
1020+
assert_eq!(diag["expected"]["chat_found"], true);
1021+
unsafe {
1022+
std::env::remove_var("OPENHUMAN_OLLAMA_BASE_URL");
1023+
}
1024+
}
1025+
1026+
#[tokio::test]
1027+
async fn list_models_returns_parsed_payload() {
1028+
let _guard = crate::openhuman::local_ai::LOCAL_AI_TEST_MUTEX
1029+
.lock()
1030+
.expect("local ai mutex");
1031+
1032+
let app = Router::new().route(
1033+
"/api/tags",
1034+
get(|| async {
1035+
Json(json!({
1036+
"models": [
1037+
{ "name": "a:latest", "modified_at": "t", "size": 1u64, "digest": "d1" },
1038+
{ "name": "b:v2", "modified_at": "t", "size": 2u64, "digest": "d2" }
1039+
]
1040+
}))
1041+
}),
1042+
);
1043+
let base = spawn_mock(app).await;
1044+
unsafe {
1045+
std::env::set_var("OPENHUMAN_OLLAMA_BASE_URL", &base);
1046+
}
1047+
1048+
let config = Config::default();
1049+
let service = LocalAiService::new(&config);
1050+
let models = service.list_models().await.expect("list_models");
1051+
assert_eq!(models.len(), 2);
1052+
assert_eq!(models[0].name, "a:latest");
1053+
assert_eq!(models[1].name, "b:v2");
1054+
unsafe {
1055+
std::env::remove_var("OPENHUMAN_OLLAMA_BASE_URL");
1056+
}
1057+
}
1058+
1059+
#[tokio::test]
1060+
async fn list_models_errors_on_non_success() {
1061+
let _guard = crate::openhuman::local_ai::LOCAL_AI_TEST_MUTEX
1062+
.lock()
1063+
.expect("local ai mutex");
1064+
1065+
let app = Router::new().route(
1066+
"/api/tags",
1067+
get(|| async { (axum::http::StatusCode::SERVICE_UNAVAILABLE, "down") }),
1068+
);
1069+
let base = spawn_mock(app).await;
1070+
unsafe {
1071+
std::env::set_var("OPENHUMAN_OLLAMA_BASE_URL", &base);
1072+
}
1073+
1074+
let config = Config::default();
1075+
let service = LocalAiService::new(&config);
1076+
let err = service.list_models().await.unwrap_err();
1077+
assert!(err.contains("503") || err.contains("tags failed"));
1078+
unsafe {
1079+
std::env::remove_var("OPENHUMAN_OLLAMA_BASE_URL");
1080+
}
1081+
}
9391082
}

‎src/openhuman/voice/dictation_listener.rs‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,4 +257,52 @@ mod tests {
257257
assert_eq!(json["hotkey"], "fn");
258258
assert_eq!(json["activation_mode"], "push");
259259
}
260+
261+
#[tokio::test]
262+
async fn start_if_enabled_returns_early_when_config_disabled() {
263+
// Fast path — `enabled=false` → the fn returns without spawning.
264+
let mut config = Config::default();
265+
config.dictation.enabled = false;
266+
start_if_enabled(&config).await;
267+
// No panic = pass. The absence of a spawned hotkey task is what
268+
// we're verifying; hard to assert directly without internals.
269+
}
270+
271+
#[tokio::test]
272+
async fn start_if_enabled_returns_early_when_hotkey_empty() {
273+
let mut config = Config::default();
274+
config.dictation.enabled = true;
275+
config.dictation.hotkey = String::new();
276+
start_if_enabled(&config).await;
277+
}
278+
279+
#[tokio::test]
280+
async fn start_if_enabled_returns_early_when_hotkey_unparseable() {
281+
let mut config = Config::default();
282+
config.dictation.enabled = true;
283+
config.dictation.hotkey = "not a real hotkey".into();
284+
start_if_enabled(&config).await;
285+
}
286+
287+
#[test]
288+
fn normalize_maps_shift_and_alt_verbatim() {
289+
let result = normalize_hotkey_for_rdev("Shift+Alt+D");
290+
assert_eq!(result, "shift+alt+d");
291+
}
292+
293+
#[test]
294+
fn normalize_handles_lowercase_input() {
295+
assert_eq!(normalize_hotkey_for_rdev("cmd+d"), "cmd+d");
296+
}
297+
298+
#[test]
299+
fn normalize_preserves_function_keys() {
300+
assert_eq!(normalize_hotkey_for_rdev("F12"), "f12");
301+
}
302+
303+
#[test]
304+
fn normalize_trims_whitespace_between_segments() {
305+
let result = normalize_hotkey_for_rdev(" cmd + shift + d ");
306+
assert_eq!(result, "cmd+shift+d");
307+
}
260308
}

0 commit comments

Comments
 (0)