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::net::IpAddr;
use std::sync::{LazyLock, Mutex};
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>> {
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 response = with_auth(request, endpoint).send()?.error_for_status()?;
let body: Value = response.json()?;
@@ -494,7 +495,7 @@ pub fn test_chat(endpoint: &AiEndpointConfig, model: &str) -> EngineResult<()> {
"stream": false,
});
with_auth(
build_http_client()?
build_http_client_for_endpoint(&endpoint.url)?
.post(chat_completions_url(&endpoint.url))
.json(&payload),
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(
client
.post(chat_completions_url(&endpoint.url))
@@ -575,8 +576,28 @@ fn parse_token_usage(body: &Value) -> TokenUsage {
}
}
fn build_http_client() -> EngineResult<Client> {
Ok(Client::builder().timeout(Duration::from_secs(5)).build()?)
fn build_http_client_for_endpoint(endpoint_url: &str) -> EngineResult<Client> {
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> {
@@ -936,7 +957,7 @@ fn store_endpoint_api_key(kind: AiEndpointKind, api_key: &str) -> EngineResult<(
TEST_API_KEYS
.lock()
.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(());
}
endpoint_keyring_entry(kind)?
@@ -950,7 +971,7 @@ fn read_endpoint_api_key(kind: AiEndpointKind) -> EngineResult<Option<String>> {
.lock()
.map_err(|error| EngineError::Validation(error.to_string()))
.map(|keys| {
keys.get(kind.as_str())
keys.get(&test_api_key_name(kind))
.cloned()
.filter(|key| !key.trim().is_empty())
});
@@ -968,7 +989,7 @@ fn delete_endpoint_api_key(kind: AiEndpointKind) -> EngineResult<()> {
TEST_API_KEYS
.lock()
.map_err(|error| EngineError::Validation(error.to_string()))?
.remove(kind.as_str());
.remove(&test_api_key_name(kind));
return Ok(());
}
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 {
EngineError::Validation(error.to_string())
}

View File

@@ -1,5 +1,6 @@
use std::collections::{BTreeMap, HashMap};
use std::io::Read;
use std::net::IpAddr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock, mpsc};
use std::time::Duration;
@@ -836,16 +837,39 @@ pub fn build_context(
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(
endpoint: &AiEndpointConfig,
payload: &Value,
cancelled: &AtomicBool,
mut on_content: impl FnMut(&str),
) -> EngineResult<AssembledResponse> {
let client = Client::builder()
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(300))
.build()?;
let client = chat_http_client(&endpoint.url)?;
let mut request = client
.post(chat_completions_url(&endpoint.url))
.json(payload);

View File

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

View File

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