Luigit
repositories / smith

smith

There are many coding harnesses - but this one is fast

owned by admin

smith-core/tests/trace_snapshot.rs

Raw
//! Durable trace snapshot (`SMH-SPEC-SPEC0001`, Sessions).
//!
//! Entries are persisted through real tool and agent records so compaction
//! prunes live context while this snapshot covers durable replay history.
//! Session, branch, entry, message, tool-call ids, and timestamps are redacted.

use futures::StreamExt;
use serde_json::Value;
use smith::config::Config;
use smith::provider::StreamFn;
use smith::stream::{StopReason, StreamEvent, Usage};
use smith_core::agent::{Agent, CompactionSummary};
use smith_core::session::EntryKind;
use smith_core::store::{SessionReader, open_session};
use smith_core::tools::ToolSession;
use smith_core::trace::trace;

/// One JSON line per durable trace record, with run-specific values replaced.
fn redacted_trace(reader: &SessionReader, frame_bytes_max: usize) -> String {
    let header = reader.header(frame_bytes_max).unwrap();
    let mut placeholders = vec![
        (header.session_id.to_string(), "<session>".to_string()),
        (header.branch_id.to_string(), "<branch>".to_string()),
    ];
    if let Some(tip) = reader.selected_tip(frame_bytes_max).unwrap() {
        let mut entry_count = 0;
        let mut call_count = 0;
        reader
            .replay_at(tip, frame_bytes_max, |entry| {
                let index = entry_count;
                entry_count += 1;
                placeholders.push((entry.id.to_string(), format!("<entry-{index}>")));
                match entry.kind {
                    EntryKind::Message { id, .. } => {
                        placeholders.push((id.to_string(), format!("<message-{index}>")));
                    }
                    EntryKind::ToolCall { call_id } => {
                        placeholders.push((call_id.to_string(), format!("<call-{call_count}>")));
                        call_count += 1;
                    }
                    EntryKind::ToolResult { .. } | EntryKind::Meta { .. } => {}
                }
                Ok(())
            })
            .unwrap();
    }
    let redact = |text: &str| {
        placeholders
            .iter()
            .fold(text.to_string(), |text, (id, placeholder)| {
                text.replace(id, placeholder)
            })
    };
    let mut lines = Vec::new();
    trace(reader, frame_bytes_max, |record| {
        let mut line = serde_json::to_value(record).unwrap();
        if line["timestamp_ms"] != 0 {
            line["timestamp_ms"] = Value::from("<timestamp>");
        }
        lines.push(redact(&line.to_string()));
        Ok(())
    })
    .unwrap();
    lines.join("\n")
}

fn final_responses() -> StreamFn {
    let responses = std::sync::Arc::new(std::sync::Mutex::new(vec![
        vec![
            StreamEvent::text_delta(smith::id::MessageId::new(), "listed"),
            StreamEvent::stop(StopReason::EndTurn, Some(Usage::new())),
        ],
        vec![
            StreamEvent::text_delta(smith::id::MessageId::new(), "still listed"),
            StreamEvent::stop(StopReason::EndTurn, Some(Usage::new())),
        ],
    ]));
    std::sync::Arc::new(move |_, _| {
        futures::stream::iter(responses.lock().unwrap().remove(0).into_iter().map(Ok)).boxed()
    })
}

#[test]
fn trace_of_message_tool_round_trip_and_compaction() {
    let dir = std::env::temp_dir().join(format!("smith_trace_snapshot_{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    let path = dir.join("trace.smh");
    let config = Config::default_valid();
    let opened = open_session(&path, config.clone()).unwrap();
    let mut tools = ToolSession::with_session(&dir, opened.session, opened.writer);
    tools
        .invoke(
            "write",
            &smith::ToolArguments::from_serializable(
                &serde_json::json!({"path": "a.txt", "content": "a"}),
            )
            .unwrap(),
        )
        .unwrap();
    let mut agent = Agent::new(final_responses(), tools, "mock-smith");
    agent.run_turn("list files").unwrap();
    agent
        .compact(&CompactionSummary {
            text: "listed a.txt".to_string(),
            complete: true,
        })
        .unwrap();
    agent.run_turn("and after compaction?").unwrap();

    let reader = SessionReader::open(&path).unwrap();
    insta::assert_snapshot!(redacted_trace(&reader, config.frame_bytes_max));
    drop(reader);
    drop(agent);
    let _ = std::fs::remove_dir_all(&dir);
}