use std::io;
use std::path::Path;
pub fn is_safe_run_id(s: &str) -> bool {
!s.is_empty()
&& s.len() <= 128
&& s.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}
pub fn read_trace(run_dir: &Path) -> io::Result<Vec<serde_json::Value>> {
let path = run_dir.join("trace.jsonl");
if !path.exists() {
return Ok(Vec::new());
}
let raw = std::fs::read_to_string(&path)?;
let mut out: Vec<serde_json::Value> = Vec::new();
for (i, line) in raw.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
match serde_json::from_str::<serde_json::Value>(trimmed) {
Ok(v) => out.push(v),
Err(e) => {
tracing::warn!(line_no = i + 1, err = %e, "trace_reader: skipping malformed line");
}
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
fn fresh_dir(label: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!("solomd-mcp-trace-{label}-{nanos}"));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn run_id_validation_matches_app_crate() {
assert!(!is_safe_run_id(""));
assert!(!is_safe_run_id("../escape"));
assert!(!is_safe_run_id("a/b"));
assert!(is_safe_run_id("20260430-142307-a3f9c1"));
}
#[test]
fn reads_known_fixture_layout() {
let dir = fresh_dir("readok");
let body = r#"{"ts":1,"run_id":"r","seq":1,"kind":"run_started","run_kind":"panel","provider":"anthropic","model":"claude-sonnet-4-6"}
{"ts":2,"run_id":"r","seq":2,"kind":"prompt","role":"user","content":"hi"}
this line is malformed and must be skipped
{"ts":3,"run_id":"r","seq":3,"kind":"tool_call","tool":"read_note","tool_call_id":"tc_a1","args":{"path":"x.md"}}
{"ts":4,"run_id":"r","seq":4,"kind":"run_ended","status":"ok","tokens_in_total":10,"tokens_out_total":5,"cost_usd_estimate":0.001}
"#;
fs::write(dir.join("trace.jsonl"), body).unwrap();
let lines = read_trace(&dir).unwrap();
assert_eq!(lines.len(), 4);
assert_eq!(lines[0]["kind"], "run_started");
assert_eq!(lines[2]["kind"], "tool_call");
assert_eq!(lines[2]["tool"], "read_note");
assert_eq!(lines[3]["kind"], "run_ended");
}
#[test]
fn missing_file_returns_empty() {
let dir = fresh_dir("missing");
let lines = read_trace(&dir).unwrap();
assert!(lines.is_empty());
}
}