Luigit
repositories / smith

smith

There are many coding harnesses - but this one is fast

owned by admin

smith-cli/tests/e2e_eval.rs

Raw
//! End-to-end `smith eval` workflows through the built binary
//! (`SMH-SPEC-SPEC0001`, Interface modes: Eval).

use assert_cmd::Command;
use smith_core::TraceRecord;
use smith_core::store::SessionReader;
use smith_core::trace::trace;
use std::io::{Read, Write};
use std::net::TcpListener;
use std::path::{Path, PathBuf};

const MOCK_REPLY: &str = "mock provider: no model configured";

/// A fresh data home per test; stale runs never leak into assertions.
fn scratch(name: &str) -> PathBuf {
    let dir = std::env::temp_dir().join(format!("smith-eval-test-{name}-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    dir
}

fn smith(data_home: &Path) -> Command {
    let mut command = Command::cargo_bin("smith").unwrap();
    command
        .env("XDG_DATA_HOME", data_home)
        .env_remove("SMITH_PROVIDER")
        .env_remove("SMITH_MODEL")
        .env_remove("SMITH_BASE_URL")
        .env_remove("ANTHROPIC_API_KEY")
        .env_remove("OPENAI_API_KEY")
        .env_remove("GEMINI_API_KEY");
    command
}

fn sessions(data_home: &Path) -> Vec<PathBuf> {
    let dir = data_home.join("smith/sessions/run");
    let mut paths: Vec<_> = std::fs::read_dir(dir)
        .unwrap()
        .map(|entry| entry.unwrap().path())
        .filter(|path| path.extension().is_some_and(|ext| ext == "smh"))
        .collect();
    paths.sort();
    paths
}

/// Decode the session file the binary left behind into its trace.
fn trace_of(path: &Path) -> Vec<TraceRecord> {
    let max = smith::config::Config::default_valid().frame_bytes_max;
    let reader = SessionReader::open(path).unwrap();
    let recovery = reader.scan(max, |_| Ok(())).unwrap();
    assert!(recovery.damage.is_empty(), "{:?}", recovery.damage);
    assert_eq!(recovery.torn_tail_at, None);
    let mut records = Vec::new();
    trace(&reader, max, |record| {
        records.push(record);
        Ok(())
    })
    .unwrap();
    records
}

#[test]
fn eval_session_directory_failure_reports_storage_path() {
    let home = scratch("blocked-session-dir");
    assert!(std::fs::write(&home, "block data directory").is_ok());
    let attempted = home.join("smith/sessions/run");
    let output = smith(&home)
        .args(["eval", "--mock", "hello"])
        .output()
        .unwrap();
    assert!(!output.status.success());
    assert!(output.stdout.is_empty());
    let stderr = String::from_utf8(output.stderr).unwrap();
    assert!(stderr.starts_with("smith: storage [RUNTIME_DIR] create_dir_all "));
    assert!(stderr.contains(&attempted.display().to_string()));
    assert!(!stderr.contains("block data directory"));
    assert!(std::fs::remove_file(home).is_ok());
}

#[test]
fn eval_mock_turn_prints_the_reply() {
    let home = scratch("text");
    smith(&home)
        .args(["eval", "--mock", "hello"])
        .assert()
        .success()
        .stdout(format!("{MOCK_REPLY}\n"))
        .stderr("");
}

#[test]
fn eval_tool_round_persists_binary_content_for_get_and_grep() {
    let home = scratch("tool-inspection");
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
    let port = listener.local_addr().unwrap().port();
    let server = std::thread::spawn(move || {
        let responses = [
            "data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_fixture\",\"function\":{\"name\":\"write\",\"arguments\":\"{\\\"path\\\":\\\"tool-fixture.txt\\\",\\\"content\\\":\\\"written\\\"}\"}}]}}]}\n\ndata: {\"choices\":[{\"delta\":{},\"finish_reason\":\"tool_calls\"}]}\n\ndata: [DONE]\n\n",
            "data: {\"choices\":[{\"delta\":{\"content\":\"Done.\"}}]}\n\ndata: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n",
        ];
        for response in responses {
            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(|bytes| bytes == 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]);
            }
            write!(socket, "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nConnection: close\r\nContent-Length: {}\r\n\r\n{response}", response.len()).unwrap();
        }
    });
    let output = smith(&home)
        .env("OPENAI_API_KEY", "sk-fixture")
        .args([
            "eval",
            "--provider",
            "openai",
            "--model",
            "test-model",
            "--base-url",
            &format!("http://127.0.0.1:{port}/v1"),
            "write fixture",
        ])
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert_eq!(output.stdout, b"Done.\n");
    server.join().unwrap();
    assert_eq!(
        std::fs::read(home.join("smith/sessions/run/tool-fixture.txt")).unwrap(),
        b"written"
    );
    let stored = sessions(&home);
    assert_eq!(stored.len(), 1);
    let id = stored[0].file_stem().unwrap().to_str().unwrap();
    let got = smith(&home)
        .args(["sessions", "get", id, "--json"])
        .output()
        .unwrap();
    assert!(
        got.status.success(),
        "{}",
        String::from_utf8_lossy(&got.stderr)
    );
    let records: Vec<serde_json::Value> = String::from_utf8(got.stdout)
        .unwrap()
        .lines()
        .map(|line| serde_json::from_str(line).unwrap())
        .collect();
    assert!(records.iter().any(|r| r["content"]["kind"] == "ToolCall"
        && r["content"]["data"]["input"]["content"] == "written"));
    assert!(
        records
            .iter()
            .any(|r| r["content"]["kind"] == "ToolResult" && r["content"]["data"]["ok"] == true)
    );
    let grep = smith(&home)
        .args(["sessions", "grep", "tool-fixture.txt", "--json"])
        .output()
        .unwrap();
    assert!(
        grep.status.success(),
        "{}",
        String::from_utf8_lossy(&grep.stderr)
    );
    assert!(String::from_utf8(grep.stdout).unwrap().contains("ToolCall"));
    let _ = std::fs::remove_dir_all(home);
}

#[test]
fn eval_json_reports_text_reason_and_cost() {
    let home = scratch("json");
    let output = smith(&home)
        .args(["eval", "--mock", "--json", "hello"])
        .output()
        .unwrap();
    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert_eq!(stdout.lines().count(), 1, "{stdout}");
    // The mock reply and its zero cost are fixed; the line is pinned byte for byte.
    insta::assert_snapshot!("eval_mock_json", stdout);
}

#[test]
fn eval_creates_distinct_sessions_and_explicit_resume_keeps_history() {
    let home = scratch("resume");
    smith(&home)
        .args(["eval", "--mock", "first"])
        .assert()
        .success();
    let first = sessions(&home);
    assert_eq!(first.len(), 1);
    let original = trace_of(&first[0]);
    assert!(original.len() > 1);
    smith(&home)
        .args(["eval", "--mock", "second"])
        .assert()
        .success();
    assert_eq!(sessions(&home).len(), 2);
    assert_eq!(trace_of(&first[0]), original);

    let id = first[0].file_stem().unwrap().to_str().unwrap();
    smith(&home)
        .args(["eval", "--mock", "--session", id, "third"])
        .assert()
        .success();
    assert_eq!(sessions(&home).len(), 2);
    let resumed = trace_of(&first[0]);
    assert!(resumed.len() > original.len());
    assert_eq!(resumed[..original.len()], original);
}