use crate::harness::*;
#[test]
fn files_bare_uuid_positional_routes_to_session() {
let h = populated_home();
let out = h.run(&[
"files",
at(SESS).as_str(),
"--by",
"summary",
"--no-subagents",
]);
assert!(
out.success,
"bare-uuid positional must resolve as a session; stderr: {}",
out.stderr
);
assert!(
!out.stderr.contains("no Claude Code project dir"),
"a bare uuid must NOT be encoded as a project dir; stderr: {}",
out.stderr
);
assert!(
out.stdout.contains("detail=summary"),
"files summary did not run; got: {}",
out.stdout
);
}
#[test]
fn files_turn_range_excludes_later_bash() {
let h = files_scenario_home();
let out = h.run(&["files", at(SESS).as_str(), "--turn", "0..0"]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(out.stdout.contains("turn=0..0"));
assert!(
out.stdout.contains("5 mutation(s)"),
"turn 1 bash excluded: {}",
out.stdout
);
}
#[test]
fn files_turn_range_and_since_intersect() {
let h = files_scenario_home();
let out = h.run(&[
"files",
at(SESS).as_str(),
"--turn",
"0..1",
"--since",
"2h",
]);
assert!(
out.success,
"combined windows intersect, never error: {}",
out.stderr
);
assert!(
out.stdout.contains("no file mutations found") || !out.stdout.contains("L0"),
"the intersected window filters: {}",
out.stdout
);
}
#[test]
fn files_since_window_keeps_only_later_mutations() {
let h = files_scenario_home();
let out = h.run(&[
"files",
at(SESS).as_str(),
"--since",
"2026-06-07T06:00:00Z",
]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(out.stdout.contains("1 mutation(s)"), "got: {}", out.stdout);
assert!(out.stdout.contains("bash (heuristic)"));
}
#[test]
fn files_no_mutations_says_none() {
let h = Home::new();
h.write(
&format!("{ENC}/{SESS}.jsonl"),
concat!(
r#"{"type":"user","uuid":"u0","message":{"role":"user","content":"just chatting"}}"#, "\n",
r#"{"type":"assistant","uuid":"a0","message":{"role":"assistant","content":[{"type":"text","text":"sure"}]}}"#, "\n",
),
);
let out = h.run(&["files", at(SESS).as_str(), "--no-subagents"]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(
out.stdout.contains("no file mutations found"),
"got: {}",
out.stdout
);
}
#[test]
fn files_detects_edit_before_read_boundaries() {
let h = Home::new();
h.write(
&format!("{ENC}/{SESS}.jsonl"),
concat!(
r#"{"type":"user","uuid":"u0","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"go"}}"#, "\n",
r#"{"type":"assistant","uuid":"a0","timestamp":"2026-06-07T05:00:01.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"w1","name":"Write","input":{"file_path":"/p/app.rs","content":"line\n"}}]}}"#, "\n",
r#"{"type":"user","uuid":"c0","timestamp":"2026-06-07T05:00:01.500Z","toolUseResult":{"type":"create","filePath":"/p/app.rs"},"message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"w1","content":"ok"}]}}"#, "\n",
r#"{"type":"assistant","uuid":"a1","timestamp":"2026-06-07T05:00:02.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"ed1","name":"Edit","input":{"file_path":"/p/app.rs","old_string":"line","new_string":"LINE"}}]}}"#, "\n",
r#"{"type":"user","uuid":"err1","timestamp":"2026-06-07T05:00:03.000Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"ed1","is_error":true,"content":"<tool_use_error>File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.</tool_use_error>"}]}}"#, "\n",
),
);
let out = h.run(&[
"files",
at(SESS).as_str(),
"--no-subagents",
"--by",
"timeline",
]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(
out.stdout.contains("Edit-before-Read boundaries"),
"boundary section present: {}",
out.stdout
);
assert!(
out.stdout.contains("/p/app.rs") && out.stdout.contains("modified_since_read"),
"boundary attributed to the file with its kind: {}",
out.stdout
);
assert!(
out.stdout.contains("1 Edit-before-Read boundary(ies)"),
"footer boundary count: {}",
out.stdout
);
let j = h.run(&[
"files",
at(SESS).as_str(),
"--no-subagents",
"--format",
"json",
]);
assert!(j.success, "stderr: {}", j.stderr);
let objs: Vec<serde_json::Value> = j
.stdout
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| serde_json::from_str(l).expect("ndjson parses"))
.collect();
let b = objs
.iter()
.find(|o| o.get("kind").and_then(|t| t.as_str()) == Some("boundary"))
.expect("a boundary object");
assert_eq!(b["path"], "/p/app.rs");
assert_eq!(b["cause"], "modified_since_read");
assert!(
b["line"].as_u64().unwrap_or(0) >= 1,
"boundary carries its jsonl line: {b}"
);
let summary = objs
.iter()
.find(|o| o.get("detail_level").is_some())
.expect("trailing summary");
assert_eq!(summary["edit_before_read_boundaries"], serde_json::json!(1));
}
#[test]
fn files_regex_filters_full_path() {
let h = Home::new();
path_filter_scenario(&h);
let out = h.run(&[
"files",
at(SESS).as_str(),
"--no-subagents",
"--by",
"file",
"--regex",
r"\.rs$",
]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(
out.stdout.contains("src/lib.rs"),
"kept .rs: {}",
out.stdout
);
assert!(
!out.stdout.contains("readme.md") && !out.stdout.contains("notes.txt"),
"regex must drop non-.rs paths: {}",
out.stdout
);
assert!(
out.stdout.contains("Edit-before-Read boundaries"),
"the .rs boundary must survive the filter: {}",
out.stdout
);
}
#[test]
fn files_glob_filters_full_path() {
let h = Home::new();
path_filter_scenario(&h);
let out = h.run(&[
"files",
at(SESS).as_str(),
"--no-subagents",
"--by",
"file",
"--glob",
"**/*.md",
]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(out.stdout.contains("readme.md"), "kept .md: {}", out.stdout);
assert!(
!out.stdout.contains("lib.rs") && !out.stdout.contains("notes.txt"),
"glob must drop non-.md paths: {}",
out.stdout
);
assert!(
!out.stdout.contains("Edit-before-Read boundaries"),
"a filtered-out boundary must not show: {}",
out.stdout
);
}
#[test]
fn files_regex_and_glob_combine_as_and() {
let h = Home::new();
path_filter_scenario(&h);
let both = h.run(&[
"files",
at(SESS).as_str(),
"--no-subagents",
"--by",
"file",
"--glob",
"**/src/**",
"--regex",
r"\.rs$",
]);
assert!(both.success, "stderr: {}", both.stderr);
assert!(both.stdout.contains("src/lib.rs"), "got: {}", both.stdout);
assert!(
!both.stdout.contains("readme.md") && !both.stdout.contains("notes.txt"),
"AND of glob+regex: {}",
both.stdout
);
let empty = h.run(&[
"files",
at(SESS).as_str(),
"--no-subagents",
"--by",
"file",
"--glob",
"**/src/**",
"--regex",
r"\.md$",
]);
assert!(empty.success, "stderr: {}", empty.stderr);
assert!(
empty.stdout.contains("no file mutations found"),
"an empty filtered set yields the empty output: {}",
empty.stdout
);
}
#[test]
fn files_invalid_regex_and_glob_are_hard_errors() {
let h = Home::new();
path_filter_scenario(&h);
let bad_re = h.run(&["files", at(SESS).as_str(), "--regex", "("]);
assert!(
!bad_re.success,
"invalid regex must fail: {}",
bad_re.stdout
);
assert!(
bad_re.stderr.contains("invalid --regex"),
"regex error names the flag: {}",
bad_re.stderr
);
let bad_glob = h.run(&["files", at(SESS).as_str(), "--glob", "[abc"]);
assert!(
!bad_glob.success,
"invalid glob must fail: {}",
bad_glob.stdout
);
assert!(
bad_glob.stderr.contains("invalid --glob"),
"glob error names the flag: {}",
bad_glob.stderr
);
}
#[test]
fn files_detects_new_bash_idioms_end_to_end() {
let h = Home::new();
let cmds = [
("pytest 2>/tmp/err.log", "/tmp/err.log"),
("make 1> /tmp/out.log", "/tmp/out.log"),
("svc &>/tmp/all.log", "/tmp/all.log"),
("curl https://x -o /tmp/dl.json", "/tmp/dl.json"),
("wget -O /tmp/w.bin https://y", "/tmp/w.bin"),
("pytest --junit-xml=/tmp/r.xml", "/tmp/r.xml"),
("dd if=/dev/zero of=/tmp/d.bin", "/tmp/d.bin"),
("zip /tmp/a.zip f1 f2", "/tmp/a.zip"),
];
let mut lines = String::new();
lines.push_str(
r#"{"type":"user","uuid":"u0","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"go"}}"#,
);
lines.push('\n');
for (n, (cmd, _)) in cmds.iter().enumerate() {
lines.push_str(&format!(
r#"{{"type":"assistant","uuid":"a{n}","timestamp":"2026-06-07T05:00:0{n}.000Z","message":{{"role":"assistant","content":[{{"type":"tool_use","id":"b{n}","name":"Bash","input":{{"command":{}}}}}]}}}}"#,
serde_json_string(cmd)
));
lines.push('\n');
}
lines.push_str(
r#"{"type":"assistant","uuid":"az","timestamp":"2026-06-07T05:00:09.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"bz","name":"Bash","input":{"command":"noisy 2>/dev/null > $OUT"}}]}}"#,
);
lines.push('\n');
h.write(&format!("{ENC}/{SESS}.jsonl"), &lines);
let out = h.run(&["files", at(SESS).as_str(), "--by", "file", "--no-subagents"]);
assert!(out.success, "stderr: {}", out.stderr);
for (cmd, want) in cmds {
assert!(
out.stdout.contains(want),
"idiom {cmd:?} should surface {want}: {}",
out.stdout
);
}
assert!(!out.stdout.contains("/dev/null"), "got: {}", out.stdout);
assert!(!out.stdout.contains("$OUT"), "got: {}", out.stdout);
}
#[test]
fn files_unknown_session_errors() {
let h = files_scenario_home();
let out = h.run(&["files", at("00000000-0000-0000-0000-000000000000").as_str()]);
assert!(!out.success);
assert!(
out.stderr.contains("no session file found"),
"stderr: {}",
out.stderr
);
}
#[test]
fn files_via_project_path_target() {
let h = files_scenario_home();
let out = h.run(&["files", ENC, "--no-subagents"]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(out.stdout.contains("/tmp: 2 write"));
}