repositories / smith
smith
There are many coding harnesses - but this one is fast
owned by admin
smith-harness/tests/http_effect.rs
Raw//! 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<serde_json::Value> {
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"], "<redacted>");
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);
}