use crate::harness::*;
const XSESS: &str = "beef5678-1234-4abc-8def-998877665544";
#[test]
fn settings_family_external_write_surfaces_and_others_do_not() {
let h = Home::new();
h.write(
&format!("{ENC}/{XSESS}.jsonl"),
concat!(
r#"{"type":"user","uuid":"u0","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"tune the config"}}"#, "\n",
r#"{"type":"assistant","uuid":"a0","timestamp":"2026-06-07T05:00:01.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"e1","name":"Edit","input":{"file_path":"/Users/dev/.claude/settings.json","old_string":"a","new_string":"b"}}]}}"#, "\n",
r#"{"type":"user","uuid":"r1","timestamp":"2026-06-07T05:00:02.000Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"e1","content":"ok"}]}}"#, "\n",
r#"{"type":"file-history-snapshot","messageId":"m1","snapshot":{"messageId":"m1","timestamp":"2026-06-07T05:01:00.000Z","trackedFileBackups":{"/Users/dev/.claude/settings.json":{"backupFileName":"aa@v1","version":1,"backupTime":"2026-06-07T05:01:00.000Z"},"/Users/dev/proj/notes.md":{"backupFileName":"bb@v1","version":1,"backupTime":"2026-06-07T05:01:00.000Z"}}},"isSnapshotUpdate":false}"#, "\n",
r#"{"type":"user","uuid":"u1","timestamp":"2026-06-07T05:05:00.000Z","message":{"role":"user","content":"carry on"}}"#, "\n",
r#"{"type":"file-history-snapshot","messageId":"m2","snapshot":{"messageId":"m2","timestamp":"2026-06-07T05:06:00.000Z","trackedFileBackups":{"/Users/dev/.claude/settings.json":{"backupFileName":"aa@v2","version":2,"backupTime":"2026-06-07T05:06:00.000Z"},"/Users/dev/proj/notes.md":{"backupFileName":"bb@v2","version":2,"backupTime":"2026-06-07T05:06:00.000Z"}}},"isSnapshotUpdate":false}"#, "\n",
),
);
let out = h.run(&["files", at(XSESS).as_str(), "--by", "timeline"]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(
out.stdout
.contains("external write /Users/dev/.claude/settings.json"),
"the settings silent write surfaces:\n{}",
out.stdout
);
assert!(
out.stdout.contains("file-history v1->v2") && out.stdout.contains("no tool record"),
"the instrument detail rides the row:\n{}",
out.stdout
);
assert!(
!out.stdout
.contains("external write /Users/dev/proj/notes.md"),
"a non-settings tracked path NEVER surfaces (the scope law):\n{}",
out.stdout
);
let j = h.run(&[
"files",
at(XSESS).as_str(),
"--by",
"timeline",
"--format",
"json",
]);
let row = j
.stdout
.lines()
.filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
.find(|v| v["op"] == "external_write")
.expect("external_write row");
assert_eq!(
row["path"], "/Users/dev/.claude/settings.json",
"{}",
j.stdout
);
assert_eq!(row["heuristic"], false, "{}", j.stdout);
assert!(
row["detail"].as_str().unwrap().contains("v1->v2"),
"{}",
j.stdout
);
}
#[test]
fn a_tool_write_in_the_interval_explains_the_jump() {
let h = Home::new();
h.write(
&format!("{ENC}/{XSESS}.jsonl"),
concat!(
r#"{"type":"user","uuid":"u0","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"tune"}}"#, "\n",
r#"{"type":"file-history-snapshot","messageId":"m1","snapshot":{"messageId":"m1","timestamp":"2026-06-07T05:01:00.000Z","trackedFileBackups":{"/Users/dev/.claude/settings.json":{"backupFileName":"aa@v1","version":1,"backupTime":"2026-06-07T05:01:00.000Z"}}},"isSnapshotUpdate":false}"#, "\n",
r#"{"type":"assistant","uuid":"a0","timestamp":"2026-06-07T05:02:00.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"e1","name":"Edit","input":{"file_path":"/Users/dev/.claude/settings.json","old_string":"a","new_string":"b"}}]}}"#, "\n",
r#"{"type":"user","uuid":"r1","timestamp":"2026-06-07T05:02:02.000Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"e1","content":"ok"}]}}"#, "\n",
r#"{"type":"file-history-snapshot","messageId":"m2","snapshot":{"messageId":"m2","timestamp":"2026-06-07T05:03:00.000Z","trackedFileBackups":{"/Users/dev/.claude/settings.json":{"backupFileName":"aa@v2","version":2,"backupTime":"2026-06-07T05:03:00.000Z"}}},"isSnapshotUpdate":false}"#, "\n",
),
);
let out = h.run(&["files", at(XSESS).as_str(), "--by", "timeline"]);
assert!(
!out.stdout.contains("external write"),
"an explained jump is not external:\n{}",
out.stdout
);
}
#[test]
fn family_gate_same_version_and_cross_spelling_pins() {
let h = Home::new();
h.write(
&format!("{ENC}/{XSESS}.jsonl"),
concat!(
r#"{"type":"user","uuid":"u0","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"tune"}}"#, "\n",
r#"{"type":"assistant","uuid":"a0","timestamp":"2026-06-07T05:00:30.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"e1","name":"Edit","input":{"file_path":"/Users/dev/.claude/settings.json","old_string":"a","new_string":"b"}}]}}"#, "\n",
r#"{"type":"user","uuid":"r1","timestamp":"2026-06-07T05:00:31.000Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"e1","content":"ok"}]}}"#, "\n",
r#"{"type":"file-history-snapshot","messageId":"m1","snapshot":{"messageId":"m1","timestamp":"2026-06-07T05:01:00.000Z","trackedFileBackups":{".claude/settings.json":{"backupFileName":"aa@v1","version":1,"backupTime":"2026-06-07T05:01:00.000Z"},".claude/CLAUDE.md":{"backupFileName":"cc@v1","version":1,"backupTime":"2026-06-07T05:01:00.000Z"}}},"isSnapshotUpdate":false}"#, "\n",
r#"{"type":"assistant","uuid":"a1","timestamp":"2026-06-07T05:02:00.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"e2","name":"Edit","input":{"file_path":"/Users/dev/.claude/settings.json","old_string":"b","new_string":"c"}}]}}"#, "\n",
r#"{"type":"user","uuid":"r2","timestamp":"2026-06-07T05:02:01.000Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"e2","content":"ok"}]}}"#, "\n",
r#"{"type":"file-history-snapshot","messageId":"m2","snapshot":{"messageId":"m2","timestamp":"2026-06-07T05:03:00.000Z","trackedFileBackups":{".claude/settings.json":{"backupFileName":"aa@v2","version":2,"backupTime":"2026-06-07T05:03:00.000Z"},".claude/CLAUDE.md":{"backupFileName":"cc@v2","version":2,"backupTime":"2026-06-07T05:03:00.000Z"}}},"isSnapshotUpdate":false}"#, "\n",
r#"{"type":"file-history-snapshot","messageId":"m3","snapshot":{"messageId":"m3","timestamp":"2026-06-07T05:04:00.000Z","trackedFileBackups":{".claude/settings.json":{"backupFileName":"aa@v2","version":2,"backupTime":"2026-06-07T05:03:00.000Z"}}},"isSnapshotUpdate":false}"#, "\n",
),
);
let out = h.run(&["files", at(XSESS).as_str(), "--by", "timeline"]);
assert!(
!out.stdout.contains("external write"),
"explained cross-spelling jump + non-settings path + same-version repeat all \
stay silent:\n{}",
out.stdout
);
}
#[test]
fn rollup_counts_and_labels_carry_the_external_write() {
let h = Home::new();
h.write(
&format!("{ENC}/{XSESS}.jsonl"),
concat!(
r#"{"type":"user","uuid":"u0","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"tune"}}"#, "\n",
r#"{"type":"file-history-snapshot","messageId":"m1","snapshot":{"messageId":"m1","timestamp":"2026-06-07T05:01:00.000Z","trackedFileBackups":{"/Users/dev/.claude/settings.json":{"backupFileName":"aa@v1","version":1,"backupTime":"2026-06-07T05:01:00.000Z"}}},"isSnapshotUpdate":false}"#, "\n",
r#"{"type":"user","uuid":"u1","timestamp":"2026-06-07T05:05:00.000Z","message":{"role":"user","content":"more"}}"#, "\n",
r#"{"type":"file-history-snapshot","messageId":"m2","snapshot":{"messageId":"m2","timestamp":"2026-06-07T05:06:00.000Z","trackedFileBackups":{"/Users/dev/.claude/settings.json":{"backupFileName":"aa@v2","version":2,"backupTime":"2026-06-07T05:06:00.000Z"}}},"isSnapshotUpdate":false}"#, "\n",
),
);
let out = h.run(&["files", at(XSESS).as_str(), "--by", "file"]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(
out.stdout.contains("1 external write (inferred)"),
"the per-file op label counts the external write exactly once:\n{}",
out.stdout
);
}