use claude::user as user_line;
use cv_testkit::{claude, World};
use std::path::PathBuf;
use std::process::Command;
use std::{fs, str};
const SECRET: &str = "sk-abcDEF1234567890ghijkl";
fn plant_legacy_tag_decision(w: &World, id: &str, title: &str, who: &str) {
let ts = chrono_at(3);
w.append_task_log(&[
serde_json::json!({"id": id, "task_id": id, "ts": ts, "by": "cv", "event": "opened",
"title": title, "channel": "tasks", "assignee": who}),
serde_json::json!({"id": format!("{}f", &id[..id.len() - 1]), "task_id": id, "ts": ts, "by": "cv",
"event": "tagged", "tags": ["decision"]}),
]);
}
fn assistant_line(uuid: &str, ts: &str, blocks: serde_json::Value) -> serde_json::Value {
claude::with(
claude::assistant(uuid, ts, "claude-test-1", blocks),
serde_json::json!({"cwd": "/work/proj"}),
)
}
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 ls_json_emits_the_same_rows_machine_readably() {
let w = World::new("lsjson");
standard_corpus(&w);
let (out, _) = w.cv_ok(&["ls", "--json"]);
let rows: Vec<serde_json::Value> = serde_json::from_str(out.trim()).expect("stdout must be pure JSON");
assert_eq!(rows.len(), 2, "{out}");
assert_eq!(rows[0]["id"], "alphasess", "updated order:\n{out}");
assert_eq!(rows[0]["harness"], "claude");
assert_eq!(rows[0]["title"], "alpha adventures");
assert_eq!(rows[0]["message_count"], 3);
assert_eq!(rows[0]["cwd"], "/work/proj");
assert!(
rows[0]["created_at"].as_str().unwrap().starts_with("2026-01-01"),
"{out}"
);
assert!(
rows[0]["updated_at"].as_str().unwrap().starts_with("2026-06-01"),
"{out}"
);
assert!(rows[0]["path"].as_str().unwrap().ends_with("alphasess.jsonl"), "{out}");
assert!(rows[1]["title"].is_null(), "{out}");
let real_size = std::fs::metadata(rows[0]["path"].as_str().unwrap()).unwrap().len();
assert_eq!(rows[0]["size_bytes"].as_u64().unwrap(), real_size, "{out}");
assert!(rows[0]["size_bytes"].as_u64().unwrap() > 0, "{out}");
assert!(rows[0].get("git").is_none(), "no --enrich ⇒ no git:\n{out}");
assert!(
rows[0].get("display_title").is_none(),
"no --enrich ⇒ no display_title:\n{out}"
);
let (out, _) = w.cv_ok(&["ls", "--json", "--limit", "1"]);
let rows: Vec<serde_json::Value> = serde_json::from_str(out.trim()).unwrap();
assert_eq!(rows.len(), 1, "{out}");
let (out, _) = w.cv_ok(&["ls", "--json", "--harness", "codex"]);
let rows: Vec<serde_json::Value> = serde_json::from_str(out.trim()).unwrap();
assert!(rows.is_empty(), "{out}");
}
#[test]
fn ls_json_enrich_adds_git_branch_and_synthesized_title() {
let w = World::new("lsjsonenrich");
w.write_session(
"enrichsess",
&[
serde_json::json!({
"type": "user", "uuid": "e1", "sessionId": "s",
"timestamp": "2026-05-01T10:00:00Z", "cwd": "/work/proj", "gitBranch": "feature/enrich",
"message": {"role": "user", "content":
"<system-reminder>be nice</system-reminder>\n\nteach me about capybaras please"}
}),
assistant_line(
"e2",
"2026-05-01T10:01:00Z",
serde_json::json!([{"type": "text", "text": "capybaras are the largest rodents"}]),
),
],
);
let (out, _) = w.cv_ok(&["ls", "--json", "--enrich"]);
let rows: Vec<serde_json::Value> = serde_json::from_str(out.trim()).expect("pure JSON");
assert_eq!(rows.len(), 1, "{out}");
assert!(rows[0]["title"].is_null(), "{out}");
assert_eq!(rows[0]["display_title"], "teach me about capybaras please", "{out}");
assert_eq!(rows[0]["git"]["branch"], "feature/enrich", "{out}");
assert!(rows[0]["size_bytes"].as_u64().unwrap() > 0, "{out}");
}
#[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 search_json_emits_machine_readable_hits() {
let w = World::new("searchjson");
standard_corpus(&w);
let (out, err) = w.cv_ok(&["search", "zebrafish", "--json"]);
assert!(err.contains("scanning live"), "{err}");
let rows: Vec<serde_json::Value> = serde_json::from_str(out.trim()).expect("stdout must be pure JSON");
assert_eq!(rows.len(), 1, "{out}");
assert_eq!(rows[0]["id"], "alphasess", "full id, not the 8-char prefix:\n{out}");
assert_eq!(rows[0]["harness"], "claude");
assert_eq!(rows[0]["title"], "alpha adventures");
assert!(rows[0]["snippet"].as_str().unwrap().contains("zebrafish"), "{out}");
assert!(rows[0]["score"].is_null(), "{out}");
for key in ["agent_id", "parent_id", "workflow"] {
let obj = rows[0].as_object().unwrap();
assert!(obj.contains_key(key), "provenance key {key} must be present:\n{out}");
assert!(obj[key].is_null(), "top-level hit must have null {key}:\n{out}");
}
assert!(
rows[0]["updated_at"].as_str().unwrap().starts_with("2026-06-01"),
"{out}"
);
let (out, _) = w.cv_ok(&["search", "xyzzyplugh", "--json"]);
let rows: Vec<serde_json::Value> = serde_json::from_str(out.trim()).unwrap();
assert!(rows.is_empty(), "{out}");
let (out, err) = w.cv_ok(&["search", "the", "--json", "--limit", "1"]);
let rows: Vec<serde_json::Value> = serde_json::from_str(out.trim()).unwrap();
assert_eq!(rows.len(), 1, "{out}");
assert!(err.contains("stopped at 1"), "{err}");
w.cv_ok(&["index"]);
let (out, err) = w.cv_ok(&["search", "zebrafish", "--json"]);
assert!(!err.contains("scanning live"), "{err}");
let rows: Vec<serde_json::Value> = serde_json::from_str(out.trim()).expect("stdout must be pure JSON");
assert_eq!(rows.len(), 1, "{out}");
assert_eq!(rows[0]["id"], "alphasess", "{out}");
assert!(rows[0]["score"].as_f64().unwrap() > 0.0, "{out}");
let obj = rows[0].as_object().unwrap();
for key in ["agent_id", "parent_id", "workflow"] {
assert!(
obj.contains_key(key) && obj[key].is_null(),
"{key} present+null:\n{out}"
);
}
assert!(
rows[0]["updated_at"].as_str().unwrap().starts_with("2026-06-01"),
"{out}"
);
let (out, _) = w.cv_ok(&["search", "xyzzyplugh", "--json"]);
let rows: Vec<serde_json::Value> = serde_json::from_str(out.trim()).unwrap();
assert!(rows.is_empty(), "{out}");
let (out, _) = w.cv_ok(&["search", "zebrafish", "--json", "--harness", "codex"]);
let rows: Vec<serde_json::Value> = serde_json::from_str(out.trim()).unwrap();
assert!(rows.is_empty(), "{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 t1]"), "{out}");
assert!(out.contains("edited ok"), "{out}");
let (out, _) = w.cv_ok(&["show", "alphasess", "--range", "1..2"]);
assert!(out.contains("[tool_use Edit t1]"), "{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 t1]"), "{out}");
let (out, _) = w.cv_ok(&["show", "alphasess", "--first", "1"]);
assert!(out.contains("zebrafish"), "{out}");
assert!(!out.contains("[tool_use Edit t1]"), "{out}");
let (out, _) = w.cv_ok(&["show", "alphasess", "--first", "2"]);
assert!(out.contains("zebrafish") && out.contains("[tool_use Edit t1]"), "{out}");
assert!(!out.contains("edited ok"), "{out}");
let (out, _) = w.cv_ok(&["show", "alphasess", "--last", "1"]);
assert!(out.contains("edited ok"), "{out}");
assert!(!out.contains("zebrafish"), "{out}");
let (out, _) = w.cv_ok(&["show", "alphasess", "--last", "99"]);
assert!(out.contains("zebrafish") && out.contains("edited ok"), "{out}");
let (out, _) = w.cv_ok(&["show", "alphasess", "--around", "1", "--context", "0"]);
assert!(out.contains("[tool_use Edit t1]"), "{out}");
assert!(!out.contains("zebrafish"), "{out}");
assert!(!out.contains("edited ok"), "{out}");
let (out, _) = w.cv_ok(&["show", "alphasess", "--around", "1", "--context", "1"]);
assert!(out.contains("zebrafish") && out.contains("edited ok"), "{out}");
let (_, err) = w.cv_fails(&["show", "alphasess", "--context", "2"]);
assert!(err.contains("--around"), "{err}");
let (out, _) = w.cv_ok(&["show", "alphasess", "--max-bytes", "1"]);
assert!(
out.contains("continue with --range"),
"continuation line expected:\n{out}"
);
assert!(!out.contains("edited ok"), "budget must cut the render short:\n{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 (_, err) = w.cv_fails(&["show", "alphasess", "--range", "1-2"]);
assert!(
err.contains("A..B") && err.contains("--first N") && err.contains("--last N"),
"`A-B` must point at the new grammar:\n{err}"
);
let (_, err) = w.cv_fails(&["show", "alphasess", "--range", "-1"]);
assert!(
err.contains("--first N") && err.contains("--last N"),
"`-N` must point at the flags:\n{err}"
);
let (_, err) = w.cv_fails(&["show", "alphasess", "--range", "2-"]);
assert!(err.contains("A.."), "`N-` must point at `A..`:\n{err}");
let (_, err) = w.cv_fails(&["show", "alphasess", "--range", "1"]);
assert!(
err.contains("--around 1 --context 0") && err.contains("1..2"),
"a bare index must point at both spellings of one message:\n{err}"
);
for pair in [
["--first", "--last"],
["--first", "--range"],
["--last", "--range"],
["--first", "--around"],
["--last", "--around"],
["--range", "--around"],
] {
let a = if pair[0] == "--range" { "0..1" } else { "1" };
let b = if pair[1] == "--range" { "0..1" } else { "1" };
let args = ["show", "alphasess", pair[0], a, pair[1], b];
let (ok, code, out, err) = w.cv(&args);
assert!(!ok, "two selectors must be refused: cv {args:?}\n{out}");
assert_eq!(code, 2, "clap usage errors exit 2\nstderr:\n{err}");
assert!(
err.contains("cannot be used with"),
"conflict must be named: cv {args:?}\n{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", "--first", "2"]);
let v: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
assert_eq!(v["messages"].as_array().unwrap().len(), 2, "{out}");
let (out, _) = w.cv_ok(&["show", "alphasess", "--json", "--last", "1"]);
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 thinking_mode_is_one_word_on_every_emitting_command() {
let w = World::new("think");
standard_corpus(&w);
for cmd in ["port", "splice", "pack"] {
let (ok, _, out, err) = w.cv(&[cmd, "--help"]);
assert!(ok, "cv {cmd} --help failed:\n{err}");
for mode in ["native", "text", "drop"] {
assert!(
out.contains(mode),
"cv {cmd} --help must offer --thinking {mode}:\n{out}"
);
}
}
let (_, _, _, err) = w.cv(&["port", "alphasess", "--thinking", "sideways"]);
assert!(err.contains("sideways"), "the bad value is quoted back:\n{err}");
let (_, code, _, err) = w.cv(&["prune", "alphasess", "--thinking"]);
assert_eq!(code, 2, "{err}");
assert!(err.contains("--drop-thinking"), "{err}");
}
#[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["open_session"], "0.3", "{out}");
assert!(
out.trim_start().starts_with("{\n \"open_session\""),
"the marker leads the document:\n{out}"
);
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(&["show", "alphasess", "--json"]);
let v: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
assert!(v.get("open_session").is_none(), "{out}");
assert_eq!(v["id"], "alphasess", "{out}");
let (ok, code, _, err) = w.cv(&["export", "alphasess", "--format", "html"]);
assert!(!ok && code == 2, "{err}");
assert!(err.contains("cv share <id>"), "{err}");
let (_, err) = w.cv_fails(&["export", "alphasess", "--format", "docx"]);
assert!(err.contains("unknown format"), "{err}");
}
#[test]
fn prune_json_emits_machine_readable_report() {
let w = World::new("prunejson");
let big = "x".repeat(4096);
w.write_session(
"prunesrc",
&[
serde_json::json!({
"type": "user", "uuid": "u1", "sessionId": "prunesrc", "timestamp": "2026-01-01T10:00:00Z",
"cwd": "/work/proj",
"message": {"role": "user", "content": "read the big file"}
}),
serde_json::json!({
"type": "assistant", "uuid": "a1", "sessionId": "prunesrc", "timestamp": "2026-01-01T10:01:00Z",
"cwd": "/work/proj",
"message": {"role": "assistant", "model": "claude-test-1", "content": [
{"type": "text", "text": "reading"},
{"type": "tool_use", "id": "t1", "name": "Read", "input": {"file_path": "/big.txt"}}
]}
}),
serde_json::json!({
"type": "user", "uuid": "u2", "sessionId": "prunesrc", "timestamp": "2026-01-01T10:02:00Z",
"message": {"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "t1", "content": big, "is_error": false}
]}
}),
serde_json::json!({
"type": "assistant", "uuid": "a2", "sessionId": "prunesrc", "timestamp": "2026-01-01T10:03:00Z",
"cwd": "/work/proj",
"message": {"role": "assistant", "model": "claude-test-1", "content": [
{"type": "text", "text": "done"}
]}
}),
],
);
let dir = w.home.join(".claude/projects/-work-proj");
let source_before = fs::read_to_string(dir.join("prunesrc.jsonl")).unwrap();
let (out, _) = w.cv_ok(&[
"prune",
"prunesrc",
"--min-size",
"100",
"--keep-last",
"0",
"--dry-run",
"--json",
]);
let v: serde_json::Value = serde_json::from_str(out.trim()).expect("stdout must be pure JSON");
assert_eq!(v["source_id"], "prunesrc", "{out}");
assert_eq!(v["dry_run"], true, "{out}");
assert!(v["new_id"].is_null(), "unpinned dry-run id must be null:\n{out}");
assert!(v["new_path"].is_null(), "{out}");
assert!(v["sidecar_path"].is_null(), "{out}");
assert!(v["note"].as_str().unwrap().contains("dry run"), "{out}");
assert_eq!(v["snipped_payloads"], 1, "{out}");
assert!(
v["before_bytes"].as_u64().unwrap() > v["after_bytes"].as_u64().unwrap(),
"{out}"
);
assert_eq!(fs::read_dir(&dir).unwrap().count(), 1, "dry run must write nothing");
let (out, _) = w.cv_ok(&[
"prune",
"prunesrc",
"--min-size",
"100",
"--keep-last",
"0",
"--dry-run",
"--to",
"pinnedsess",
"--json",
]);
let v: serde_json::Value = serde_json::from_str(out.trim()).unwrap();
assert_eq!(v["new_id"], "pinnedsess", "{out}");
assert!(v["new_path"].is_null(), "still nothing written:\n{out}");
let (out, _) = w.cv_ok(&[
"prune",
"prunesrc",
"--min-size",
"100",
"--keep-last",
"0",
"--to",
"prunedsess",
"--json",
]);
let v: serde_json::Value = serde_json::from_str(out.trim()).expect("stdout must be pure JSON");
assert_eq!(v["source_id"], "prunesrc", "{out}");
assert_eq!(v["new_id"], "prunedsess", "{out}");
assert_eq!(v["harness"], "claude", "{out}");
assert_eq!(v["dry_run"], false, "{out}");
assert!(v["note"].is_null(), "{out}");
assert_eq!(v["snipped_payloads"], 1, "{out}");
assert!(v["tokens_freed"].as_u64().unwrap() > 0, "{out}");
let new_path = PathBuf::from(v["new_path"].as_str().unwrap());
assert!(new_path.ends_with("prunedsess.jsonl"), "{out}");
assert!(new_path.exists(), "pruned session must exist at new_path");
let sidecar = PathBuf::from(v["sidecar_path"].as_str().unwrap());
assert!(sidecar.ends_with("prunedsess.flat.jsonl"), "{out}");
assert!(sidecar.exists(), "sidecar must exist at sidecar_path");
assert_eq!(
fs::read_to_string(dir.join("prunesrc.jsonl")).unwrap(),
source_before,
"prune must never modify the source session"
);
let (out, err) = w.cv_ok(&[
"prune",
"prunesrc",
"--min-size",
"100",
"--keep-last",
"0",
"--dry-run",
]);
assert!(out.trim().is_empty(), "non-json prune must keep stdout empty:\n{out}");
assert!(err.contains("pruned"), "{err}");
}
fn keeper_corpus(w: &World) -> PathBuf {
let sid = "aaaaaaaa-0000-4000-8000-000000000001";
let line = |ty: &str, uuid: &str, parent: Option<&str>, message: serde_json::Value| {
serde_json::json!({"type": ty, "sessionId": sid, "uuid": uuid, "parentUuid": parent,
"isSidechain": false, "cwd": "/work/proj", "timestamp": "2026-06-01T10:00:00Z", "message": message})
};
let boundary = |uuid: &str| {
serde_json::json!({"type": "system", "subtype": "compact_boundary", "sessionId": sid, "uuid": uuid,
"parentUuid": null, "isSidechain": false, "compactMetadata": {"trigger": "auto"}})
};
let path = w.write_session(
sid,
&[
line("user", "u0", None, serde_json::json!({"role": "user", "content": "fix the gate"})),
boundary("b1"),
line("assistant", "a1", Some("b1"), serde_json::json!({"role": "assistant", "content": [
{"type": "tool_use", "id": "toolu_c", "name": "Bash",
"input": {"command": "git commit -am 'gate: name the project'"}}]})),
line("user", "r1", Some("a1"), serde_json::json!({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "toolu_c", "content": "[main 63cef47] gate: name the project"}]})),
line("assistant", "a2", Some("r1"), serde_json::json!({"role": "assistant", "content": [
{"type": "text", "text": "landed"}]})),
boundary("b2"),
line("user", "u3", Some("b2"), serde_json::json!({"role": "user", "content": "now something else"})),
],
);
let agent = w
.home
.join(".claude/projects/-work-proj")
.join(sid)
.join("subagents/agent-a7f5742c50c0b9654.jsonl");
fs::create_dir_all(agent.parent().unwrap()).unwrap();
let side = |uuid: &str, parent: Option<&str>, ty: &str, message: serde_json::Value| {
serde_json::json!({"type": ty, "sessionId": sid, "uuid": uuid, "parentUuid": parent,
"isSidechain": true, "agentId": "a7f5742c50c0b9654", "cwd": "/work/proj",
"timestamp": "2026-06-01T11:00:00Z", "message": message})
};
let lines = [
side(
"s0",
None,
"user",
serde_json::json!({"role": "user", "content": "review lane"}),
),
side(
"s1",
Some("s0"),
"assistant",
serde_json::json!({"role": "assistant", "content": [
{"type": "tool_use", "id": "toolu_s", "name": "Bash",
"input": {"command": "git commit -qam fix && git rev-parse HEAD"}}]}),
),
side(
"s2",
Some("s1"),
"user",
serde_json::json!({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "toolu_s", "content": "9f00ba5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e"}]}),
),
side(
"s3",
Some("s2"),
"assistant",
serde_json::json!({"role": "assistant", "content": [
{"type": "text", "text": "done"}]}),
),
];
let body: String = lines.iter().map(|l| format!("{l}\n")).collect();
fs::write(&agent, body).unwrap();
path
}
fn jsonl(path: &std::path::Path) -> Vec<serde_json::Value> {
fs::read_to_string(path)
.unwrap()
.lines()
.map(|l| serde_json::from_str(l).unwrap())
.collect()
}
#[test]
fn rewind_derives_the_agent_as_of_a_commit() {
let w = World::new("rewind");
let src = keeper_corpus(&w);
let before = fs::read(&src).unwrap();
let out = w.base.join("rw");
let out_s = out.to_str().unwrap();
let (stdout, err) = w.cv_ok(&[
"rewind",
"aaaaaaaa",
"--at",
"63cef473b771e07a",
"--out",
out_s,
"--json",
]);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("stdout must be pure JSON");
assert_eq!(v["source_id"], "aaaaaaaa-0000-4000-8000-000000000001", "{stdout}");
assert_eq!(
(v["start_line"].as_u64(), v["cut_line"].as_u64()),
(Some(2), Some(4)),
"{stdout}"
);
assert_eq!(v["evidence"]["kind"], "created", "{stdout}");
assert_eq!(v["omitted_lines"], 3, "{stdout}");
let new_id = v["new_id"].as_str().unwrap().to_string();
let new_path = PathBuf::from(v["new_path"].as_str().unwrap());
assert_eq!(new_path, out.join(format!("{new_id}.jsonl")));
let got = jsonl(&new_path);
assert_eq!(got.len(), 3);
assert!(got.iter().all(|r| r["sessionId"] == new_id.as_str()), "{got:?}");
assert!(PathBuf::from(v["provenance_path"].as_str().unwrap()).exists());
assert_eq!(
v["resume"],
serde_json::json!(["cd /work/proj", format!("claude --resume {new_id}")])
);
assert!(err.contains("exact: commit created here"), "{err}");
assert!(
err.contains("copy it there first"),
"an --out elsewhere must say where claude looks:\n{err}"
);
assert_eq!(fs::read(&src).unwrap(), before, "rewind must never modify the source");
let (stdout, _) = w.cv_ok(&["rewind", "aaaaaaaa", "--at", "1", "--to", "rw-by-idx", "--json"]);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(v["cut_msg_idx"], 1, "{stdout}");
assert!(w.home.join(".claude/projects/-work-proj/rw-by-idx.jsonl").exists());
assert!(w
.home
.join(".claude/projects/-work-proj/rw-by-idx.rewind.json")
.exists());
let (stdout, _) = w.cv_ok(&["rewind", "aaaaaaaa", "--full", "--dry-run", "--json"]);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(
(v["dry_run"].as_bool(), v["start_line"].as_u64()),
(Some(true), Some(1)),
"{stdout}"
);
assert!(v["new_id"].is_null() && v["new_path"].is_null(), "{stdout}");
let (_, err) = w.cv_fails(&["rewind", "aaaaaaaa", "--at", "deadbeef1"]);
assert!(err.contains("deadbeef1") && err.contains("git commit"), "{err}");
let (ok, code, _, err) = w.cv(&["rewind", "aaaaaaaa", "--at", "nope"]);
assert!(!ok && code == 2, "{err}");
let (ok, code, _, err) = w.cv(&["rewind", "aaaaaaaa", "--at", "99"]);
assert!(!ok && code == 1 && err.contains("message(s)"), "{err}");
}
#[test]
fn rewind_extracts_a_subagent_as_a_standalone_session() {
let w = World::new("rewindsub");
keeper_corpus(&w);
let proj = w.home.join(".claude/projects/-work-proj");
let (stdout, err) = w.cv_ok(&[
"rewind",
"agent-a7f5742c",
"--at",
"9f00ba5",
"--to",
"kept-sub",
"--json",
]);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap_or_else(|e| panic!("{e}: {stdout}\n{err}"));
assert_eq!(v["subagent"], true, "{stdout}");
assert_eq!(
v["source_session_id"], "aaaaaaaa-0000-4000-8000-000000000001",
"{stdout}"
);
assert_eq!(v["cut_line"], 3, "{stdout}");
let got = jsonl(&proj.join("kept-sub.jsonl"));
assert_eq!(got.len(), 3);
assert!(got
.iter()
.all(|r| r["isSidechain"] == false && r.get("agentId").is_none() && r["sessionId"] == "kept-sub"));
assert!(got[0]["parentUuid"].is_null(), "the root chain starts at a null parent");
assert!(err.contains("sub-agent of aaaaaaaa"), "{err}");
let (stdout, _) = w.cv_ok(&["rewind", "aaaaaaaa", "--agent", "a7f57", "--dry-run", "--json"]);
assert!(stdout.contains("\"subagent\": true"), "{stdout}");
let path = proj.join("aaaaaaaa-0000-4000-8000-000000000001/subagents/agent-a7f5742c50c0b9654.jsonl");
let (stdout, _) = w.cv_ok(&["rewind", path.to_str().unwrap(), "--dry-run", "--json"]);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(
(v["subagent"].as_bool(), v["lines_written"].as_u64()),
(Some(true), Some(4)),
"{stdout}"
);
let (out, _) = w.cv_ok(&["show", "kept-sub"]);
assert!(out.contains("review lane"), "{out}");
}
#[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 export_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(&["export", "alphasess", "--redact"]);
assert!(!out.contains(SECRET), "export --redact leaked the secret:\n{out}");
assert!(out.contains("[REDACTED:api_key]"), "{out}");
assert!(out.contains("## user"), "still the export rendering: {out}");
assert!(err.contains("redacted") && err.contains("1 api_key"), "{err}");
let (out, err) = w.cv_ok(&["export", "alphasess", "--format", "json", "--redact"]);
assert!(
!out.contains(SECRET),
"export --format json --redact leaked the secret:\n{out}"
);
let v: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
assert_eq!(v["open_session"], "0.3", "{out}");
assert_eq!(v["id"], "alphasess");
assert!(err.contains("1 api_key"), "{err}");
let (out, err) = w.cv_ok(&["export", "alphasess"]);
assert!(out.contains(SECRET), "{out}");
assert!(!err.contains("redacted"), "{err}");
let (ok, code, _, err) = w.cv(&["redact", "alphasess", "--stats"]);
assert!(!ok && code == 2, "{err}");
assert!(err.contains("cv export <id> --redact"), "{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 (and no sub-agent) matching"), "{err}");
let (_, err) = w.cv_fails(&["export", "nonexistent-id"]);
assert!(err.contains("no session matching"), "{err}");
let (_, err) = w.cv_fails(&["show", "nonexistent-id", "--tree"]);
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(&["show", "alphasess", "--harness", "marsrover"]);
assert!(err.contains("unknown harness"), "{err}");
let removed: &[(&[&str], &str)] = &[
(&["timeline", "--limit", "5"], "cv ls --sort-by updated --asc"),
(&["redact", "alphasess", "--format", "json"], "cv export <id> --redact"),
(&["diff", "alphasess", "betasess"], "cv export <id> --format md"),
(&["export", "alphasess", "--format", "html"], "cv share <id>"),
(&["adopt", "--list", "alphasess"], "cv lanes alphasess"),
(&["adopt", "--orphans"], "cv lanes --recent <N> --unfinished"),
(
&["formats", "check"],
"cargo test -p clustervision-core --test formats_manifest",
),
(&["formats", "census"], "cv formats"),
(
&["fork", "alphasess", "--at", "2", "--session"],
"cv splice <id>:..N --session",
),
(
&["loom", "alphasess", "--at", "2", "--graft", "betasess", "--from", "1"],
"cv splice <base>:..N <other>:M..",
),
(&["tree", "alphasess"], "cv show <id> --tree"),
(&["prompts", "alphasess", "--json"], "cv show <id> --kind prompt,answer"),
(
&["compaction", "alphasess", "--summaries"],
"cv show <id> --kind compaction_boundary,compaction_summary",
),
];
for (args, pointer) in removed {
let (ok, code, out, err) = w.cv(args);
assert!(!ok, "cv {args:?} must not still work\nstdout:\n{out}");
assert_eq!(code, 2, "cv {args:?} must exit 2 (caller mistake)\nstderr:\n{err}");
assert!(!err.contains("panicked"), "cv {args:?} panicked:\n{err}");
assert!(
err.contains("0.15.0") || err.contains("is now"),
"cv {args:?} must say when it went away:\n{err}"
);
assert!(err.contains(pointer), "cv {args:?} must point at {pointer:?}:\n{err}");
}
let (ok, code, _, err) = w.cv(&["convert", "alphasess"]);
assert!(!ok && code == 2 && err.contains("unrecognized subcommand"), "{err}");
let (ok, code, _, err) = w.cv(&["prune", "alphasess", "--retrieve", "t1"]);
assert!(!ok && code == 2 && err.contains("unexpected argument"), "{err}");
let (_, err) = w.cv_fails(&["port", "alphasess", "--harness", "marsrover"]);
assert!(err.contains("unknown"), "{err}");
let (_, err) = w.cv_fails(&["splice", "alphasess:1-2"]);
assert!(err.contains("bad spec") && err.contains("<id>:A..B"), "{err}");
let (_, err) = w.cv_fails(&["splice", "alphasess:zz..3"]);
assert!(err.contains("bad spec"), "{err}");
let (ok, code, out, err) = w.cv(&["events", ""]);
assert!(!ok, "an ambiguous id must not be silently resolved:\n{out}");
assert_eq!(code, 2, "{err}");
assert!(err.contains("ambiguous id"), "{err}");
assert!(err.contains("2 candidates"), "{err}");
assert!(
err.contains("claude:alphasess") && err.contains("claude:betasess"),
"candidates must be listed as harness:full-id:\n{err}"
);
w.cv_ok(&["events", "claude:alphasess"]);
}
#[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 show_tree_renders() {
let w = World::new("tree");
standard_corpus(&w);
let (out, _) = w.cv_ok(&["show", "alphasess", "--tree"]);
assert!(out.contains("# alpha adventures"), "{out}");
assert!(out.contains("3 msg"), "{out}");
}
#[test]
fn stats_and_ls_asc() {
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(&["ls"]);
assert!(pos(&out, "alphases") < pos(&out, "betasess"), "{out}");
let (out, _) = w.cv_ok(&["ls", "--sort-by", "updated", "--asc"]);
assert!(out.contains("2 session(s)"), "{out}");
assert!(pos(&out, "betasess") < pos(&out, "alphases"), "{out}");
let (out, _) = w.cv_ok(&["ls", "--asc", "--limit", "1", "--json"]);
let rows: Vec<serde_json::Value> = serde_json::from_str(&out).unwrap();
assert_eq!(rows.len(), 1, "{out}");
assert_eq!(rows[0]["id"], "alphasess", "{out}");
let (ok, code, _, err) = w.cv(&["timeline"]);
assert!(!ok && code == 2, "{err}");
assert!(err.contains("cv ls --sort-by updated --asc"), "{err}");
}
#[test]
fn stats_tokens_totals_usage_per_model() {
let w = World::new("stats-tokens");
let reply = |uuid: &str, id: &str, model: &str| {
serde_json::json!({
"type": "assistant", "uuid": uuid, "sessionId": "s", "timestamp": "2026-03-01T09:05:00Z",
"message": {"id": id, "role": "assistant", "model": model,
"content": [{"type": "text", "text": "ok"}],
"usage": {"input_tokens": 5, "cache_read_input_tokens": 2_000_000,
"cache_creation_input_tokens": 1_000, "output_tokens": 300}}
})
};
w.write_session(
"toksess",
&[
user_line("u1", "2026-03-01T09:00:00Z", "go"),
reply("a1", "msg_1", "claude-opus-5-5"),
reply("a2", "msg_1", "claude-opus-5-5"),
reply("a3", "msg_2", "claude-fable-5-1"),
],
);
let (out, _) = w.cv_ok(&["stats", "--tokens"]);
assert!(out.contains("1 repeated usage record(s) skipped"), "{out}");
assert!(out.contains("claude-opus-5-5"), "{out}");
assert!(out.contains("claude-fable-5-1"), "{out}");
assert!(out.contains("4.0M"), "{out}");
let (json, _) = w.cv_ok(&["stats", "--tokens", "--json"]);
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["tokens"]["total"]["calls"], 2);
assert_eq!(v["tokens"]["total"]["total"], 4_002_610);
assert_eq!(v["tokens"]["total"]["uncached"], 2_610);
assert_eq!(v["tokens"]["duplicates"], 1);
let (json, _) = w.cv_ok(&["stats", "--json"]);
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(
v.get("tokens").is_none(),
"without --tokens the payload stays catalog-only"
);
}
#[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}");
}
#[test]
fn extra_claude_roots_are_listed_and_read() {
let w = World::new("roots");
standard_corpus(&w);
let seat_proj = w.home.join("seats/bonsai/claude/projects/-work-proj");
fs::create_dir_all(&seat_proj).unwrap();
let seat_line = user_line("s1", "2026-07-01T10:00:00Z", "the seat asks about narwhals");
fs::write(seat_proj.join("seatsess.jsonl"), format!("{seat_line}\n")).unwrap();
let env_proj = w.base.join("elsewhere/projects/-work-proj");
fs::create_dir_all(&env_proj).unwrap();
let env_line = user_line("e1", "2026-07-02T10:00:00Z", "the env root asks about axolotls");
fs::write(env_proj.join("envsess.jsonl"), format!("{env_line}\n")).unwrap();
w.cv_fails(&["show", "seatsess"]);
fs::write(w.cv_home.join("claude-roots"), "# agent seats\n~/seats/*/claude\n").unwrap();
let (out, _) = w.cv_ok(&["show", "seatsess"]);
assert!(out.contains("narwhals"), "{out}");
let (out, _) = w.cv_ok(&["show", "alphasess"]);
assert!(out.contains("zebrafish"), "the default root still works:\n{out}");
let (out, _) = w.cv_ok(&["ls"]);
assert!(out.contains("3 session(s)"), "{out}");
let (out, _) = w.cv_ok(&["search", "narwhals"]);
assert!(out.contains("seatsess"), "{out}");
let roots = w.base.join("elsewhere");
let (ok, _, out, err) = w.cv_env(
&["show", "envsess"],
&[("CLUSTERVISION_CLAUDE_ROOTS", roots.to_str().unwrap())],
);
assert!(ok, "stdout:\n{out}\nstderr:\n{err}");
assert!(out.contains("axolotls"), "{out}");
}
fn opened_task_id(stdout: &str) -> String {
stdout
.lines()
.map(str::trim)
.find(|l| l.len() == 36 && l.chars().filter(|c| *c == '-').count() == 4)
.unwrap_or_else(|| panic!("no task id in open output:\n{stdout}"))
.to_string()
}
#[test]
fn task_identity_resolution_order() {
let w = World::new("task-ident");
let (out, _) = w.cv_ok(&["task", "open", "first task"]);
let id = opened_task_id(&out);
let (json, _) = w.cv_ok(&["task", "show", &id, "--json"]);
assert!(json.contains(r#""opened_by": "cv""#), "{json}");
let (ok, _, _, err) = w.cv_env(&["task", "claim", &id], &[]);
assert!(!ok, "claim without identity must fail");
assert!(
err.contains("set CV_ENDPOINT or pass --from"),
"error names the cure:\n{err}"
);
let (ok, code, out, err) = w.cv_env(&["task", "claim", &id], &[("CV_ENDPOINT", "agent:demo")]);
assert!(ok, "claim with CV_ENDPOINT: exit {code}\n{out}\n{err}");
let (json, _) = w.cv_ok(&["task", "show", &id, "--json"]);
assert!(json.contains(r#""assignee": "agent:demo""#), "{json}");
let (ok, ..) = w.cv_env(
&["task", "release", &id, "--from", "agent:explicit"],
&[("CV_ENDPOINT", "agent:demo")],
);
assert!(ok);
let (json, _) = w.cv_ok(&["task", "show", &id, "--json", "--events"]);
assert!(
json.contains(r#""by": "agent:explicit""#),
"explicit --from wins:\n{json}"
);
let (ok, _, _, err) = w.cv_env(&["task", "claim", &id], &[("CV_ENDPOINT", " ")]);
assert!(!ok && err.contains("CV_ENDPOINT"), "{err}");
}
#[test]
fn task_surfaces_sanitize_and_age() {
let w = World::new("task-ansi");
let bomb = "evil\u{1b}]0;pwned\u{7}title \u{1b}[31mred\nline";
let (out, _) = w.cv_ok(&["task", "open", bomb]);
let id = opened_task_id(&out);
let (out, _) = w.cv_ok(&["task", "list"]);
assert!(!out.contains('\u{1b}'), "list leaks ESC:\n{out:?}");
assert!(out.contains("eviltitle red line"), "stripped title renders:\n{out}");
assert!(out.contains("0s") || out.contains("1s"), "age column present:\n{out}");
let (out, _) = w.cv_ok(&["task", "show", &id]);
assert!(!out.contains('\u{1b}'), "show leaks ESC:\n{out:?}");
let (ok, ..) = w.cv_env(&["task", "claim", &id], &[("CV_ENDPOINT", "agent:demo")]);
assert!(ok);
let (out, _) = w.cv_ok(&["task", "inbox", "agent:demo"]);
assert!(!out.contains('\u{1b}'), "inbox leaks ESC:\n{out:?}");
assert!(out.contains("claimed work (1):"), "inbox groups by reason:\n{out}");
}
#[test]
fn task_land_facet_verbs_and_flags_are_removed_with_pointers() {
let w = World::new("task-land-removed");
let (out, _) = w.cv_ok(&["task", "open", "finish it"]);
let id = opened_task_id(&out);
let removed: &[&[&str]] = &[
&["task", "propose", &id, "--branch", "x"],
&["task", "reroute", &id, "--to", "y"],
&["task", "pass", &id],
&["task", "refute", &id],
&["task", "verify", "--all"],
&["task", "debt"],
&["task", "stats"],
&["task", "claim", &id, "--from", "a", "--token", "t"],
&["task", "done", &id, "--check-cmd", "true"],
&["task", "split", &id, "--dry-run"],
&["task", "watch", "ember"],
];
for args in removed {
let (ok, code, out, err) = w.cv(args);
assert!(!ok, "cv {args:?} must not still work\nstdout:\n{out}");
assert_eq!(code, 2, "cv {args:?} must exit 2 (caller mistake)\nstderr:\n{err}");
assert!(
err.contains("removed in 0.15.0"),
"cv {args:?} must say when it went away:\n{err}"
);
assert!(
err.contains(" — ") && err.matches('`').count() >= 2,
"cv {args:?} must point at a replacement:\n{err}"
);
}
let (out, _) = w.cv_ok(&["task", "list", "--all"]);
assert!(
out.contains("open"),
"none of the refused commands wrote anything:\n{out}"
);
w.cv_ok(&["task", "done", &id, "--observed", "tests green in sess-1"]);
let (out, _) = w.cv_ok(&["task", "show", &id]);
assert!(
out.contains("done: self-reported") && out.contains("tests green in sess-1"),
"a completion is labeled as the agent's own word:\n{out}"
);
}
#[test]
fn board_is_removed_with_a_pointer() {
let w = World::new("board-removed");
for args in [
&["board", "post", "general", "hi"][..],
&["board", "read", "general"],
&["board", "channels"],
] {
let (ok, code, out, err) = w.cv(args);
assert!(!ok, "cv {args:?} must not still work\nstdout:\n{out}");
assert_eq!(code, 2, "cv {args:?} must exit 2\nstderr:\n{err}");
assert!(
err.contains("removed in 0.15.0") && err.contains("cv task events"),
"{err}"
);
}
assert!(!w.cv_home.join("board").exists(), "nothing wrote a board dir");
}
#[test]
fn task_open_relations_tags_and_file_bodies() {
let w = World::new("task-ergo");
let brief = w.base.join("brief.md");
fs::write(
&brief,
"# The brief\n\nA body that is a document, not a shell string.\n",
)
.unwrap();
let (out, _) = w.cv_ok(&[
"task",
"open",
"FOUNDATION: the thing others wait on",
"--tags",
"lean,deploy",
]);
let a = opened_task_id(&out);
let (out, _) = w.cv_ok(&[
"task",
"open",
"DEPENDENT: needs the foundation",
"--body-file",
brief.to_str().unwrap(),
"--blocked-by",
&a,
"--tags",
"lean",
]);
let b = opened_task_id(&out);
let (out, _) = w.cv_ok(&["task", "open", "UPSTREAM: blocks the foundation", "--blocks", &a]);
let c = opened_task_id(&out);
let (json, _) = w.cv_ok(&["task", "show", &b, "--json"]);
let t: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(t["body"].as_str().unwrap().contains("not a shell string"), "{json}");
assert_eq!(t["tags"], serde_json::json!(["lean"]), "{json}");
assert_eq!(t["blocked_by"], serde_json::json!([a]), "{json}");
let (json, _) = w.cv_ok(&["task", "show", &a, "--json"]);
let t: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(t["blocked_by"], serde_json::json!([c]), "{json}");
assert_eq!(t["tags"], serde_json::json!(["lean", "deploy"]), "{json}");
let (out, _) = w.cv_ok(&["task", "show", &a]);
assert!(out.contains("tags: lean, deploy"), "{out}");
assert!(
out.contains("blocked by: ") && out.contains("[open] UPSTREAM: blocks the foundation"),
"{out}"
);
assert!(out.contains("⊘ BLOCKED"), "{out}");
assert!(
out.contains("blocks: ") && out.contains("DEPENDENT: needs the foundation"),
"{out}"
);
let (out, _) = w.cv_ok(&["task", "list"]);
let blocked_rows = |out: &str| {
out.lines()
.filter(|l| l.starts_with(|c: char| c.is_ascii_hexdigit()) && l.contains("⊘ "))
.count()
};
assert_eq!(blocked_rows(&out), 2, "A and B are blocked:\n{out}");
assert!(out.contains("⊘ = blocked"), "{out}");
let (out, _) = w.cv_ok(&["task", "list", "--tag", "deploy"]);
assert!(out.contains("FOUNDATION") && !out.contains("DEPENDENT"), "{out}");
let (out, _) = w.cv_ok(&["task", "list"]);
for line in out.lines().filter(|l| l.starts_with(|c: char| c.is_ascii_hexdigit())) {
let shown = line.split_whitespace().next().unwrap();
let (ok, _, _, err) = w.cv(&["task", "show", shown]);
assert!(ok, "the prefix `list` prints must resolve in `show`: {shown}\n{err}");
}
w.cv_ok(&["task", "done", &c, "--observed", "merged"]);
let (out, _) = w.cv_ok(&["task", "show", &a]);
assert!(!out.contains("⊘ BLOCKED"), "{out}");
let (out, _) = w.cv_ok(&["task", "list"]);
assert_eq!(blocked_rows(&out), 1, "only B stays blocked:\n{out}");
let (out, _) = w.cv_ok(&["task", "list", "--tsv"]);
assert_eq!(out.lines().count(), 2, "{out}");
let cells: Vec<&str> = out.lines().find(|l| l.starts_with(&b)).unwrap().split('\t').collect();
assert_eq!(cells.len(), 7, "{cells:?}");
assert_eq!(cells[1], "open");
assert_eq!(cells[5], "DEPENDENT: needs the foundation");
assert_eq!(cells[6], a);
let (ok, code, _, err) = w.cv(&["task", "list", "--wide"]);
assert!(!ok && code == 2, "{err}");
assert!(
err.contains("removed in 0.15.0") && err.contains("show <id> --brief"),
"{err}"
);
let note = w.base.join("note.txt");
fs::write(¬e, "progress from a file").unwrap();
w.cv_ok(&["task", "note", &b, "--file", note.to_str().unwrap()]);
let (out, _) = w.cv_ok(&["task", "show", &b]);
assert!(out.contains("progress from a file"), "{out}");
let (ok, ..) = w.cv(&["task", "note", &b]);
assert!(!ok, "note needs text or --file");
w.cv_ok(&["task", "tag", &b, "deploy"]);
let (json, _) = w.cv_ok(&["task", "show", &b, "--json"]);
assert!(json.contains("\"deploy\""), "{json}");
let (ok, _, _, err) = w.cv(&["task", "block", &b, "--by", &b]);
assert!(!ok && err.contains("cannot block itself"), "{err}");
let (ok, _, _, err) = w.cv(&["task", "open", "typo", "--blocked-by", "ffffffff"]);
assert!(
!ok && err.contains("no task matches"),
"relations resolve before the open:\n{err}"
);
let (out, _) = w.cv_ok(&["task", "list", "--all"]);
assert!(!out.contains("typo"), "a refused open leaves nothing behind:\n{out}");
}
#[test]
fn task_sweep_names_merged_branches_and_missing_issue_paths_without_closing() {
let w = World::new("task-sweep");
let repo = w.base.join("repo");
fs::create_dir_all(&repo).unwrap();
let git = |args: &[&str]| {
let out = Command::new("git")
.args(args)
.current_dir(&repo)
.env("GIT_AUTHOR_NAME", "t")
.env("GIT_AUTHOR_EMAIL", "t@t")
.env("GIT_COMMITTER_NAME", "t")
.env("GIT_COMMITTER_EMAIL", "t@t")
.env("HOME", &w.home)
.output()
.unwrap();
assert!(
out.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
};
git(&["init", "-q", "-b", "main"]);
fs::write(repo.join("a.txt"), "a").unwrap();
git(&["add", "."]);
git(&["commit", "-q", "-m", "init"]);
git(&["checkout", "-q", "-b", "k-ran"]);
fs::write(repo.join("b.txt"), "b").unwrap();
git(&["add", "."]);
git(&["commit", "-q", "-m", "work"]);
git(&["checkout", "-q", "main"]);
git(&["merge", "-q", "--no-ff", "-m", "merge", "k-ran"]);
git(&["checkout", "-q", "-b", "p3b1"]);
fs::write(repo.join("c.txt"), "c").unwrap();
git(&["add", "."]);
git(&["commit", "-q", "-m", "unmerged work"]);
git(&["checkout", "-q", "main"]);
fs::write(repo.join("plan.md"), "plan").unwrap();
let r = repo.to_str().unwrap();
let (out, _) = w.cv_ok(&[
"task",
"open",
"K-RAN: run claim",
"--repo",
r,
"--body",
"one commit on branch `k-ran`, not pushed",
]);
let merged = opened_task_id(&out);
w.cv_ok(&[
"task",
"open",
"P3B1: receiver",
"--repo",
r,
"--body",
"work is on `p3b1`",
]);
w.cv_ok(&["task", "open", "PLAN: keep", "--repo", r, "--issue", "plan.md"]);
let (out, _) = w.cv_ok(&[
"task",
"open",
"GONE: issue file deleted",
"--repo",
r,
"--issue",
"docs/gone.md",
]);
let gone = opened_task_id(&out);
w.cv_ok(&[
"task",
"open",
"WORDS: a body that says fix and main",
"--repo",
r,
"--body",
"fix main",
]);
w.cv_ok(&[
"task",
"open",
"OTHER REPO: names k-ran but lives elsewhere",
"--repo",
w.base.to_str().unwrap(),
"--body",
"k-ran",
]);
let (out, _) = w.cv_ok(&["task", "sweep", "--repo", r]);
assert!(out.contains("2 candidate(s)"), "{out}");
assert!(
out.contains("K-RAN: run claim") && out.contains("names branch `k-ran`, merged into main"),
"{out}"
);
assert!(
out.contains("GONE: issue file deleted") && out.contains("issue path no longer exists"),
"{out}"
);
assert!(
!out.contains("P3B1") && !out.contains("PLAN:") && !out.contains("WORDS") && !out.contains("OTHER REPO"),
"{out}"
);
assert!(out.contains("nothing was closed"), "{out}");
let (json, _) = w.cv_ok(&["task", "show", &merged, "--json"]);
assert!(json.contains("\"state\": \"open\""), "{json}");
let (json, _) = w.cv_ok(&["task", "sweep", "--repo", r, "--json"]);
let rows: Vec<serde_json::Value> = serde_json::from_str(&json).unwrap();
assert_eq!(rows.len(), 2);
assert!(rows.iter().any(|x| x["task_id"] == gone));
}
fn normalize(s: &str) -> String {
let s = regex::Regex::new(r"\b[0-9a-f]{8}-[0-9a-f]{1,4}(?:-[0-9a-f]{1,4}){0,3}(?:-[0-9a-f]{1,12})?\b")
.unwrap()
.replace_all(s, "<id>");
let s = regex::Regex::new(r"\b\d{4}-\d\d-\d\d \d\d:\d\d\b")
.unwrap()
.replace_all(&s, "<ts>");
let s = regex::Regex::new(r"\b\d\d-\d\d \d\d:\d\d\b")
.unwrap()
.replace_all(&s, "<ts>");
regex::Regex::new(r"\b\d+[smhdw] ago\b")
.unwrap()
.replace_all(&s, "<age> ago")
.into_owned()
}
#[test]
fn task_inbox_orders_decisions_first_and_renders_markdown() {
let w = World::new("inbox-md");
let (out, _) = w.cv_ok(&["task", "open", "the claimed one", "--body", "claimed body"]);
let c = opened_task_id(&out);
assert!(w.cv_env(&["task", "claim", &c], &[("CV_ENDPOINT", "ember")]).0);
let (out, _) = w.cv_ok(&[
"task",
"open",
"write the docs",
"--assignee",
"ember",
"--body",
"First line of the action.\n\nSecond paragraph.",
]);
let a = opened_task_id(&out);
w.cv_ok(&[
"task",
"note",
&a,
"a note with two lines\nsecond line hidden in md",
"--from",
"orchestrator:x",
]);
let (out, _) = w.cv_ok(&[
"task",
"decide",
"P6: WHO MAY REFILL a purse",
"--for",
"ember",
"--default",
"keep open",
"--option",
"per-refill consent",
"--body",
"today anyone may fund any purse.",
"--from",
"orchestrator:x",
]);
let d = opened_task_id(&out);
plant_legacy_tag_decision(
&w,
"01a0f000-0000-7000-8000-00000000000b",
"legacy decision by tag",
"ember",
);
let (out, _) = w.cv_ok(&["task", "inbox", "ember"]);
let dec = out.find("decisions owed (2):").expect(&out);
let assigned = out.find("assigned actions (1):").expect(&out);
let claimed = out.find("claimed work (1):").expect(&out);
assert!(dec < assigned && assigned < claimed, "group order:\n{out}");
assert!(
out.find("P6: WHO MAY REFILL").unwrap() < out.find("legacy decision by tag").unwrap(),
"{out}"
);
let (md, _) = w.cv_ok(&["task", "inbox", "ember", "--md"]);
let got = normalize(&md);
let expected = "\
# Inbox for ember — <ts>
2 decision(s) owed · 1 assigned action(s) · 1 claimed · 3 unread
## Decisions owed (2)
### 1. P6: WHO MAY REFILL a purse
`<id>` · open · asked <age> ago by orchestrator:x · **unread** (last: orchestrator:x)
- **default:** keep open
- alternative: per-refill consent
- resolve: `cv task resolve <id> --accept-default --from ember`
today anyone may fund any purse.
### 2. legacy decision by tag
`<id>` · open · waiting <age> ago by cv · **unread** (last: cv)
## Assigned actions (1)
### 3. write the docs
`<id>` · open · waiting <age> ago by cv · **unread** (last: orchestrator:x)
First line of the action.
Second paragraph.
notes (1):
- _orchestrator:x, <ts>:_ a note with two lines
## Claimed work (1)
### 4. the claimed one
`<id>` · claimed · waiting <age> ago by cv
claimed body
";
assert_eq!(got, expected, "--md golden (normalized):\n{md}");
let _ = d;
}
#[test]
fn task_show_notes_last_and_grep() {
let w = World::new("notes-filter");
let (out, _) = w.cv_ok(&[
"task",
"open",
"EMBER: everything only ember can do",
"--assignee",
"ember",
]);
let parent = opened_task_id(&out);
for n in [
"move the build base from /tank (slow) to NVMe?",
"PUSH also ~/dev/cv: one unsigned commit.",
"K-PORTAL: who births a guest cell",
"JOIN-SOLANA works.",
] {
w.cv_ok(&["task", "note", &parent, n, "--from", "orchestrator:x"]);
}
let (out, _) = w.cv_ok(&["task", "show", &parent, "--brief"]);
assert!(out.contains("notes: 4 of 4 (newest last) — one line each"), "{out}");
let (out, _) = w.cv_ok(&["task", "show", &parent, "--brief", "--notes-last", "2"]);
assert!(out.contains("notes: 2 of 4"), "{out}");
assert!(
!out.contains("move the build base") && out.contains("JOIN-SOLANA"),
"{out}"
);
let (out, _) = w.cv_ok(&["task", "show", &parent, "--notes-grep", "k-portal"]);
assert!(out.contains("notes: 1 of 4") && out.contains("K-PORTAL"), "{out}");
}
#[test]
fn task_note_reads_stdin_and_open_absolutizes_issue() {
let w = World::new("stdin-paths");
fs::write(w.base.join("PLAN.md"), "# plan\n").unwrap();
let (ok, _, out, err) = w.cv_stdin(
&[
"task",
"open",
"with a heredoc body",
"--body",
"-",
"--issue",
"PLAN.md",
],
"a `code span` body\nwith two lines\n",
);
assert!(ok, "{err}");
let a = opened_task_id(&out);
let (json, _) = w.cv_ok(&["task", "show", &a, "--json"]);
let t: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(t["body"], "a `code span` body\nwith two lines\n");
let issue = t["issue"].as_str().unwrap();
assert!(
std::path::Path::new(issue).is_absolute() && issue.ends_with("PLAN.md"),
"{issue}"
);
assert_eq!(
std::path::Path::new(issue).canonicalize().unwrap(),
w.base.join("PLAN.md").canonicalize().unwrap()
);
let (ok, _, _, err) = w.cv_stdin(&["task", "note", &a, "-"], "note from stdin\n");
assert!(ok, "{err}");
let (out, _) = w.cv_ok(&["task", "show", &a]);
assert!(out.contains("note from stdin"), "{out}");
let (out, _) = w.cv_ok(&["task", "open", "handle", "--issue", "#42"]);
let b = opened_task_id(&out);
let (json, _) = w.cv_ok(&["task", "show", &b, "--json"]);
assert_eq!(
serde_json::from_str::<serde_json::Value>(&json).unwrap()["issue"],
"#42"
);
let (out, _) = w.cv_ok(&["task", "open", "url", "--issue", "https://x.y/issues/4"]);
let c = opened_task_id(&out);
let (json, _) = w.cv_ok(&["task", "show", &c, "--json"]);
assert_eq!(
serde_json::from_str::<serde_json::Value>(&json).unwrap()["issue"],
"https://x.y/issues/4"
);
let (out, _) = w.cv_ok(&["task", "open", "future doc", "--issue", "docs/later.md"]);
let d = opened_task_id(&out);
let (json, _) = w.cv_ok(&["task", "show", &d, "--json"]);
let issue = serde_json::from_str::<serde_json::Value>(&json).unwrap()["issue"]
.as_str()
.unwrap()
.to_string();
assert!(
issue.starts_with(w.base.to_str().unwrap())
|| issue.starts_with(&w.base.canonicalize().unwrap().display().to_string()),
"{issue}"
);
}
fn seeded_old_and_new(w: &World) -> (String, String) {
let old = "01900000-0000-7000-8000-000000000001".to_string(); let new = "01a00000-0000-7000-8000-000000000002".to_string();
w.seed_task_log(&[
serde_json::json!({"id": old, "task_id": old, "ts": "2026-01-01T00:00:00Z", "by": "orchestrator:old",
"event": "opened", "title": "OLD: from another project", "channel": "tasks", "assignee": "ember"}),
serde_json::json!({"id": new, "task_id": new, "ts": chrono_now(), "by": "orchestrator:new",
"event": "opened", "title": "NEW: today's task", "channel": "tasks", "assignee": "ember"}),
]);
(old, new)
}
fn chrono_now() -> String {
chrono_at(0)
}
fn chrono_at(offset: u64) -> String {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
+ offset;
let days = secs / 86_400;
let (h, m, s) = ((secs % 86_400) / 3600, (secs % 3600) / 60, secs % 60);
let z = days as i64 + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let mo = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if mo <= 2 { y + 1 } else { y };
format!("{y:04}-{mo:02}-{d:02}T{h:02}:{m:02}:{s:02}Z")
}
#[test]
fn task_list_and_inbox_scope_to_recent_and_all_shows_old() {
let w = World::new("scope");
let (old, new) = seeded_old_and_new(&w);
let (out, _) = w.cv_ok(&["task", "list"]);
assert!(out.contains("NEW: today") && !out.contains("OLD: from"), "{out}");
assert!(
out.trim_end()
.ends_with("(1 older task(s) hidden — `--all`, or `--since 90d`)"),
"{out}"
);
let (out, _) = w.cv_ok(&["task", "list", "--all"]);
assert!(
out.contains("OLD: from") && out.contains("NEW: today") && !out.contains("hidden"),
"{out}"
);
let (out, _) = w.cv_ok(&["task", "list", "--since", "2025-12-01"]);
assert!(out.contains("OLD: from") && !out.contains("hidden"), "{out}");
let (out, _) = w.cv_ok(&["task", "list", "--since", "1d"]);
assert!(!out.contains("OLD: from") && out.contains("hidden"), "{out}");
let (ok, _, out, _) = w.cv_env(&["task", "list"], &[("CV_ENDPOINT", "orchestrator:old")]);
assert!(ok && out.contains("OLD: from") && !out.contains("hidden"), "{out}");
let (ok, _, out, _) = w.cv_env(&["task", "list"], &[("CV_ENDPOINT", "web:ember")]);
assert!(
ok && out.contains("OLD: from"),
"web:<who> ≡ <who>, the assignee:\n{out}"
);
let (out, _) = w.cv_ok(&["task", "inbox", "ember"]);
assert!(out.contains("NEW: today") && !out.contains("OLD: from"), "{out}");
assert!(out.contains("(1 older item(s) hidden"), "{out}");
let (out, _) = w.cv_ok(&["task", "inbox", "ember", "--all"]);
assert!(out.contains("OLD: from") && out.contains("⏰"), "{out}");
let (md, _) = w.cv_ok(&["task", "inbox", "ember", "--md"]);
assert!(md.contains("1 older hidden (`--all`)"), "{md}");
let _ = (old, new);
}
#[test]
fn task_inbox_unread_and_events_since() {
let w = World::new("unread-events");
let (out, _) = w.cv_ok(&[
"task",
"open",
"A: touched by the orchestrator last",
"--assignee",
"ember",
"--from",
"orchestrator:x",
]);
let a = opened_task_id(&out);
let (out, _) = w.cv_ok(&[
"task",
"open",
"B: ember spoke last",
"--assignee",
"ember",
"--from",
"orchestrator:x",
]);
let b = opened_task_id(&out);
w.cv_ok(&["task", "note", &b, "on it", "--from", "ember"]);
let (out, _) = w.cv_ok(&[
"task",
"open",
"C: web ember spoke last",
"--assignee",
"ember",
"--from",
"orchestrator:x",
]);
let c = opened_task_id(&out);
w.cv_ok(&["task", "note", &c, "via the page", "--from", "web:ember"]);
let (out, _) = w.cv_ok(&["task", "inbox", "ember", "--unread"]);
assert!(
out.contains("A: touched") && !out.contains("B: ember") && !out.contains("C: web"),
"{out}"
);
let (json, _) = w.cv_ok(&["task", "inbox", "ember", "--unread", "--json"]);
assert!(json.contains(&a) && !json.contains(&b), "{json}");
let (out, err) = w.cv_ok(&["task", "events"]);
let lines: Vec<serde_json::Value> = out.lines().map(|l| serde_json::from_str(l).unwrap()).collect();
assert_eq!(lines.len(), 5, "{out}");
assert_eq!(lines[0]["event"], "opened");
assert_eq!(lines[0]["title"], "A: touched by the orchestrator last");
assert_eq!(lines[0]["task_state"], "open");
assert_eq!(lines[0]["assignee"], "ember");
let last_id = lines[4]["id"].as_str().unwrap().to_string();
assert!(err.contains(&format!("next: --since {last_id}")), "{err}");
let (out, _) = w.cv_ok(&["task", "events", "--since", &last_id]);
assert!(out.trim().is_empty(), "{out}");
let (out, _) = w.cv_ok(&["task", "events", "--since", lines[3]["id"].as_str().unwrap()]);
assert_eq!(out.lines().count(), 1, "{out}");
let (out, _) = w.cv_ok(&["task", "events", "--since", "1h", "--kind", "note"]);
assert_eq!(out.lines().count(), 2, "{out}");
let (out, _) = w.cv_ok(&["task", "events", "--by", "ember"]);
assert_eq!(
out.lines().count(),
2,
"web:ember is ember acting through the page:\n{out}"
);
let (out, _) = w.cv_ok(&["task", "events", "--not-by", "orchestrator:x", "--text"]);
assert_eq!(out.lines().count(), 2, "{out}");
assert!(
out.contains("noted") && out.contains("on it") && out.contains("via the page"),
"{out}"
);
let (out, _) = w.cv_ok(&["task", "events", "--task", &b]);
assert_eq!(out.lines().count(), 2, "{out}");
let (_, err) = w.cv_fails(&["task", "events", "--since", "whenever"]);
assert!(err.contains("cannot read"), "{err}");
let (ok, _, out, _) = w.cv_env(
&[
"task",
"events",
"--assignee",
"ember",
"--not-by",
"orchestrator:x",
"--since",
"1d",
],
&[("CV_ENDPOINT", "orchestrator:x")],
);
assert!(ok);
let rows: Vec<serde_json::Value> = out.lines().map(|l| serde_json::from_str(l).unwrap()).collect();
assert_eq!(rows.len(), 2, "{out}");
assert!(rows.iter().all(|r| r["event"] == "noted"), "{out}");
let (ok, code, _, err) = w.cv(&["task", "watch", "--assignee", "ember"]);
assert!(!ok && code == 2, "{err}");
assert!(err.contains("removed in 0.15.0") && err.contains("--not-by"), "{err}");
}
fn http(addr: &str, method: &str, path: &str, body: Option<&str>) -> (u16, String) {
use std::io::{Read, Write};
let mut s = std::net::TcpStream::connect(addr).expect("connect to cv serve");
s.set_read_timeout(Some(std::time::Duration::from_secs(10))).unwrap();
let body = body.unwrap_or("");
let req = format!(
"{method} {path} HTTP/1.1\r\nHost: {addr}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
);
s.write_all(req.as_bytes()).unwrap();
let mut raw = Vec::new();
s.read_to_end(&mut raw).unwrap();
let text = String::from_utf8_lossy(&raw).into_owned();
let (head, body) = text.split_once("\r\n\r\n").unwrap_or((&text, ""));
let status: u16 = head.split_whitespace().nth(1).unwrap_or("0").parse().unwrap_or(0);
(status, body.to_string())
}
fn serve(w: &World, who: &str) -> (std::process::Child, String) {
use std::io::{BufRead, BufReader};
let mut child = w
.cv_command()
.args(["serve", "--port", "0", "--who", who])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("cv serve starts");
let stderr = child.stderr.take().unwrap();
let mut lines = BufReader::new(stderr).lines();
let banner = lines.next().expect("a banner line").expect("readable");
let addr = banner
.split("http://")
.nth(1)
.and_then(|rest| rest.split_whitespace().next())
.unwrap_or_else(|| panic!("no address in banner: {banner}"))
.to_string();
std::thread::spawn(move || for _ in lines {});
(child, addr)
}
#[test]
fn the_inbox_records_the_same_events_as_the_cli() {
let w = World::new("serve");
let (out, _) = w.cv_ok(&[
"task",
"decide",
"PAY §7: tariff",
"--for",
"ember",
"--default",
"1 credit/unit",
"--option",
"free tier",
"--from",
"orchestrator:x",
]);
let d = opened_task_id(&out);
let (out, _) = w.cv_ok(&[
"task",
"open",
"push dregg-infra",
"--assignee",
"ember",
"--from",
"orchestrator:x",
]);
let a = opened_task_id(&out);
let (mut child, addr) = serve(&w, "ember");
let result = std::panic::catch_unwind(|| {
let (status, html) = http(&addr, "GET", "/", None);
assert_eq!(status, 200);
assert!(
html.contains("<title>cv inbox</title>") && html.contains("/api/inbox"),
"{}",
&html[..200.min(html.len())]
);
assert!(
!html.contains("<script src=") && !html.contains("<link rel=\"stylesheet\""),
"no external resources"
);
let (status, _) = {
use std::io::{Read, Write};
let mut s = std::net::TcpStream::connect(&addr).unwrap();
s.write_all(b"GET /api/inbox HTTP/1.1\r\nHost: evil.example\r\nConnection: close\r\n\r\n")
.unwrap();
let mut raw = String::new();
s.read_to_string(&mut raw).unwrap();
(
raw.split_whitespace().nth(1).unwrap_or("0").parse::<u16>().unwrap_or(0),
raw,
)
};
assert_eq!(status, 403);
let (status, body) = http(&addr, "GET", "/api/inbox?who=ember", None);
assert_eq!(status, 200, "{body}");
let page: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(page["who"], "ember");
assert_eq!(page["counts"]["decisions"], 1);
assert_eq!(page["counts"]["assigned"], 1);
let items = page["items"].as_array().unwrap();
assert_eq!(items[0]["reason"], "decision_owed");
assert_eq!(items[0]["decision"]["default"], "1 credit/unit");
assert_eq!(items[0]["decision"]["alternatives"], serde_json::json!(["free tier"]));
assert_eq!(items[1]["reason"], "assigned_open");
let (status, body) = http(&addr, "GET", "/api/inbox", None);
assert_eq!(status, 200);
assert!(body.contains("\"who\":\"ember\""), "{body}");
let (status, body) = http(
&addr,
"POST",
&format!("/api/task/{d}/note"),
Some(r#"{"text":"reading PAY first"}"#),
);
assert_eq!(status, 200, "{body}");
assert!(body.contains("\"by\":\"web:ember\""), "{body}");
let (status, body) = http(
&addr,
"POST",
&format!("/api/task/{d}/discuss"),
Some(r#"{"text":"what is a unit?"}"#),
);
assert_eq!(status, 200, "{body}");
let (status, body) = http(
&addr,
"POST",
&format!("/api/task/{d}/resolve"),
Some(r#"{"choice":"free tier","note":"for October"}"#),
);
assert_eq!(status, 200, "{body}");
assert!(body.contains("\"task_state\":\"resolved\""), "{body}");
let (status, body) = http(
&addr,
"POST",
&format!("/api/task/{d}/resolve"),
Some(r#"{"accept_default":true}"#),
);
assert_eq!(status, 400, "a second resolution is refused: {body}");
let (status, body) = http(
&addr,
"POST",
&format!("/api/task/{a}/done"),
Some(r#"{"observed":"pushed"}"#),
);
assert_eq!(status, 200, "{body}");
let (status, body) = http(&addr, "POST", &format!("/api/task/{a}/done"), Some("{}"));
assert_eq!(status, 400, "{body}");
let (status, body) = http(&addr, "POST", &format!("/api/task/{a}/frobnicate"), Some("{}"));
assert_eq!(status, 400, "{body}");
let (status, body) = http(&addr, "POST", &format!("/api/task/{a}/note"), Some("not json"));
assert_eq!(status, 400, "{body}");
let (status, body) = http(&addr, "GET", "/api/inbox?who=ember", None);
assert_eq!(status, 200);
let page: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(page["items"].as_array().unwrap().len(), 0);
assert_eq!(page["closed"].as_array().unwrap().len(), 2);
let (status, body) = http(&addr, "POST", &format!("/api/task/{a}/reopen"), Some("{}"));
assert_eq!(status, 200, "{body}");
let (status, body) = http(&addr, "GET", "/api/inbox?who=ember", None);
assert_eq!(status, 200);
let page: serde_json::Value = serde_json::from_str(&body).unwrap();
let items = page["items"].as_array().unwrap();
assert_eq!(items.len(), 1, "{body}");
assert_eq!(items[0]["title"], "push dregg-infra");
assert!(
items[0]["tags"].as_array().unwrap().iter().any(|t| t == "reopened"),
"{body}"
);
let (status, body) = http(&addr, "GET", &format!("/api/task/{d}?events=1"), None);
assert_eq!(status, 200);
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["effective_state"], "resolved");
assert!(v["events"].as_array().unwrap().len() >= 6, "{body}");
let (status, body) = http(&addr, "GET", "/api/events?since=1h&kind=resolved,done", None);
assert_eq!(status, 200);
let rows: Vec<serde_json::Value> = body.lines().map(|l| serde_json::from_str(l).unwrap()).collect();
assert_eq!(rows.len(), 2, "{body}");
assert!(rows.iter().all(|r| r["by"] == "web:ember"), "{body}");
});
let _ = child.kill();
let _ = child.wait();
if let Err(e) = result {
std::panic::resume_unwind(e);
}
let (out, _) = w.cv_ok(&["task", "show", &d, "--brief"]);
assert!(out.contains("[resolved]"), "{out}");
assert!(
out.contains("resolved: free tier — by web:ember,") && out.contains("· for October"),
"{out}"
);
assert!(out.contains("web:ember") && out.contains("reading PAY first"), "{out}");
assert!(out.contains("NEEDS DISCUSSION: what is a unit?"), "{out}");
assert!(
out.contains("tags: discuss"),
"a decision is posed, never tagged:\n{out}"
);
let (out, _) = w.cv_ok(&["task", "show", &a, "--brief"]);
assert!(
out.contains("[done]") && out.contains("self-reported") && out.contains("(pushed)"),
"{out}"
);
let (json, _) = w.cv_ok(&["task", "show", &a, "--events"]);
assert!(json.contains("\"by\": \"web:ember\""), "{json}");
let (out, _) = w.cv_ok(&["task", "events", "--since", "1h", "--kind", "resolved,done"]);
assert_eq!(out.lines().count(), 2, "the CLI feed and /api/events agree:\n{out}");
}
fn chain_walk(path: &std::path::Path) -> (usize, usize, serde_json::Value) {
let lines: Vec<serde_json::Value> = fs::read_to_string(path)
.unwrap()
.lines()
.map(|l| serde_json::from_str(l).unwrap())
.collect();
let threaded: Vec<&serde_json::Value> = lines.iter().filter(|v| v.get("uuid").is_some()).collect();
let by: std::collections::HashMap<&str, &serde_json::Value> =
threaded.iter().map(|v| (v["uuid"].as_str().unwrap(), *v)).collect();
let mut cur = *threaded.last().unwrap();
let mut n = 1;
while let Some(p) = cur["parentUuid"].as_str() {
match by.get(p) {
Some(v) => {
cur = v;
n += 1;
}
None => break,
}
}
(n, threaded.len(), cur.clone())
}
#[test]
fn distill_emits_a_whole_resumable_session_and_a_sub_agent() {
let w = World::new("distill");
let big = "y".repeat(9000);
w.write_session(
"rootsess",
&[serde_json::json!({
"type": "user", "uuid": "r1", "sessionId": "rootsess", "timestamp": "2026-01-01T09:00:00Z",
"cwd": "/work/proj", "message": {"role": "user", "content": "run the lanes"}
})],
);
let sub = w.home.join(".claude/projects/-work-proj/rootsess/subagents");
fs::create_dir_all(&sub).unwrap();
let agent = sub.join("agent-a0123456789abcdef.jsonl");
let line = |v: serde_json::Value| {
let mut v = v;
v["isSidechain"] = true.into();
v["agentId"] = "a0123456789abcdef".into();
v["sessionId"] = "rootsess".into();
v["cwd"] = "/work/proj".into();
format!("{v}\n")
};
let body = [
line(serde_json::json!({"type": "user", "uuid": "u0", "parentUuid": null, "timestamp": "2026-01-01T10:00:00Z",
"message": {"role": "user", "content": "You are lane TEST. Build it on box1 and commit on lane/test."}})),
line(serde_json::json!({"type": "assistant", "uuid": "a1", "parentUuid": "u0", "timestamp": "2026-01-01T10:01:00Z",
"message": {"role": "assistant", "model": "claude-test-1", "content": [
{"type": "tool_use", "id": "t1", "name": "Bash", "input": {"command": "ssh ember@box1 'cat big.log'", "description": "Read the big log"}}]}})),
line(serde_json::json!({"type": "user", "uuid": "u1", "parentUuid": "a1", "timestamp": "2026-01-01T10:02:00Z",
"message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "t1", "content": big, "is_error": false}]}})),
line(serde_json::json!({"type": "attachment", "uuid": "h1", "parentUuid": "u1", "timestamp": "2026-01-01T10:02:30Z",
"attachment": {"type": "hook_success", "hookName": "PostToolUse", "content": ""}})),
line(serde_json::json!({"type": "assistant", "uuid": "a2", "parentUuid": "h1", "timestamp": "2026-01-01T10:03:00Z",
"message": {"role": "assistant", "model": "claude-test-1", "content": [
{"type": "text", "text": "Chose a rebase: the queue wants linear ranges."},
{"type": "tool_use", "id": "t2", "name": "Bash", "input": {"command": "cd /srv/x && git commit -F msg", "description": "Commit"}}]}})),
line(serde_json::json!({"type": "user", "uuid": "u2", "parentUuid": "a2", "timestamp": "2026-01-01T10:04:00Z",
"message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "t2", "content": "[lane/test 1234abcd] kernel: one", "is_error": false}]}})),
line(serde_json::json!({"type": "assistant", "uuid": "a3", "parentUuid": "u2", "timestamp": "2026-01-01T10:05:00Z",
"message": {"role": "assistant", "model": "claude-test-1", "content": [{"type": "text", "text": "Committed."}],
"usage": {"input_tokens": 5, "cache_read_input_tokens": 900000, "output_tokens": 10}}})),
]
.concat();
fs::write(&agent, &body).unwrap();
let agent_s = agent.to_str().unwrap();
let (out, err) = w.cv_ok(&["distill", agent_s, "--keep-last", "1"]);
assert!(out.contains("You are lane TEST."), "{out}");
assert!(out.contains("Chose a rebase"), "{out}");
assert!(out.contains("`lane/test` `1234abcd`"), "{out}");
assert!(out.contains("`ember@box1`"), "{out}");
assert!(!out.contains(&"y".repeat(200)), "the big output must be elided");
assert!(err.contains("distilled"), "{err}");
let (out, _) = w.cv_ok(&["distill", agent_s, "--keep-last", "1", "--session", "--json"]);
let v: serde_json::Value = serde_json::from_str(out.trim()).expect("stdout must be pure JSON");
let path = PathBuf::from(v["emitted"]["path"].as_str().unwrap());
let new_id = v["emitted"]["session_id"].as_str().unwrap().to_string();
let (reached, threaded, root) = chain_walk(&path);
assert_eq!(reached, threaded, "a dangling parent truncates the resumed context");
assert!(
root["message"]["content"].as_str().unwrap().contains("[cv distill]"),
"{root}"
);
let text = fs::read_to_string(&path).unwrap();
assert!(!text.contains("\"isSidechain\":true"), "a main-thread session");
assert!(
!text.contains("900000"),
"the source's near-limit usage must not reach the resume gate"
);
let (cat, _) = w.cv_ok(&["cat", &new_id, "t1"]);
assert!(cat.contains(&"y".repeat(9000)), "cv cat must return the elided payload");
let (out, _) = w.cv_ok(&[
"distill",
agent_s,
"--keep-last",
"1",
"--agent-of",
"rootsess",
"--json",
]);
let v: serde_json::Value = serde_json::from_str(out.trim()).unwrap();
let aid = v["emitted"]["agent_id"].as_str().unwrap();
let apath = sub.join(format!("agent-{aid}.jsonl"));
assert!(apath.exists() && sub.join(format!("agent-{aid}.meta.json")).exists());
let (reached, threaded, _) = chain_walk(&apath);
assert_eq!(reached, threaded);
for l in fs::read_to_string(&apath).unwrap().lines() {
let r: serde_json::Value = serde_json::from_str(l).unwrap();
assert_eq!(r["isSidechain"], true, "{l}");
assert_eq!(r["agentId"], aid, "{l}");
assert_eq!(r["sessionId"], "rootsess", "{l}");
}
assert_eq!(
fs::read_to_string(&agent).unwrap(),
body,
"distill must never modify the source"
);
let (out, _) = w.cv_ok(&[
"distill",
agent_s,
"--agent-of",
"rootsess",
"--model",
"inherit",
"--json",
]);
let v: serde_json::Value = serde_json::from_str(out.trim()).unwrap();
let meta = sub.join(format!(
"agent-{}.meta.json",
v["emitted"]["agent_id"].as_str().unwrap()
));
let meta: serde_json::Value = serde_json::from_str(&fs::read_to_string(meta).unwrap()).unwrap();
assert_eq!(meta["model"], "inherit", "{meta}");
let (ok, code, _, _) = w.cv(&["distill", agent_s, "--session", "--model", "opus"]);
assert!(!ok && code == 2);
let spec = format!("{agent_s}:..2");
let (out, _) = w.cv_ok(&["splice", &spec, "--session", "--json"]);
let v: serde_json::Value = serde_json::from_str(out.trim()).expect("stdout must be pure JSON");
let path = PathBuf::from(v["path"].as_str().unwrap());
let (reached, threaded, root) = chain_walk(&path);
assert_eq!(reached, threaded);
assert_eq!(root["uuid"], "u0", "{root}");
let text = fs::read_to_string(&path).unwrap();
assert!(
!text.contains("\"t1\"") && !text.contains("\"isSidechain\":true"),
"{text}"
);
let (out, _) = w.cv_ok(&["splice", &spec, "--agent-of", "rootsess", "--json"]);
let v: serde_json::Value = serde_json::from_str(out.trim()).unwrap();
let aid = v["agent_id"].as_str().unwrap();
let apath = sub.join(format!("agent-{aid}.jsonl"));
assert!(apath.exists() && sub.join(format!("agent-{aid}.meta.json")).exists());
for l in fs::read_to_string(&apath).unwrap().lines() {
let r: serde_json::Value = serde_json::from_str(l).unwrap();
assert!(
r["isSidechain"] == true && r["agentId"] == aid && r["sessionId"] == "rootsess",
"{l}"
);
}
}
#[test]
fn adopt_moves_a_stranded_lane_into_the_live_session() {
let w = World::new("adopt");
let (dead, live, aid) = ("deadsess", "livesess", "a0123456789abcdef");
let root_line = |sid: &str| {
serde_json::json!({"type": "user", "uuid": format!("{sid}-1"), "sessionId": sid,
"timestamp": "2026-01-01T09:00:00Z", "cwd": "/work/proj",
"message": {"role": "user", "content": "run the lanes"}})
};
w.write_session(dead, &[root_line(dead)]);
let sub = w.home.join(".claude/projects/-work-proj/deadsess/subagents");
fs::create_dir_all(&sub).unwrap();
let line = |v: serde_json::Value| {
let mut v = v;
v["isSidechain"] = true.into();
v["agentId"] = aid.into();
v["sessionId"] = dead.into();
v["cwd"] = "/work/proj".into();
format!("{v}\n")
};
let body = [
line(serde_json::json!({"type": "user", "uuid": "u0", "parentUuid": null, "timestamp": "2026-01-01T10:00:00Z",
"message": {"role": "user", "content": "You are lane TEST."}})),
line(serde_json::json!({"type": "assistant", "uuid": "a1", "parentUuid": "u0", "timestamp": "2026-01-01T10:01:00Z",
"message": {"role": "assistant", "model": "claude-test-1", "content": [
{"type": "tool_use", "id": "t1", "name": "Bash", "input": {"command": "grep sessionId x.jsonl"}}]}})),
line(serde_json::json!({"type": "user", "uuid": "u1", "parentUuid": "a1", "timestamp": "2026-01-01T10:02:00Z",
"message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "t1",
"content": "{\"sessionId\":\"deadsess\"}", "is_error": false}]}})),
]
.concat();
let agent = sub.join(format!("agent-{aid}.jsonl"));
fs::write(&agent, &body).unwrap();
let meta = r#"{"agentType":"general-purpose","description":"Lane TEST","model":"opus"}"#;
fs::write(sub.join(format!("agent-{aid}.meta.json")), meta).unwrap();
std::thread::sleep(std::time::Duration::from_millis(20));
w.write_session(live, &[root_line(live)]);
let (out, _) = w.cv_ok(&["lanes", dead]);
assert!(out.contains(&aid[..8]) && out.contains("Lane TEST"), "{out}");
let (out, _) = w.cv_ok(&["lanes", "--recent", "5", "--unfinished", "--cwd", "/work/proj"]);
assert!(out.contains("SESSION"), "rows are tagged with their session: {out}");
assert!(out.contains("deadsess") && out.contains(&aid[..8]), "{out}");
assert!(out.contains("cv adopt <agent-id>"), "the rescue hint: {out}");
let (out, _) = w.cv_ok(&[
"lanes",
"--recent",
"5",
"--unfinished",
"--cwd",
"/work/proj",
"--json",
]);
let rows: Vec<serde_json::Value> = serde_json::from_str(&out).unwrap();
assert!(
rows.iter()
.any(|r| r["parent_id"] == "deadsess" && r["agent_id"] == aid),
"{out}"
);
let (ok, code, _, err) = w.cv(&["adopt", "--list", dead]);
assert!(!ok && code == 2 && err.contains("cv lanes deadsess"), "{err}");
let (ok, code, _, err) = w.cv(&["adopt", "--orphans"]);
assert!(
!ok && code == 2 && err.contains("cv lanes --recent <N> --unfinished"),
"{err}"
);
let live_sub = w.home.join(".claude/projects/-work-proj/livesess/subagents");
let (_, err) = w.cv_ok(&["adopt", &aid[..8], "--dry-run"]);
assert!(err.contains("into livesess"), "the default target is announced: {err}");
assert!(err.contains("restamped on 3 of 3 lines"), "{err}");
assert!(
err.contains(&format!("\"to\": \"{aid}\"")),
"the SendMessage incantation: {err}"
);
assert!(err.contains("cv cat deadsess"), "the sidecar reminder: {err}");
assert!(!live_sub.exists(), "a dry run writes nothing");
w.cv_ok(&["adopt", aid]);
let copied = fs::read_to_string(live_sub.join(format!("agent-{aid}.jsonl"))).unwrap();
assert_eq!(
copied,
body.replace("\"sessionId\":\"deadsess\"", "\"sessionId\":\"livesess\"")
.replace(
"{\\\"sessionId\\\":\\\"livesess\\\"}",
"{\\\"sessionId\\\":\\\"deadsess\\\"}"
)
);
assert!(
copied.contains("{\\\"sessionId\\\":\\\"deadsess\\\"}"),
"content untouched"
);
assert_eq!(
fs::read_to_string(live_sub.join(format!("agent-{aid}.meta.json"))).unwrap(),
meta
);
assert_eq!(
fs::read_to_string(&agent).unwrap(),
body,
"the source is never modified"
);
fs::write(live_sub.join(format!("agent-{aid}.jsonl")), "sentinel\n").unwrap();
let (ok, _, _, err) = w.cv(&["adopt", aid, "--from", dead, "--into", live]);
assert!(!ok && err.contains("refusing to overwrite"), "{err}");
assert_eq!(
fs::read_to_string(live_sub.join(format!("agent-{aid}.jsonl"))).unwrap(),
"sentinel\n"
);
w.cv_ok(&["adopt", aid, "--from", dead, "--into", live, "--force"]);
assert!(fs::read_to_string(live_sub.join(format!("agent-{aid}.jsonl")))
.unwrap()
.contains("livesess"));
w.cv_ok(&[
"adopt", aid, "--from", dead, "--into", live, "--force", "--model", "sonnet",
]);
let m: serde_json::Value =
serde_json::from_str(&fs::read_to_string(live_sub.join(format!("agent-{aid}.meta.json"))).unwrap()).unwrap();
assert_eq!(
(m["model"].as_str(), m["description"].as_str()),
(Some("sonnet"), Some("Lane TEST"))
);
let (out, _) = w.cv_ok(&["cat", &format!("agent-{aid}"), "t1"]);
assert!(out.contains("deadsess"), "{out}");
}
#[test]
fn adopt_reaches_extra_claude_roots_and_never_targets_the_dead_session() {
let w = World::new("adopt-roots");
let (dead, live, aid) = ("deadsess", "livesess", "afedcba9876543210");
let root_line = |sid: &str| {
serde_json::json!({"type": "user", "uuid": format!("{sid}-1"), "sessionId": sid,
"timestamp": "2026-01-01T09:00:00Z", "cwd": "/work/proj",
"message": {"role": "user", "content": "run the lanes"}})
};
w.write_session(live, &[root_line(live)]);
std::thread::sleep(std::time::Duration::from_millis(20));
let seat_proj = w.home.join("seats/bonsai/claude/projects/-work-proj");
let sub = seat_proj.join(dead).join("subagents");
fs::create_dir_all(&sub).unwrap();
fs::write(
seat_proj.join(format!("{dead}.jsonl")),
format!("{}\n", root_line(dead)),
)
.unwrap();
let mut l = serde_json::json!({"type": "user", "uuid": "u0", "parentUuid": null, "timestamp": "2026-01-01T10:00:00Z",
"cwd": "/work/proj", "message": {"role": "user", "content": "You are lane SEAT."}});
l["isSidechain"] = true.into();
l["agentId"] = aid.into();
l["sessionId"] = dead.into();
fs::write(sub.join(format!("agent-{aid}.jsonl")), format!("{l}\n")).unwrap();
let (ok, _, _, err) = w.cv(&["adopt", aid, "--dry-run"]);
assert!(!ok && err.contains("no sub-agent"), "{err}");
fs::write(w.cv_home.join("claude-roots"), "~/seats/*/claude\n").unwrap();
let (_, err) = w.cv_ok(&["adopt", aid, "--dry-run"]);
assert!(
err.contains("into livesess"),
"the dead session is newer but is never the target: {err}"
);
w.cv_ok(&["adopt", aid]);
let copied = w
.home
.join(".claude/projects/-work-proj/livesess/subagents")
.join(format!("agent-{aid}.jsonl"));
assert!(fs::read_to_string(copied)
.unwrap()
.contains("\"sessionId\":\"livesess\""));
let (out, _) = w.cv_ok(&["lanes", dead]);
assert!(out.contains(&aid[..8]), "a seat session lists by id too: {out}");
}
#[test]
fn task_provisional_resolution_veto_and_confirm() {
let w = World::new("provisional");
let (out, _) = w.cv_ok(&[
"task",
"decide",
"build base: /tank or NVMe",
"--for",
"ember",
"--default",
"keep /tank",
"--option",
"move to NVMe",
"--provisional",
"--from",
"orchestrator:o",
]);
let d = opened_task_id(&out);
assert!(
out.contains("resolved") && out.contains("provisionally on the default"),
"{out}"
);
let (out, _) = w.cv_ok(&["task", "inbox", "ember"]);
assert!(
out.starts_with("made for you (veto?) (1):"),
"not owed, its own group:\n{out}"
);
assert!(
out.contains("⇒ made: keep /tank — by orchestrator:o · veto to: move to NVMe"),
"{out}"
);
assert!(out.contains("cv task resolve <id> --confirm --from ember"), "{out}");
let (json, _) = w.cv_ok(&["task", "inbox", "ember", "--json"]);
assert!(json.contains("\"provisional\""), "{json}");
let (out, _) = w.cv_ok(&["task", "show", &d]);
assert!(out.contains("[resolved]") && out.contains("PROVISIONAL"), "{out}");
assert!(out.contains("veto?: cv task resolve"), "{out}");
let (out, _) = w.cv_ok(&[
"task",
"decide",
"Q2",
"--for",
"ember",
"--default",
"a",
"--option",
"b",
"--from",
"orchestrator:o",
]);
let d2 = opened_task_id(&out);
let (_, err) = w.cv_fails(&[
"task",
"resolve",
&d2,
"--choice",
"b",
"--provisional",
"--from",
"orchestrator:o",
]);
assert!(err.contains("stands on the default"), "{err}");
let (_, err) = w.cv_fails(&[
"task",
"resolve",
&d,
"--choice",
"move to NVMe",
"--from",
"lane:someone",
]);
assert!(err.contains("only the decision's assignee (ember)"), "{err}");
let (_, err) = w.cv_fails(&["task", "resolve", &d2, "--confirm", "--from", "ember"]);
assert!(err.contains("no provisional resolution to confirm"), "{err}");
let (_, err) = w.cv_fails(&["task", "resolve", &d2, "--accept-default"]);
assert!(
err.contains("records WHO decided") && err.contains("--accept-default --from ember"),
"{err}"
);
let (out, _) = w.cv_ok(&["task", "resolve", &d, "--choice", "move to NVMe", "--from", "ember"]);
assert!(out.contains("→ move to NVMe (by ember)"), "{out}");
let (out, _) = w.cv_ok(&["task", "show", &d]);
assert!(out.contains("provisionally: keep /tank — by orchestrator:o"), "{out}");
assert!(
out.contains("resolved: move to NVMe — by ember") && !out.contains("PROVISIONAL"),
"{out}"
);
let (json, _) = w.cv_ok(&["task", "show", &d, "--json"]);
let t: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(t["decision"]["resolution"]["choice"], "move to NVMe");
assert!(t["decision"]["resolution"].get("provisional").is_none(), "{json}");
assert_eq!(t["decision"]["superseded_provisional"]["provisional"], true, "{json}");
let (out, _) = w.cv_ok(&["task", "inbox", "ember"]);
assert!(
out.contains("(inbox empty for ember)") || !out.contains("made for you"),
"{out}"
);
let (_, err) = w.cv_fails(&["task", "resolve", &d, "--accept-default", "--from", "ember"]);
assert!(err.contains("cannot apply resolved"), "{err}");
let (out, _) = w.cv_ok(&[
"task",
"decide",
"Q3",
"--for",
"ember",
"--default",
"x",
"--option",
"y",
"--provisional",
"--from",
"orchestrator:o",
]);
let d3 = opened_task_id(&out);
let (mut child, addr) = serve(&w, "ember");
let result = std::panic::catch_unwind(|| {
let (status, body) = http(&addr, "GET", "/api/inbox?who=ember", None);
assert_eq!(status, 200, "{body}");
let page: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(page["counts"]["provisional"], 1, "{body}");
assert_eq!(page["counts"]["decisions"], 1, "Q2 is still owed; Q3 is not: {body}");
let made: Vec<&serde_json::Value> = page["items"]
.as_array()
.unwrap()
.iter()
.filter(|i| i["reason"] == "provisional")
.collect();
assert_eq!(made.len(), 1, "{body}");
assert_eq!(made[0]["title"], "Q3");
let (status, body) = http(
&addr,
"POST",
&format!("/api/task/{d3}/resolve"),
Some(r#"{"confirm":true}"#),
);
assert_eq!(status, 200, "{body}");
});
let _ = child.kill();
let _ = child.wait();
if let Err(e) = result {
std::panic::resume_unwind(e);
}
let (json, _) = w.cv_ok(&["task", "show", &d3, "--json"]);
let t: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(t["decision"]["resolution"]["by"], "web:ember");
assert_eq!(t["decision"]["resolution"]["choice"], "x");
assert_eq!(t["decision"]["superseded_provisional"]["by"], "orchestrator:o");
}