Files
openhuman/tests/raw_coverage/inference_provider_raw_coverage_e2e.rs
+16 2e5b5e7b23 Promote main → release (#5203)
Co-authored-by: YellowSnnowmann <167776381+YellowSnnowmann@users.noreply.github.com>
Co-authored-by: Steven Enamakel <31011319+senamakel@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cyrus Gray <144336577+graycyrus@users.noreply.github.com>
Co-authored-by: Horst1993 <horst.w@gmicloud.ai>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: James Gentes <jgentes@users.noreply.github.com>
Co-authored-by: Sam <samrusani@users.noreply.github.com>
Co-authored-by: Sami Rusani <14844597+samrusani@users.noreply.github.com>
Co-authored-by: oxoxDev <164490987+oxoxDev@users.noreply.github.com>
Co-authored-by: Muhammad Ismail <78064250+myi1@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: nb213 <binyangzhu000@gmail.com>
Co-authored-by: binyangzhu000-sudo <224954946+binyangzhu000-sudo@users.noreply.github.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
Co-authored-by: CodeGhost21 <164498022+CodeGhost21@users.noreply.github.com>
Co-authored-by: sanil-23 <sanil@tinyhumans.ai>
Co-authored-by: M3gA-Mind <elvin@mahadao.com>
Co-authored-by: oxoxDev <oxoxdev@users.noreply.github.com>
Co-authored-by: mysma-9403 <64923976+mysma-9403@users.noreply.github.com>
Co-authored-by: mwakidenis <mwakidenice@gmail.com>
Co-authored-by: NgoQuocViet2001 <123613986+NgoQuocViet2001@users.noreply.github.com>
Co-authored-by: viet.ngo <viet.ngo@sotatek.com>
Co-authored-by: Maciej Myszkiewicz <mmyszkiewicz@bwcoders.com>
2026-07-27 11:49:45 +05:30

588 lines
19 KiB
Rust

//! Round 15 raw/E2E coverage for inference provider and local runtime paths.
//!
//! These tests use only loopback HTTP mocks and temp workspaces. They do not
//! require real Ollama, LM Studio, Piper, Whisper, Python, or model binaries.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use axum::body::Body;
use axum::extract::State;
use axum::http::{header, HeaderMap, Response, StatusCode};
use axum::response::IntoResponse;
use axum::routing::{get, post};
use axum::{Json, Router};
use serde_json::{json, Value};
use tempfile::{tempdir, TempDir};
use openhuman_core::openhuman::config::schema::cloud_providers::{
AuthStyle as CloudAuthStyle, CloudProviderCreds,
};
use openhuman_core::openhuman::config::Config;
use openhuman_core::openhuman::credentials::{
AuthService, APP_SESSION_PROVIDER, DEFAULT_AUTH_PROFILE_NAME,
};
use openhuman_core::openhuman::inference::local::LocalAiService;
use openhuman_core::openhuman::inference::provider::factory::{
auth_key_for_slug, create_chat_model_from_string_with_model_id, provider_for_role,
};
use openhuman_core::openhuman::inference::provider::{list_configured_models, sanitize_api_error};
#[derive(Clone, Default)]
struct MockState {
requests: Arc<Mutex<Vec<(String, Option<String>, Value)>>>,
}
struct EnvVarGuard {
key: &'static str,
previous: Option<String>,
}
impl EnvVarGuard {
fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {
let previous = std::env::var(key).ok();
// SAFETY: this test binary is run with --test-threads=1 in validation.
unsafe { std::env::set_var(key, value) };
Self { key, previous }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
match &self.previous {
Some(value) => {
// SAFETY: this test binary is run with --test-threads=1 in validation.
unsafe { std::env::set_var(self.key, value) }
}
None => {
// SAFETY: this test binary is run with --test-threads=1 in validation.
unsafe { std::env::remove_var(self.key) }
}
}
}
}
// Serialize env mutation against every other aggregated suite via the
// single crate-wide SHARED_ENV_LOCK (these tests use an `EnvGuard` struct
// that does not itself hold a lock). Poison is recovered so a panic
// elsewhere cannot wedge the suite.
fn __shared_env_lock() -> std::sync::MutexGuard<'static, ()> {
crate::SHARED_ENV_LOCK
.get_or_init(|| std::sync::Mutex::new(()))
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[tokio::test]
async fn provider_factory_and_model_listing_cover_cloud_local_and_invalid_shapes() {
let _env_lock = __shared_env_lock();
let (base, _state) = serve_mock().await;
let tmp = tempdir().expect("tempdir");
let mut config = temp_config(&tmp);
let _workspace_env = EnvVarGuard::set(
"OPENHUMAN_WORKSPACE",
config.config_path.parent().expect("config parent"),
);
seed_session(&config);
config.cloud_providers = vec![
CloudProviderCreds {
id: "custom-id".to_string(),
slug: "custom".to_string(),
label: "Custom".to_string(),
endpoint: format!("{base}/v1"),
auth_style: CloudAuthStyle::Bearer,
legacy_type: None,
default_model: Some("demo-chat".to_string()),
},
CloudProviderCreds {
id: "openrouter-id".to_string(),
slug: "openrouter".to_string(),
label: "OpenRouter".to_string(),
endpoint: format!("{base}/openrouter"),
auth_style: CloudAuthStyle::Bearer,
legacy_type: None,
default_model: None,
},
CloudProviderCreds {
id: "missing-id".to_string(),
slug: "missing".to_string(),
label: "Missing".to_string(),
endpoint: format!("{base}/missing"),
auth_style: CloudAuthStyle::None,
legacy_type: None,
default_model: None,
},
CloudProviderCreds {
id: "html-id".to_string(),
slug: "html".to_string(),
label: "HTML".to_string(),
endpoint: format!("{base}/html"),
auth_style: CloudAuthStyle::None,
legacy_type: None,
default_model: None,
},
CloudProviderCreds {
id: "wrong-data-id".to_string(),
slug: "wrong-data".to_string(),
label: "Wrong Data".to_string(),
endpoint: format!("{base}/wrong-data"),
auth_style: CloudAuthStyle::None,
legacy_type: None,
default_model: None,
},
CloudProviderCreds {
id: "error-payload-id".to_string(),
slug: "error-payload".to_string(),
label: "Error Payload".to_string(),
endpoint: format!("{base}/error-payload"),
auth_style: CloudAuthStyle::None,
legacy_type: None,
default_model: None,
},
];
config.chat_provider = Some("custom:demo-chat@0.4".to_string());
config.reasoning_provider = None;
config.local_ai.base_url = Some(base.clone());
AuthService::from_config(&config)
.store_provider_token(
&auth_key_for_slug("custom"),
DEFAULT_AUTH_PROFILE_NAME,
"custom-key",
HashMap::new(),
true,
)
.expect("store custom key");
AuthService::from_config(&config)
.store_provider_token(
&auth_key_for_slug("openrouter"),
DEFAULT_AUTH_PROFILE_NAME,
"openrouter-key",
HashMap::new(),
true,
)
.expect("store openrouter key");
config.save().await.expect("save temp config");
assert_eq!(provider_for_role("chat", &config), "custom:demo-chat@0.4");
assert_eq!(
provider_for_role("reasoning", &config),
"custom:demo-chat@0.4"
);
let (_provider, model) =
create_chat_model_from_string_with_model_id("chat", "custom:demo-chat@0.4", &config, 0.7)
.expect("cloud model");
assert_eq!(model, "demo-chat");
let (_local_provider, local_model) =
create_chat_model_from_string_with_model_id(
"chat",
"ollama:gemma3:1b-it-qat@0.1",
&config,
0.7,
)
.expect("ollama model");
assert_eq!(local_model, "gemma3:1b-it-qat");
let empty_model = match create_chat_model_from_string_with_model_id(
"chat", "ollama:", &config, 0.7,
) {
Ok(_) => panic!("expected empty model error"),
Err(err) => err,
};
assert!(empty_model.to_string().contains("empty model"));
let listed = list_configured_models("custom")
.await
.expect("list models")
.value;
assert_eq!(listed["models"][0]["id"], "demo-chat");
assert_eq!(listed["models"][1]["context_window"], 8192);
let local_listed = list_configured_models("ollama")
.await
.expect("synthetic ollama list")
.value;
assert_eq!(local_listed["models"][0]["id"], "demo-chat");
// PR #2959 reverted the list_models 404 suppression: a 404 from /models
// now surfaces as a real error instead of a synthetic `unsupported: true`
// success, so the failure fires to Sentry for a root-cause fix.
let missing_err = list_configured_models("missing")
.await
.expect_err("404 list_models now surfaces as an error");
assert!(
missing_err.contains("provider returned 404"),
"404 list_models error should surface the status: {missing_err:?}"
);
let openrouter = list_configured_models("openrouter")
.await
.expect("openrouter key validation and list")
.value;
assert_eq!(openrouter["models"][0]["owned_by"], "test-suite");
for provider_id in ["html", "wrong-data", "error-payload", ""] {
let err = list_configured_models(provider_id)
.await
.expect_err("invalid model listing");
assert!(
err.contains("provider")
|| err.contains("provider_id")
|| err.contains("OpenRouter")
|| err.contains("parse JSON")
);
}
}
#[tokio::test]
async fn local_service_public_inference_assets_and_shutdown_use_loopback_ollama() {
let _env_lock = __shared_env_lock();
let (base, _state) = serve_mock().await;
let _ollama_env = EnvVarGuard::set("OPENHUMAN_OLLAMA_BASE_URL", &base);
let tmp = tempdir().expect("tempdir");
let mut config = temp_config(&tmp);
config.local_ai.runtime_enabled = true;
config.local_ai.base_url = Some(base);
config.local_ai.chat_model_id = "gemma3:1b-it-qat".to_string();
config.local_ai.vision_model_id = "llava:mock".to_string();
config.local_ai.embedding_model_id = "bge-m3".to_string();
config.local_ai.preload_embedding_model = true;
config.local_ai.preload_vision_model = false;
config.local_ai.preload_stt_model = false;
config.local_ai.preload_tts_voice = false;
config.local_ai.whisper_in_process = false;
let service = LocalAiService::new(&config);
let prompt = service
.prompt(&config, "Say hi", Some(8), true)
.await
.expect("prompt");
assert_eq!(prompt, "generated final");
let summarized = service
.summarize(&config, "one two three", Some(16))
.await
.expect("summarize");
assert_eq!(summarized, "generated final");
let completion = service
.inline_complete_interactive(
&config,
"OpenHuman is",
"concise",
Some("short"),
&["OpenHuman is useful".to_string()],
Some(6),
)
.await
.expect("inline");
assert_eq!(completion, "generated final");
let assets = service.assets_status(&config).await.expect("assets");
assert!(assets.ollama_available);
assert_eq!(assets.chat.state, "ready");
assert_eq!(assets.embedding.state, "ready");
assert!(matches!(assets.stt.state.as_str(), "ondemand" | "missing"));
assert!(matches!(
assets.tts.state.as_str(),
"ready" | "ondemand" | "missing"
));
let diagnostics = service.diagnostics(&config).await.expect("diagnostics");
assert_eq!(diagnostics["ollama_running"], true);
assert_eq!(diagnostics["expected"]["chat_found"], true);
assert!(
diagnostics["installed_models"]
.as_array()
.expect("installed_models")
.len()
>= 4
);
let progress = service.downloads_progress(&config).await.expect("progress");
assert_eq!(progress.chat.id, "gemma3:1b-it-qat");
assert_eq!(progress.embedding.id, "bge-m3");
let disabled_config = Config::default();
let disabled_err = service
.prompt(&disabled_config, "disabled", None, false)
.await
.expect_err("disabled prompt");
assert_eq!(disabled_err, "local ai is disabled");
service.shutdown_owned_ollama(&config).await;
assert!(!service.has_owned_ollama());
}
fn temp_config(tmp: &TempDir) -> Config {
let root = tmp.path().join(".openhuman");
std::fs::create_dir_all(root.join("workspace")).expect("workspace dir");
let mut config = Config::default();
config.config_path = root.join("config.toml");
config.workspace_dir = root.join("workspace");
config.secrets.encrypt = false;
config.api_url = Some("http://127.0.0.1:9".to_string());
config
}
fn seed_session(config: &Config) {
AuthService::from_config(config)
.store_provider_token(
APP_SESSION_PROVIDER,
DEFAULT_AUTH_PROFILE_NAME,
"session-token",
HashMap::new(),
true,
)
.expect("seed session");
}
async fn serve_mock() -> (String, MockState) {
let state = MockState::default();
let app = Router::new()
.route("/v1/chat/completions", post(chat_completions))
.route("/v1/responses", post(responses))
.route("/v1/models", get(models))
.route("/missing/models", get(missing_models))
.route("/html/models", get(html_models))
.route("/wrong-data/models", get(wrong_data_models))
.route("/error-payload/models", get(error_payload_models))
.route("/openrouter/key", get(openrouter_key))
.route("/openrouter/models", get(models))
.route("/api/tags", get(ollama_tags))
.route("/api/show", post(ollama_show))
.route("/api/generate", post(ollama_generate))
.with_state(state.clone());
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind mock");
let addr = listener.local_addr().expect("addr");
tokio::spawn(async move {
axum::serve(listener, app).await.expect("serve mock");
});
(format!("http://{addr}"), state)
}
async fn chat_completions(
State(state): State<MockState>,
headers: HeaderMap,
Json(body): Json<Value>,
) -> impl IntoResponse {
remember(&state, "/v1/chat/completions", &headers, body.clone());
let model = body["model"].as_str().unwrap_or_default();
if model == "responses-only" {
return (StatusCode::NOT_FOUND, Json(json!({"error": "not found"}))).into_response();
}
if model == "budget-model" {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": {"message": "budget exhausted for sk-very-secret"}})),
)
.into_response();
}
if model == "stream-model" && body["stream"] == true {
return sse_response();
}
if model == "tool-model" {
return Json(json!({
"choices": [{
"message": {
"content": "tool response",
"reasoning_content": "hidden reasoning",
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": {
"name": "lookup",
"arguments": { "query": "openhuman" }
}
}]
}
}],
"usage": {
"prompt_tokens": 1,
"completion_tokens": 2,
"prompt_tokens_details": { "cached_tokens": 1 }
},
"openhuman": {
"usage": {
"input_tokens": 11,
"output_tokens": 7,
"cached_input_tokens": 3
},
"billing": { "charged_amount_usd": 0.0042 }
}
}))
.into_response();
}
Json(json!({
"choices": [{
"message": {
"content": format!("chat:{model}"),
"function_call": {
"name": "legacy_tool",
"arguments": "{\"ok\":true}"
}
}
}]
}))
.into_response()
}
async fn responses(
State(state): State<MockState>,
headers: HeaderMap,
Json(body): Json<Value>,
) -> impl IntoResponse {
remember(&state, "/v1/responses", &headers, body);
Json(json!({
"output": [{
"content": [{ "type": "output_text", "text": "nested fallback text" }]
}],
"output_text": "responses fallback text"
}))
}
fn sse_response() -> Response<Body> {
let chunks = [
json!({"choices":[{"delta":{"content":"hello "}}]}).to_string(),
json!({"choices":[{"delta":{"reasoning_content":"thinking"}}]}).to_string(),
json!({"choices":[{"delta":{"content":"world","tool_calls":[{
"index":0,
"id":"call_stream",
"type":"function",
"function":{"name":"lookup","arguments":"{\"query\""}
}]}}]})
.to_string(),
json!({"choices":[{"delta":{"tool_calls":[{
"index":0,
"function":{"arguments":":\"stream\"}"}
}]}}]})
.to_string(),
json!({
"choices": [],
"usage": {
"prompt_tokens": 5,
"completion_tokens": 6,
"prompt_tokens_details": { "cached_tokens": 2 }
}
})
.to_string(),
];
let body = chunks
.into_iter()
.map(|chunk| format!("data: {chunk}\n\n"))
.chain(std::iter::once("data: [DONE]\n\n".to_string()))
.collect::<String>();
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "text/event-stream")
.body(Body::from(body))
.expect("sse response")
}
async fn models(State(state): State<MockState>, headers: HeaderMap) -> impl IntoResponse {
remember(&state, "/v1/models", &headers, Value::Null);
Json(json!({
"object": "list",
"data": [
{ "id": "demo-chat", "owned_by": "test-suite" },
{ "id": "demo-coder", "owned_by": "test-suite", "context_window": 8192 },
{ "owned_by": "ignored-without-id" }
]
}))
}
async fn missing_models() -> impl IntoResponse {
(
StatusCode::NOT_FOUND,
Json(json!({"error": "models unsupported"})),
)
}
async fn html_models() -> impl IntoResponse {
(StatusCode::OK, "<html>login</html>")
}
async fn wrong_data_models() -> impl IntoResponse {
Json(json!({ "object": "error", "data": { "message": "wrong shape" } }))
}
async fn error_payload_models() -> impl IntoResponse {
Json(json!({ "error": { "message": "bad sk-secret-key" } }))
}
async fn openrouter_key(headers: HeaderMap) -> impl IntoResponse {
let auth = header_value(&headers, "authorization").unwrap_or_default();
if auth == "Bearer openrouter-key" {
Json(json!({"data": {"label": "ok"}})).into_response()
} else {
(
StatusCode::UNAUTHORIZED,
Json(json!({"error": "missing key"})),
)
.into_response()
}
}
async fn ollama_tags() -> impl IntoResponse {
Json(json!({
"models": [
{ "name": "gemma3:1b-it-qat", "model": "gemma3:1b-it-qat" },
{ "name": "llava:mock", "model": "llava:mock" },
{ "name": "bge-m3", "model": "bge-m3" },
{ "name": "demo-chat", "model": "demo-chat" }
]
}))
}
async fn ollama_show(Json(body): Json<Value>) -> impl IntoResponse {
let model = body
.get("model")
.or_else(|| body.get("name"))
.and_then(Value::as_str)
.unwrap_or_default();
if model == "___nonexistent_probe___" {
return (
StatusCode::NOT_FOUND,
Json(json!({"error": "model not found"})),
)
.into_response();
}
Json(json!({
"model_info": {
"general.context_length": if model == "bge-m3" { 8192 } else { 4096 }
}
}))
.into_response()
}
async fn ollama_generate(Json(_body): Json<Value>) -> impl IntoResponse {
Json(json!({
"model": "gemma3:1b-it-qat",
"response": "generated final",
"done": true,
"prompt_eval_count": 8,
"prompt_eval_duration": 400000000,
"eval_count": 6,
"eval_duration": 300000000
}))
}
fn remember(state: &MockState, path: &str, headers: &HeaderMap, body: Value) {
state.requests.lock().expect("requests").push((
path.to_string(),
header_value(headers, "authorization"),
body,
));
}
fn header_value(headers: &HeaderMap, key: &str) -> Option<String> {
headers
.get(key)
.and_then(|value| value.to_str().ok())
.map(ToOwned::to_owned)
}