use std::path::PathBuf;
use std::process::Command;
use std::{fs, str};
const SECRET: &str = "sk-abcDEF1234567890ghijkl";
struct World {
base: PathBuf,
home: PathBuf,
cv_home: PathBuf,
}
impl World {
fn new(tag: &str) -> World {
let base = std::env::temp_dir().join(format!(
"cv-cli-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let home = base.join("home");
let cv_home = base.join("cvhome");
fs::create_dir_all(home.join(".claude/projects/-work-proj")).unwrap();
fs::create_dir_all(&cv_home).unwrap();
World { base, home, cv_home }
}
fn write_session(&self, name: &str, lines: &[serde_json::Value]) -> PathBuf {
let path = self
.home
.join(".claude/projects/-work-proj")
.join(format!("{name}.jsonl"));
let body: String = lines.iter().map(|l| format!("{l}\n")).collect();
fs::write(&path, body).unwrap();
path
}
fn cv(&self, args: &[&str]) -> (bool, i32, String, String) {
let out = Command::new(env!("CARGO_BIN_EXE_cv"))
.args(args)
.current_dir(&self.base)
.env("HOME", &self.home)
.env("CLUSTERVISION_HOME", &self.cv_home)
.env("XDG_CACHE_HOME", self.home.join(".cache"))
.env("XDG_CONFIG_HOME", self.home.join(".config"))
.env("XDG_DATA_HOME", self.home.join(".local/share"))
.output()
.expect("cv should run");
(
out.status.success(),
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
fn cv_ok(&self, args: &[&str]) -> (String, String) {
let (ok, code, out, err) = self.cv(args);
assert!(ok, "cv {args:?} exited {code}\nstdout:\n{out}\nstderr:\n{err}");
(out, err)
}
fn cv_fails(&self, args: &[&str]) -> (String, String) {
let (ok, code, out, err) = self.cv(args);
assert!(!ok, "cv {args:?} unexpectedly succeeded\nstdout:\n{out}");
assert_ne!(code, -1, "cv {args:?} died by signal (panic/abort?)\nstderr:\n{err}");
assert!(
!err.trim().is_empty(),
"cv {args:?} failed silently (empty stderr)\nstdout:\n{out}"
);
assert!(
!err.contains("panicked"),
"cv {args:?} panicked instead of erroring:\n{err}"
);
(out, err)
}
}
impl Drop for World {
fn drop(&mut self) {
fs::remove_dir_all(&self.base).ok();
}
}
fn user_line(uuid: &str, ts: &str, text: &str) -> serde_json::Value {
serde_json::json!({
"type": "user", "uuid": uuid, "sessionId": "s", "timestamp": ts,
"cwd": "/work/proj",
"message": {"role": "user", "content": text}
})
}
fn assistant_line(uuid: &str, ts: &str, blocks: serde_json::Value) -> serde_json::Value {
serde_json::json!({
"type": "assistant", "uuid": uuid, "sessionId": "s", "timestamp": ts,
"cwd": "/work/proj",
"message": {"role": "assistant", "model": "claude-test-1", "content": blocks}
})
}
fn standard_corpus(w: &World) {
w.write_session(
"alphasess",
&[
serde_json::json!({"type": "ai-title", "aiTitle": "alpha adventures"}),
user_line("u1", "2026-01-01T10:00:00Z", "please fix the zebrafish migration"),
assistant_line(
"a1",
"2026-01-02T10:00:00Z",
serde_json::json!([
{"type": "text", "text": format!("on it — found a leaked key {SECRET} in the env")},
{"type": "tool_use", "id": "t1", "name": "Edit",
"input": {"file_path": "/work/proj/src/widget.rs", "old_string": "a", "new_string": "b"}}
]),
),
serde_json::json!({
"type": "user", "uuid": "u2", "sessionId": "s", "timestamp": "2026-06-01T10:00:00Z",
"message": {"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "t1", "content": "edited ok", "is_error": false}
]}
}),
],
);
w.write_session(
"betasess",
&[
user_line("b1", "2026-03-01T09:00:00Z", "how many quokkas in the census"),
assistant_line(
"b2",
"2026-03-01T09:05:00Z",
serde_json::json!([{"type": "text", "text": "seventeen quokkas"}]),
),
user_line("b3", "2026-03-02T09:00:00Z", "thanks"),
assistant_line(
"b4",
"2026-03-02T09:01:00Z",
serde_json::json!([{"type": "text", "text": "anytime"}]),
),
],
);
}
fn pos(hay: &str, needle: &str) -> usize {
hay.find(needle)
.unwrap_or_else(|| panic!("expected {needle:?} in:\n{hay}"))
}
#[test]
fn ls_sort_modes_and_filters() {
let w = World::new("ls");
standard_corpus(&w);
let (out, _) = w.cv_ok(&["ls"]);
assert!(out.contains("2 session(s)"), "{out}");
assert!(pos(&out, "alphases") < pos(&out, "betasess"), "updated order:\n{out}");
assert!(out.contains("alpha adventures"), "{out}");
let (out, _) = w.cv_ok(&["ls", "--sort-by", "created"]);
assert!(pos(&out, "betasess") < pos(&out, "alphases"), "created order:\n{out}");
let (out, _) = w.cv_ok(&["ls", "--sort-by", "messages"]);
assert!(pos(&out, "betasess") < pos(&out, "alphases"), "messages order:\n{out}");
assert!(out.contains("4 msg"), "{out}");
let (out, _) = w.cv_ok(&["ls", "--sort-by", "updated"]);
assert!(pos(&out, "alphases") < pos(&out, "betasess"), "{out}");
let (_, err) = w.cv_fails(&["ls", "--sort-by", "bogus"]);
assert!(err.contains("bogus"), "{err}");
let (out, _) = w.cv_ok(&["ls", "--limit", "1"]);
assert!(out.contains("… 1 more"), "{out}");
let (out, _) = w.cv_ok(&["ls", "--cwd", "/work/proj"]);
assert!(out.contains("2 session(s)"), "{out}");
let (out, _) = w.cv_ok(&["ls", "--cwd", "/nowhere"]);
assert!(out.contains("0 session(s) (of 2 discovered; filtered)"), "{out}");
let (_, err) = w.cv_fails(&["ls", "--harness", "clippy9000"]);
assert!(err.contains("unknown harness"), "{err}");
}
#[test]
fn search_fallback_messaging_and_index_path() {
let w = World::new("search");
standard_corpus(&w);
let (out, err) = w.cv_ok(&["search", "zebrafish"]);
assert!(err.contains("no index yet"), "want live-scan note, got stderr:\n{err}");
assert!(err.contains("scanning live"), "{err}");
assert!(out.contains("alphases"), "{out}");
assert!(out.to_lowercase().contains("zebrafish"), "snippet expected:\n{out}");
fs::write(w.cv_home.join("index.sqlite"), b"stale").unwrap();
let (_, err) = w.cv_ok(&["search", "zebrafish"]);
assert!(
err.contains("legacy sqlite index no longer used"),
"want legacy-index note, got stderr:\n{err}"
);
fs::remove_file(w.cv_home.join("index.sqlite")).unwrap();
let (out, _) = w.cv_ok(&["index"]);
assert!(out.contains("indexed 2 top-level session(s)"), "{out}");
let (out, err) = w.cv_ok(&["search", "zebrafish"]);
assert!(
!err.contains("scanning live"),
"indexed search must not live-scan:\n{err}"
);
assert!(out.contains("alphases"), "{out}");
let (out, _) = w.cv_ok(&["search", "xyzzyplugh"]);
assert!(out.contains("no matches"), "{out}");
assert!(out.contains("(index"), "{out}");
let (out, _) = w.cv_ok(&["search", "zebrafish", "--harness", "codex"]);
assert!(out.contains("no matches"), "{out}");
}
#[test]
fn show_range_windowing() {
let w = World::new("show");
standard_corpus(&w);
let (out, _) = w.cv_ok(&["show", "alphasess"]);
assert!(out.contains("# alpha adventures"), "{out}");
assert!(out.contains("zebrafish migration"), "{out}");
assert!(out.contains("[tool_use Edit]"), "{out}");
assert!(out.contains("edited ok"), "{out}");
let (out, _) = w.cv_ok(&["show", "alphasess", "--range", "1"]);
assert!(out.contains("[tool_use Edit]"), "{out}");
assert!(!out.contains("zebrafish"), "window must exclude msg 0:\n{out}");
assert!(!out.contains("edited ok"), "window must exclude msg 2:\n{out}");
let (out, _) = w.cv_ok(&["show", "alphasess", "--range", "2-"]);
assert!(out.contains("edited ok"), "{out}");
assert!(!out.contains("zebrafish"), "{out}");
let (out, _) = w.cv_ok(&["show", "alphasess", "--range", "-1"]);
assert!(out.contains("zebrafish"), "{out}");
assert!(!out.contains("[tool_use Edit]"), "{out}");
let (out, _) = w.cv_ok(&["show", "alphasess", "--range", "10-20"]);
assert!(out.contains("# alpha adventures"), "{out}");
assert!(!out.contains("──"), "no message blocks expected:\n{out}");
let (_, err) = w.cv_fails(&["show", "alphasess", "--range", "9-3"]);
assert!(err.contains("end (3) is before start (9)"), "{err}");
let (_, err) = w.cv_fails(&["show", "alphasess", "--range", "abc"]);
assert!(err.contains("bad range"), "{err}");
let (out, _) = w.cv_ok(&["show", "alphasess", "--json", "--range", "1-2"]);
let v: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
assert_eq!(v["messages"].as_array().unwrap().len(), 1, "{out}");
let (out, _) = w.cv_ok(&["show", "alphasess", "--json", "--range", "10-20"]);
let v: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
assert_eq!(v["messages"].as_array().unwrap().len(), 0, "{out}");
}
#[test]
fn export_formats() {
let w = World::new("export");
standard_corpus(&w);
let (out, _) = w.cv_ok(&["export", "alphasess"]);
assert!(out.contains("# alpha adventures"), "{out}");
assert!(out.contains("- harness: claude"), "{out}");
assert!(out.contains("- id: alphasess"), "{out}");
assert!(out.contains("## User"), "{out}");
assert!(out.contains("zebrafish migration"), "{out}");
assert!(out.contains("**🔧 Edit**"), "{out}");
let (out, _) = w.cv_ok(&["export", "alphasess", "--format", "json"]);
let v: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
assert_eq!(v["id"], "alphasess", "{out}");
assert_eq!(v["harness"], "claude", "{out}");
assert_eq!(v["messages"].as_array().unwrap().len(), 3, "{out}");
let (out, _) = w.cv_ok(&["export", "alphasess", "--format", "html"]);
assert!(out.contains("<html"), "{out}");
assert!(out.contains("zebrafish"), "{out}");
let (_, err) = w.cv_fails(&["export", "alphasess", "--format", "docx"]);
assert!(err.contains("unknown format"), "{err}");
}
#[test]
fn dataset_emits_parseable_jsonl() {
let w = World::new("dataset");
standard_corpus(&w);
let (out, err) = w.cv_ok(&["dataset"]);
let lines: Vec<&str> = out.lines().filter(|l| !l.trim().is_empty()).collect();
assert_eq!(
lines.len(),
2,
"one record per session:\nstdout:\n{out}\nstderr:\n{err}"
);
for line in &lines {
let v: serde_json::Value = serde_json::from_str(line).expect("each record is JSON");
assert!(v["messages"].is_array(), "chatml shape: {line}");
}
assert!(err.contains("wrote 2 record(s)"), "{err}");
let (out, _) = w.cv_ok(&["dataset", "--format", "sharegpt", "--redact"]);
assert!(!out.contains(SECRET), "dataset --redact leaked the secret:\n{out}");
for line in out.lines().filter(|l| !l.trim().is_empty()) {
let v: serde_json::Value = serde_json::from_str(line).expect("valid JSON");
assert!(v["conversations"].is_array(), "sharegpt shape: {line}");
}
let (_, err) = w.cv_fails(&["dataset", "--format", "parquet"]);
assert!(err.contains("unknown format"), "{err}");
}
#[test]
fn events_and_touched() {
let w = World::new("events");
standard_corpus(&w);
let (out, _) = w.cv_ok(&["events", "alphasess"]);
assert!(out.contains("event(s) in claude:alphases"), "{out}");
assert!(out.contains("file_edit"), "{out}");
assert!(out.contains("/work/proj/src/widget.rs"), "{out}");
let (out, _) = w.cv_ok(&["events", "alphasess", "--kind", "command"]);
assert!(out.contains("no \"command\" events"), "{out}");
let (out, _) = w.cv_ok(&["events", "betasess"]);
assert!(out.contains("no tool events"), "{out}");
let (out, _) = w.cv_ok(&["touched", "/work/proj/src/widget.rs"]);
assert!(out.contains("1 session(s) touched"), "{out}");
assert!(out.contains("alphases"), "{out}");
let (out, _) = w.cv_ok(&["touched", "src/widget.rs", "--edits-only"]);
assert!(out.contains("alphases"), "{out}");
assert!(out.contains("edit(s)"), "{out}");
let (out, _) = w.cv_ok(&["touched", "src/nonexistent.rs"]);
assert!(out.contains("no sessions touched"), "{out}");
let (_, err) = w.cv_fails(&["events", "bogus-id"]);
assert!(err.contains("no session matching"), "{err}");
}
#[test]
fn redact_scrubs_planted_secret() {
let w = World::new("redact");
standard_corpus(&w);
let (out, _) = w.cv_ok(&["show", "alphasess"]);
assert!(out.contains(SECRET), "fixture sanity: secret present in show:\n{out}");
let (out, err) = w.cv_ok(&["redact", "alphasess", "--stats"]);
assert!(!out.contains(SECRET), "redact leaked the secret:\n{out}");
assert!(out.contains("[REDACTED:api_key]"), "{out}");
assert!(err.contains("redacted"), "{err}");
assert!(err.contains("1 api_key"), "{err}");
let (out, _) = w.cv_ok(&["redact", "alphasess", "--format", "json"]);
assert!(!out.contains(SECRET), "redact --format json leaked the secret:\n{out}");
let v: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
assert_eq!(v["id"], "alphasess");
let (_, err) = w.cv_fails(&["redact", "alphasess", "--format", "pdf"]);
assert!(err.contains("unknown format"), "{err}");
}
#[test]
fn error_paths_exit_nonzero_with_stderr() {
let w = World::new("errs");
standard_corpus(&w);
let (_, err) = w.cv_fails(&["show", "nonexistent-id"]);
assert!(err.contains("no session matching"), "{err}");
let (_, err) = w.cv_fails(&["export", "nonexistent-id"]);
assert!(err.contains("no session matching"), "{err}");
let (_, err) = w.cv_fails(&["tree", "nonexistent-id"]);
assert!(err.contains("no session matching"), "{err}");
let (_, err) = w.cv_fails(&["resume", "nonexistent-id"]);
assert!(err.contains("no session matching"), "{err}");
let (_, err) = w.cv_fails(&["diff", "alphasess", "nonexistent-id"]);
assert!(err.contains("no session matching"), "{err}");
let (_, err) = w.cv_fails(&["show", "alphasess", "--harness", "marsrover"]);
assert!(err.contains("unknown harness"), "{err}");
let (_, err) = w.cv_fails(&["convert", "alphasess", "--to", "marsrover"]);
assert!(err.contains("unknown target harness"), "{err}");
let (_, err) = w.cv_fails(&["splice", "alphasess:zz-3"]);
assert!(err.contains("bad spec"), "{err}");
let (_, err) = w.cv_fails(&["events", ""]);
assert!(err.contains("ambiguous session id"), "{err}");
}
#[test]
fn blame_outside_repo_and_missing_file() {
let w = World::new("blame");
standard_corpus(&w);
let (_ok, code, out, err) = w.cv(&["blame", "/not/a/file"]);
assert_ne!(code, -1, "cv blame died by signal\nstderr:\n{err}");
assert!(!err.contains("panicked"), "panic in blame:\n{err}");
assert!(
out.contains("not inside a git repository") || !err.trim().is_empty(),
"blame must explain itself\nstdout:\n{out}\nstderr:\n{err}"
);
}
#[test]
fn diff_and_tree_render() {
let w = World::new("difftree");
standard_corpus(&w);
let (out, _) = w.cv_ok(&["diff", "alphasess", "betasess"]);
assert!(out.contains("only-in-A"), "{out}");
assert!(out.contains("0 shared"), "different sessions share no prefix:\n{out}");
let (out, _) = w.cv_ok(&["tree", "alphasess"]);
assert!(out.contains("# alpha adventures"), "{out}");
assert!(out.contains("3 msg"), "{out}");
}
#[test]
fn stats_and_timeline() {
let w = World::new("stats");
standard_corpus(&w);
let (out, _) = w.cv_ok(&["stats"]);
assert!(out.contains("2 session(s) · 7 message(s)"), "{out}");
assert!(out.contains("claude"), "{out}");
let (out, _) = w.cv_ok(&["timeline"]);
assert!(out.contains("2 session(s)"), "{out}");
assert!(pos(&out, "betasess") < pos(&out, "alphases"), "{out}");
}
#[test]
fn id_prefix_resolution() {
let w = World::new("prefix");
standard_corpus(&w);
let (out, _) = w.cv_ok(&["show", "alpha"]);
assert!(out.contains("# alpha adventures"), "{out}");
let (out, _) = w.cv_ok(&["show", "beta", "--harness", "claude"]);
assert!(out.contains("quokkas"), "{out}");
}