//! End-to-end provider traffic through the typed HTTP effect //! (`SMH-SPEC-SPEC0001`, Network transport). //! Integration tests drive a real socket, a real executor, and the //! durable session store. use smith_harness::runtime::{RuntimeConfig, RuntimeProvider, build_agent}; use std::io::{Read, Write}; use std::net::TcpListener; use smith_core::frame::Frame; use smith_core::session::{EntryContent, PROVIDER_HTTP_KIND}; use smith_core::store::SessionReader; /// Serve one OpenAI-shaped SSE response and return the port. fn spawn_sse_server(body: &'static str) -> u16 { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let port = listener.local_addr().unwrap().port(); std::thread::spawn(move || { let (mut socket, _) = listener.accept().unwrap(); socket .set_read_timeout(Some(std::time::Duration::from_secs(10))) .unwrap(); let mut request = Vec::new(); let mut buffer = [0u8; 4096]; let header_end = loop { let n = socket.read(&mut buffer).unwrap(); assert!(n > 0 && request.len() + n <= 1 << 20); request.extend_from_slice(&buffer[..n]); if let Some(at) = request.windows(4).position(|part| part == b"\r\n\r\n") { break at + 4; } }; let headers = std::str::from_utf8(&request[..header_end]).unwrap(); let len: usize = headers .lines() .find_map(|line| { line.to_ascii_lowercase() .strip_prefix("content-length: ") .and_then(|value| value.trim().parse().ok()) }) .unwrap(); while request.len() < header_end + len { let n = socket.read(&mut buffer).unwrap(); assert!(n > 0 && request.len() + n <= 1 << 20); request.extend_from_slice(&buffer[..n]); } let response = format!( "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nConnection: close\r\n\r\n{body}" ); socket.write_all(response.as_bytes()).unwrap(); }); port } fn provider_http_records(path: &std::path::Path) -> Vec { let config = smith::config::Config::default_valid(); let mut records = Vec::new(); SessionReader::open(path) .unwrap() .scan(config.frame_bytes_max, |frame| { if let Frame::Known { entry } = frame && let EntryContent::Meta { kind, detail } = EntryContent::decode(&entry.content)? && kind == PROVIDER_HTTP_KIND { records.push(serde_json::to_value(detail).unwrap()); } Ok(()) }) .unwrap(); records } #[test] fn provider_round_trips_through_http_effect_with_durable_redacted_records() { let dir = std::env::temp_dir().join(format!("smith_http_effect_{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let sse = "data: {\"choices\":[{\"delta\":{\"content\":\"hello \"}}]}\n\n\ data: {\"choices\":[{\"delta\":{\"content\":\"world\"}}]}\n\n\ data: {\"choices\":[{\"finish_reason\":\"stop\",\"delta\":{}}],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":2}}\n\n\ data: [DONE]\n\n"; let port = spawn_sse_server(sse); let config = RuntimeConfig { provider: RuntimeProvider::OpenAiCompatible { base_url: format!("http://127.0.0.1:{port}/v1"), model: "gpt-test".to_string(), }, api_key: Some("sk-integration-secret".to_string()), ..RuntimeConfig::mock(&dir, Vec::new()) }; let mut agent = build_agent(&config).unwrap(); let outcome = agent.run_turn("say it").unwrap(); assert_eq!(outcome.text, "hello world"); let records = provider_http_records(&config.session_path); assert_eq!(records.len(), 2, "intent and outcome records"); let intent = &records[0]; assert_eq!(intent["phase"], "intent"); assert_eq!(intent["method"], "POST"); assert!(intent["request_bytes"].as_u64().unwrap() > 0); assert_eq!( intent .as_object() .unwrap() .keys() .map(String::as_str) .collect::>(), ["url", "phase", "method", "headers", "request_bytes"] ); assert_eq!( intent["url"], format!("http://127.0.0.1:{port}/v1/chat/completions") ); let headers = intent["headers"].as_array().unwrap(); let authorization = headers .iter() .find(|header| { header["name"] .as_str() .unwrap_or_default() .eq_ignore_ascii_case("authorization") }) .unwrap(); assert_eq!(authorization["value"], ""); let outcome_record = &records[1]; assert_eq!(outcome_record["phase"], "outcome"); assert_eq!(outcome_record["status"], 200); assert_eq!(outcome_record["outcome"], "completed"); assert_eq!( outcome_record["response_bytes"].as_u64(), Some(sse.len() as u64) ); assert_eq!( outcome_record .as_object() .unwrap() .keys() .map(String::as_str) .collect::>(), ["phase", "status", "outcome", "response_bytes"] ); assert!( !serde_json::to_string(&records) .unwrap() .contains("sk-integration-secret") ); let _ = std::fs::remove_dir_all(&dir); } #[test] fn explicit_credential_wins_over_missing_env() { // No store, no env: only the explicit override can resolve. let dir = std::env::temp_dir().join(format!("smith_http_explicit_{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let store = smith_ai::auth::AuthStore::at(dir.join("empty-store")); assert!( store .load(smith_ai::models::ProviderKind::Anthropic) .unwrap() .is_none() ); let config = RuntimeConfig { provider: RuntimeProvider::Anthropic { base_url: "https://example.test".to_string(), model: "m".to_string(), }, api_key: Some("sk-explicit".to_string()), ..RuntimeConfig::mock(&dir, Vec::new()) }; let _agent = build_agent(&config).unwrap(); let _ = std::fs::remove_dir_all(&dir); }