Luigit
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;
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<serde_json::Value> {
    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::<Vec<_>>(),
        ["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"], "<redacted>");

    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::<Vec<_>>(),
        ["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);
}