//! 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, read_frames}; use smith_core::session::{EntryContent, PROVIDER_HTTP_KIND}; /// 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(); let mut buffer = [0u8; 8192]; let _ = socket.read(&mut buffer); 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 bytes = std::fs::read(path).unwrap(); let config = smith::config::Config::default_valid(); read_frames(&mut bytes.as_slice(), config.max_frame_bytes) .frames .into_iter() .filter_map(|frame| match frame { Frame::Known { entry } => match EntryContent::decode(&entry.content).unwrap() { EntryContent::Meta { kind, detail } if kind == PROVIDER_HTTP_KIND => Some(detail), _ => None, }, Frame::Unknown { .. } | smith_core::Frame::Header { .. } => None, }) .collect() } #[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["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"); 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); }