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::trace::build_trace;
use smith_core::{Session, TraceRecord, load_session};
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 eval_session(data_home: &Path) -> PathBuf {
    data_home.join("smith/sessions/run/eval.smh")
}

/// Decode the session file the binary left behind into its trace.
fn trace_of(path: &Path) -> Vec<TraceRecord> {
    let recovery =
        load_session(path, smith::config::Config::default_valid().max_frame_bytes).unwrap();
    assert!(recovery.damage.is_empty(), "{:?}", recovery.damage);
    assert_eq!(recovery.torn_tail_at, None);
    build_trace(&Session::from_frames(&recovery.frames).unwrap())
}

#[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_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 second_eval_appends_to_the_same_session_and_keeps_the_first_trace() {
    let home = scratch("resume");
    let session = eval_session(&home);

    smith(&home)
        .args(["eval", "--mock", "first"])
        .assert()
        .success();
    let first_len = std::fs::metadata(&session).unwrap().len();
    let first_trace = trace_of(&session);
    assert!(first_trace.len() > 1, "{first_trace:?}");

    smith(&home)
        .args(["eval", "--mock", "second"])
        .assert()
        .success()
        .stdout(format!("{MOCK_REPLY}\n"));
    let second_len = std::fs::metadata(&session).unwrap().len();
    let second_trace = trace_of(&session);

    assert!(second_len > first_len, "{second_len} <= {first_len}");
    assert!(second_trace.len() > first_trace.len());
    assert_eq!(second_trace[..first_trace.len()], first_trace[..]);
}