Fix loopback test networking.

This commit is contained in:
Georg Bauer
2026-07-21 19:02:07 +02:00
parent 9d3a092359
commit 489d3646f8
5 changed files with 83 additions and 21 deletions

View File

@@ -1,4 +1,5 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::net::IpAddr;
use std::sync::{LazyLock, Mutex}; use std::sync::{LazyLock, Mutex};
use std::time::Duration; use std::time::Duration;
@@ -427,7 +428,7 @@ pub fn load_endpoint_api_key(kind: AiEndpointKind) -> EngineResult<Option<String
pub fn refresh_model_catalog(endpoint: &AiEndpointConfig) -> EngineResult<Vec<AiModelInfo>> { pub fn refresh_model_catalog(endpoint: &AiEndpointConfig) -> EngineResult<Vec<AiModelInfo>> {
validate_endpoint_access(endpoint)?; validate_endpoint_access(endpoint)?;
let client = build_http_client()?; let client = build_http_client_for_endpoint(&endpoint.url)?;
let request = client.get(models_url(&endpoint.url)); let request = client.get(models_url(&endpoint.url));
let response = with_auth(request, endpoint).send()?.error_for_status()?; let response = with_auth(request, endpoint).send()?.error_for_status()?;
let body: Value = response.json()?; let body: Value = response.json()?;
@@ -494,7 +495,7 @@ pub fn test_chat(endpoint: &AiEndpointConfig, model: &str) -> EngineResult<()> {
"stream": false, "stream": false,
}); });
with_auth( with_auth(
build_http_client()? build_http_client_for_endpoint(&endpoint.url)?
.post(chat_completions_url(&endpoint.url)) .post(chat_completions_url(&endpoint.url))
.json(&payload), .json(&payload),
endpoint, endpoint,
@@ -535,7 +536,7 @@ pub fn run_one_shot(
} }
} }
}); });
let client = build_http_client()?; let client = build_http_client_for_endpoint(&endpoint.url)?;
let response = with_auth( let response = with_auth(
client client
.post(chat_completions_url(&endpoint.url)) .post(chat_completions_url(&endpoint.url))
@@ -575,8 +576,28 @@ fn parse_token_usage(body: &Value) -> TokenUsage {
} }
} }
fn build_http_client() -> EngineResult<Client> { fn build_http_client_for_endpoint(endpoint_url: &str) -> EngineResult<Client> {
Ok(Client::builder().timeout(Duration::from_secs(5)).build()?) let builder = Client::builder().timeout(Duration::from_secs(5));
let builder = if should_bypass_proxy(endpoint_url) {
builder.no_proxy()
} else {
builder
};
Ok(builder.build()?)
}
fn should_bypass_proxy(endpoint_url: &str) -> bool {
let Ok(url) = reqwest::Url::parse(endpoint_url) else {
return false;
};
let Some(host) = url.host_str() else {
return false;
};
host.eq_ignore_ascii_case("localhost")
|| host
.parse::<IpAddr>()
.map(|address| address.is_loopback())
.unwrap_or(false)
} }
fn load_endpoint(conn: &Connection, kind: AiEndpointKind) -> EngineResult<StoredAiEndpointConfig> { fn load_endpoint(conn: &Connection, kind: AiEndpointKind) -> EngineResult<StoredAiEndpointConfig> {
@@ -936,7 +957,7 @@ fn store_endpoint_api_key(kind: AiEndpointKind, api_key: &str) -> EngineResult<(
TEST_API_KEYS TEST_API_KEYS
.lock() .lock()
.map_err(|error| EngineError::Validation(error.to_string()))? .map_err(|error| EngineError::Validation(error.to_string()))?
.insert(kind.as_str().to_string(), api_key.to_string()); .insert(test_api_key_name(kind), api_key.to_string());
return Ok(()); return Ok(());
} }
endpoint_keyring_entry(kind)? endpoint_keyring_entry(kind)?
@@ -950,7 +971,7 @@ fn read_endpoint_api_key(kind: AiEndpointKind) -> EngineResult<Option<String>> {
.lock() .lock()
.map_err(|error| EngineError::Validation(error.to_string())) .map_err(|error| EngineError::Validation(error.to_string()))
.map(|keys| { .map(|keys| {
keys.get(kind.as_str()) keys.get(&test_api_key_name(kind))
.cloned() .cloned()
.filter(|key| !key.trim().is_empty()) .filter(|key| !key.trim().is_empty())
}); });
@@ -968,7 +989,7 @@ fn delete_endpoint_api_key(kind: AiEndpointKind) -> EngineResult<()> {
TEST_API_KEYS TEST_API_KEYS
.lock() .lock()
.map_err(|error| EngineError::Validation(error.to_string()))? .map_err(|error| EngineError::Validation(error.to_string()))?
.remove(kind.as_str()); .remove(&test_api_key_name(kind));
return Ok(()); return Ok(());
} }
match endpoint_keyring_entry(kind)?.delete_credential() { match endpoint_keyring_entry(kind)?.delete_credential() {
@@ -989,6 +1010,10 @@ fn cargo_test_process() -> bool {
}) })
} }
fn test_api_key_name(kind: AiEndpointKind) -> String {
format!("{:?}:{}", std::thread::current().id(), kind.as_str())
}
fn keyring_error(error: keyring::Error) -> EngineError { fn keyring_error(error: keyring::Error) -> EngineError {
EngineError::Validation(error.to_string()) EngineError::Validation(error.to_string())
} }

