//! Read-only inspection through the built binary (`SMH-SPEC-SPEC0001`, Session inspection). use assert_cmd::Command; use serde_json::Value; use std::path::Path; fn smith(home: &Path, args: &[&str]) -> std::process::Output { Command::cargo_bin("smith") .unwrap() .env("XDG_DATA_HOME", home) .env_remove("SMITH_PROVIDER") .env_remove("SMITH_MODEL") .env_remove("SMITH_BASE_URL") .env_remove("OPENAI_API_KEY") .env_remove("ANTHROPIC_API_KEY") .env_remove("GEMINI_API_KEY") .args(args) .timeout(std::time::Duration::from_secs(30)) .output() .unwrap() } #[test] fn session_list_limit_reports_capacity_not_corruption() { // Create real sessions through eval; inspection must reject capacity without claiming damage. let home = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../target/style-exec") .join(format!("list-limit-e2e-{}", std::process::id())); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(120); for _ in 0..1025 { assert!( std::time::Instant::now() < deadline, "session fixture deadline" ); let created = smith(&home, &["eval", "--mock", "fixture"]); assert!( created.status.success(), "{}", String::from_utf8_lossy(&created.stderr) ); } let listed = smith(&home, &["sessions", "list", "--json"]); assert!(!listed.status.success()); let diagnostic = String::from_utf8(listed.stderr).unwrap(); assert!(diagnostic.contains("SESSION_LIST_LIMIT")); assert!(diagnostic.contains("inspect sessions individually")); assert!(!diagnostic.contains("SESSION_CORRUPT")); let records = String::from_utf8(listed.stdout).unwrap(); assert_eq!(records.lines().count(), 1024); assert!( records .lines() .all(|line| serde_json::from_str::(line).is_ok()) ); std::fs::remove_dir_all(home).unwrap(); } #[test] fn sessions_list_get_grep_jsonl_and_no_match() { let home = std::env::temp_dir().join(format!("smith-sessions-e2e-{}", std::process::id())); let _ = std::fs::remove_dir_all(&home); assert!( smith(&home, &["eval", "--mock", "needle \u{1b}[31m in haystack"]) .status .success() ); assert!( smith(&home, &["eval", "--mock", "other haystack"]) .status .success() ); let listed = smith(&home, &["sessions", "list", "--json"]); assert!( listed.status.success(), "{}", String::from_utf8_lossy(&listed.stderr) ); let listed = String::from_utf8(listed.stdout).unwrap(); let lines: Vec = listed .lines() .map(|line| serde_json::from_str(line).unwrap()) .collect(); assert_eq!(lines.len(), 2); let id = lines[0]["session"].as_str().unwrap(); let human_list = smith(&home, &["sessions", "list"]); assert!(human_list.status.success()); let (mut json_bytes, mut human_bytes) = (String::new(), String::new()); for line in &lines { let (session, bytes) = (line["session"].as_str().unwrap(), &line["bytes"]); json_bytes += &format!("{{\"session\":\"{session}\",\"bytes\":{bytes}}}\n"); human_bytes += &format!("{session} {bytes} bytes\n"); } assert_eq!(listed, json_bytes, "JSON list lines keep their exact bytes"); assert_eq!( String::from_utf8(human_list.stdout).unwrap(), human_bytes, "human list lines keep their exact bytes" ); let retrieved = smith(&home, &["sessions", "get", id, "--json"]); assert!( retrieved.status.success(), "{}", String::from_utf8_lossy(&retrieved.stderr) ); let records: Vec = String::from_utf8(retrieved.stdout) .unwrap() .lines() .map(|line| serde_json::from_str(line).unwrap()) .collect(); assert_eq!(records.len(), 2); assert!( records .iter() .all(|record| record["content"]["kind"] == "Message") ); assert!( records .iter() .all(|record| record["content"]["data"]["blocks"][0]["type"] == "Text") ); assert!( records .iter() .all(|record| record["content"]["data"]["blocks"][0]["data"].is_string()) ); let human_get = smith(&home, &["sessions", "get", id]); assert!(human_get.status.success()); assert!(!human_get.stdout.contains(&0x1b)); let matches = smith(&home, &["sessions", "grep", "needle", "--json"]); assert!( matches.status.success(), "{}", String::from_utf8_lossy(&matches.stderr) ); let found: Vec = String::from_utf8(matches.stdout) .unwrap() .lines() .map(|line| serde_json::from_str(line).unwrap()) .collect(); assert_eq!(found.len(), 1); assert!(found[0]["entry"].is_string()); assert_eq!( found[0]["content"]["data"]["blocks"][0]["data"], "needle \u{1b}[31m in haystack" ); let human_grep = smith(&home, &["sessions", "grep", "needle"]); assert!(human_grep.status.success()); assert!( String::from_utf8(human_grep.stdout) .unwrap() .contains("needle") ); let none = smith(&home, &["sessions", "grep", "not-in-any-session"]); assert_eq!(none.status.code(), Some(1)); assert!(none.stdout.is_empty()); assert!(none.stderr.is_empty()); let _ = std::fs::remove_dir_all(home); } #[test] fn corrupt_session_is_reported_without_repairing_source() { // SMH-SPEC-SPEC0001: Session inspection never repairs or hides corruption. let home = std::env::temp_dir().join(format!("smith-corrupt-e2e-{}", std::process::id())); let _ = std::fs::remove_dir_all(&home); assert!(smith(&home, &["eval", "--mock", "before"]).status.success()); let dir = home.join("smith/sessions/run"); let path = std::fs::read_dir(&dir) .unwrap() .map(|entry| entry.unwrap().path()) .find(|path| path.extension().is_some_and(|ext| ext == "smh")) .unwrap(); let mut bytes = std::fs::read(&path).unwrap(); let last = bytes.len() - 1; bytes[last] ^= 0xff; std::fs::write(&path, &bytes).unwrap(); let id = path.file_stem().unwrap().to_str().unwrap(); let output = smith(&home, &["sessions", "get", id]); assert_eq!(output.status.code(), Some(2)); assert!(String::from_utf8_lossy(&output.stderr).contains("SESSION_CORRUPT")); assert_eq!(std::fs::read(path).unwrap(), bytes); let _ = std::fs::remove_dir_all(home); }