View File

@@ -1,5 +1,6 @@
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use std::io::Read; use std::io::Read;
use std::net::IpAddr;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock, mpsc}; use std::sync::{Arc, Mutex, OnceLock, mpsc};
use std::time::Duration; use std::time::Duration;
@@ -836,16 +837,39 @@ pub fn build_context(
Ok(result) Ok(result)
} }
fn chat_http_client(endpoint_url: &str) -> EngineResult<Client> {
let builder = Client::builder()
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(300));
let builder = if should_bypass_proxy(endpoint_url) {
builder.no_proxy()
} else {
builder
};
Ok(builder.build()?)
}
fn should_bypass_proxy(endpoint_url: &str) -> bool {
let Ok(url) = reqwest::Url::parse(endpoint_url) else {
return false;
};
let Some(host) = url.host_str() else {
return false;
};
host.eq_ignore_ascii_case("localhost")
|| host
.parse::<IpAddr>()
.map(|address| address.is_loopback())
.unwrap_or(false)
}
fn request_completion( fn request_completion(
endpoint: &AiEndpointConfig, endpoint: &AiEndpointConfig,
payload: &Value, payload: &Value,
cancelled: &AtomicBool, cancelled: &AtomicBool,
mut on_content: impl FnMut(&str), mut on_content: impl FnMut(&str),
) -> EngineResult<AssembledResponse> { ) -> EngineResult<AssembledResponse> {
let client = Client::builder() let client = chat_http_client(&endpoint.url)?;
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(300))
.build()?;
let mut request = client let mut request = client
.post(chat_completions_url(&endpoint.url)) .post(chat_completions_url(&endpoint.url))
.json(payload); .json(payload);

View File

@@ -664,6 +664,13 @@ mod tests {
GUARD.get_or_init(|| Mutex::new(())) GUARD.get_or_init(|| Mutex::new(()))
} }
fn preview_client() -> reqwest::blocking::Client {
reqwest::blocking::Client::builder()
.no_proxy()
.build()
.unwrap()
}
fn setup_preview_fixture() -> (tempfile::TempDir, Database) { fn setup_preview_fixture() -> (tempfile::TempDir, Database) {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("meta")).unwrap(); std::fs::create_dir_all(dir.path().join("meta")).unwrap();
@@ -1112,7 +1119,7 @@ mod tests {
) )
.unwrap(); .unwrap();
let client = reqwest::blocking::Client::new(); let client = preview_client();
let mut body = None; let mut body = None;
for _ in 0..20 { for _ in 0..20 {
if let Ok(response) = client if let Ok(response) = client
@@ -1229,7 +1236,7 @@ mod tests {
) )
.unwrap(); .unwrap();
let client = reqwest::blocking::Client::new(); let client = preview_client();
let response = client let response = client
.get(format!( .get(format!(
"http://{PREVIEW_HOST}:{PREVIEW_PORT}/media/../outside.txt" "http://{PREVIEW_HOST}:{PREVIEW_PORT}/media/../outside.txt"
@@ -1269,7 +1276,7 @@ mod tests {
) )
.unwrap(); .unwrap();
let client = reqwest::blocking::Client::new(); let client = preview_client();
let media = client let media = client
.get(format!("http://{PREVIEW_HOST}:{PREVIEW_PORT}/media/ok.txt")) .get(format!("http://{PREVIEW_HOST}:{PREVIEW_PORT}/media/ok.txt"))
.send() .send()
@@ -1362,7 +1369,7 @@ mod tests {
"project-1".into(), "project-1".into(),
) )
.unwrap(); .unwrap();
let client = reqwest::blocking::Client::new(); let client = preview_client();
let preview_html = client let preview_html = client
.get(format!( .get(format!(
"http://{PREVIEW_HOST}:{PREVIEW_PORT}/__draft/post-1" "http://{PREVIEW_HOST}:{PREVIEW_PORT}/__draft/post-1"
@@ -1427,7 +1434,7 @@ mod tests {
) )
.unwrap(); .unwrap();
let client = reqwest::blocking::Client::new(); let client = preview_client();
let response = client let response = client
.get(format!( .get(format!(
"http://{PREVIEW_HOST}:{PREVIEW_PORT}/__style-preview?theme=amber&mode=dark" "http://{PREVIEW_HOST}:{PREVIEW_PORT}/__style-preview?theme=amber&mode=dark"
@@ -1487,7 +1494,7 @@ mod tests {
) )
.unwrap(); .unwrap();
let client = reqwest::blocking::Client::new(); let client = preview_client();
let response = client let response = client
.get(format!( .get(format!(
"http://{PREVIEW_HOST}:{PREVIEW_PORT}/?theme=amber&mode=light" "http://{PREVIEW_HOST}:{PREVIEW_PORT}/?theme=amber&mode=light"

View File

@@ -671,7 +671,10 @@ fn protocol_routes_every_family_and_http_enforces_loopback_origin_and_cors() {
); );
let server = mcp::McpHttpServer::start(fixture.database_path.clone(), 0).unwrap(); let server = mcp::McpHttpServer::start(fixture.database_path.clone(), 0).unwrap();
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::builder()
.no_proxy()
.build()
.unwrap();
let response = client let response = client
.post(server.endpoint()) .post(server.endpoint())
.header("accept", "application/json, text/event-stream") .header("accept", "application/json, text/event-stream")

View File

@@ -123,8 +123,11 @@ mod tests {
#[test] #[test]
fn desktop_handler_queues_supported_menu_actions() { fn desktop_handler_queues_supported_menu_actions() {
let queued = Arc::new(Mutex::new(Vec::new())); let queued = Arc::new(Mutex::new(Vec::new()));
handler(Arc::clone(&queued), String::new())("trigger_menu_action", &[serde_json::json!("new_post")]) handler(Arc::clone(&queued), String::new())(
.unwrap(); "trigger_menu_action",
&[serde_json::json!("new_post")],
)
.unwrap();
assert_eq!(queued.lock().unwrap().as_slice(), &[MenuAction::NewPost]); assert_eq!(queued.lock().unwrap().as_slice(), &[MenuAction::NewPost]);
} }
} }