use crate::harness::*;
#[test]
fn range_open_and_negative_forms() {
let h = populated_home();
let t = at(SESS);
let count = |spec: &str| -> String {
let out = h.run(&[
"search",
"",
t.as_str(),
"--no-subagents",
"--turn",
spec,
"-c",
]);
assert!(out.success, "turn {spec:?} stderr: {}", out.stderr);
out.stdout.trim().to_string()
};
assert_eq!(count("0..0"), "1", "turn 0 only");
assert_eq!(count("1.."), "1", "open end: turn 1 → last");
assert_eq!(count("..0"), "1", "open start: first → turn 0");
assert_eq!(count("-1.."), "1", "from-end: the last 1 turn");
assert_eq!(count("-2.."), "2", "from-end: the last 2 turns = both");
let rev = h.run(&["search", "", t.as_str(), "--turn", "9..3", "-c"]);
assert!(!rev.success, "a reversed closed range must error");
let raw = h.run(&["show", t.as_str(), "--line", "-1..", "--raw"]);
assert!(raw.success, "stderr: {}", raw.stderr);
assert!(
raw.stdout.contains("broken json"),
"last line via -1..: {}",
raw.stdout
);
}
#[test]
fn range_grammar_is_n_or_dotdot_everywhere() {
let (h, sess, _hex) = show_subagent_home();
let ok = h.run(&["show", &format!("@{sess}"), "--line", "1..2"]);
assert!(ok.success, "stderr: {}", ok.stderr);
assert!(ok.stdout.contains("go"), "record in span: {}", ok.stdout);
let dash = h.run(&["show", &format!("@{sess}"), "--line", "1-2"]);
assert!(
!dash.success,
"dash ranges must hard-error: {}",
dash.stdout
);
assert!(
dash.stderr.contains("START..END"),
"the error teaches the ..-form: {}",
dash.stderr
);
let bare = h.run(&["search", "go", &format!("@{sess}"), "--turn", "0"]);
assert!(bare.success, "stderr: {}", bare.stderr);
assert!(
bare.stdout.contains("go"),
"turn 0 matched via the bare-N shorthand: {}",
bare.stdout
);
let tdash = h.run(&["search", "go", &format!("@{sess}"), "--turn", "0-1"]);
assert!(!tdash.success);
assert!(tdash.stderr.contains("START..END"), "got: {}", tdash.stderr);
}
#[test]
fn turn_range_old_spelling_hard_errors() {
let h = populated_home();
let out = h.run(&["search", "go", ENC, "--turn-range", "0"]);
assert!(
!out.success,
"old spelling must hard-error:\n{}",
out.stdout
);
assert!(
out.stderr.contains("--turn-range"),
"names the offending token: {}",
out.stderr
);
assert!(
out.stderr.contains("'--turn'"),
"the tip names the new spelling: {}",
out.stderr
);
}
#[test]
fn timestamps_canonical_local_marker_everywhere() {
let h = populated_home();
let at_s = at(SESS);
let tz_syd = [("TZ", "Australia/Sydney")];
for cmd in [
vec!["list", at_s.as_str(), "--no-subagents"],
vec!["stats", at_s.as_str(), "--no-subagents"],
vec!["search", "carry", at_s.as_str(), "--no-subagents"],
vec!["show", at_s.as_str(), "--turn", "0"],
] {
let out = h.run_with_env(&cmd, &tz_syd);
assert!(out.success, "{cmd:?} stderr: {}", out.stderr);
assert!(
out.stdout.contains("AEST(UTC+10)"),
"{cmd:?} missing the canonical marker:\n{}",
out.stdout
);
assert!(
!out.stdout.contains("Z)"),
"{cmd:?} must carry no raw-UTC copy:\n{}",
out.stdout
);
}
let jan = "77777777-8888-4999-8aaa-bbbbccccdddd";
h.write(
&format!("{ENC}/{jan}.jsonl"),
concat!(
r#"{"type":"user","uuid":"j1","sessionId":"77777777-8888-4999-8aaa-bbbbccccdddd","timestamp":"2026-01-15T05:00:00.000Z","message":{"role":"user","content":"summer question"}}"#,
"\n",
r#"{"type":"assistant","uuid":"j2","sessionId":"77777777-8888-4999-8aaa-bbbbccccdddd","timestamp":"2026-01-15T05:00:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"summer answer"}]}}"#,
"\n",
),
);
let out = h.run_with_env(&["list", &format!("@{jan}")], &tz_syd);
assert!(
out.stdout.contains("AEDT(UTC+11)"),
"January in Sydney is AEDT(UTC+11):\n{}",
out.stdout
);
let out = h.run_with_env(
&["list", at_s.as_str(), "--no-subagents"],
&[("TZ", "Asia/Kolkata")],
);
assert!(
out.stdout.contains("IST(UTC+05:30)"),
"Indian zone renders IST(UTC+05:30):\n{}",
out.stdout
);
}
#[test]
fn time_window_bare_datetime_is_local_wall_clock_not_midnight() {
let h = Home::new();
let enc = "-Users-test-Projects-tw";
let sess = "cccccccc-dddd-4eee-8fff-000000000000";
let body = concat!(
r#"{"type":"user","uuid":"u1","sessionId":"cccccccc-dddd-4eee-8fff-000000000000","cwd":"/Users/test/Projects/tw","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"afternoon message"}}"#,
"\n",
r#"{"type":"assistant","uuid":"a1","parentUuid":"u1","timestamp":"2026-06-07T05:00:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"reply one"}]}}"#,
"\n",
r#"{"type":"user","uuid":"u2","sessionId":"cccccccc-dddd-4eee-8fff-000000000000","timestamp":"2026-06-07T09:00:00.000Z","message":{"role":"user","content":"evening message"}}"#,
"\n",
r#"{"type":"assistant","uuid":"a2","parentUuid":"u2","timestamp":"2026-06-07T09:00:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"reply two"}]}}"#,
"\n",
);
h.write(&format!("{enc}/{sess}.jsonl"), body);
let tz = [("TZ", "Australia/Sydney")];
let count = |since: &str| -> String {
let out = h.run_with_env(
&["search", "", &format!("@{sess}"), "--since", since, "-c"],
&tz,
);
assert!(out.success, "since={since} stderr: {}", out.stderr);
out.stdout.trim().to_string()
};
assert_eq!(count("2026-06-07"), "2");
assert_eq!(count("2026-06-07T16:00:00"), "1");
assert_eq!(count("2026-06-07T20:00:00"), "0");
let bad = h.run_with_env(
&[
"search",
"",
&format!("@{sess}"),
"--since",
"2026-06-07T16:00:00+99:00",
],
&tz,
);
assert!(!bad.success, "malformed offset must hard-error");
}
#[test]
fn malformed_non_candidate_lines_are_counted_never_invisible() {
let h = Home::new();
let enc = "-Users-test-Projects-corrupt";
let sess = "dddddddd-eeee-4fff-8000-111111111111";
let body = concat!(
r#"{"type":"user","uuid":"u1","sessionId":"dddddddd-eeee-4fff-8000-111111111111","cwd":"/Users/test/Projects/corrupt","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"only real record"}}"#,
"\n",
"THIS IS COMPLETE GARBAGE NOT JSON AT ALL !!!",
"\n",
r#"{"type":"assistant","uuid":"a1","timestamp":"2026-06-07T05:00:05.000Z","message":{"role":"assistant","content":[{"type":"te"#,
"\n",
"\n", );
h.write(&format!("{enc}/{sess}.jsonl"), body);
let at = format!("@{sess}");
for args in [
vec!["search", "", at.as_str(), "--no-subagents"],
vec!["list", at.as_str(), "--no-subagents"],
vec!["show", at.as_str(), "--turn", ".."],
vec!["stats", at.as_str(), "--no-subagents"],
] {
let mut a = args.clone();
a.extend(["--format", "json"]);
let out = h.run(&a);
assert!(out.success, "{args:?} stderr: {}", out.stderr);
assert_eq!(
json_summary(&out.stdout)["skipped_lines"],
2,
"{args:?} must count BOTH corrupt lines: {}",
out.stdout
);
}
let t = h.run(&["search", "", &at, "--no-subagents"]);
assert!(
format!("{}{}", t.stdout, t.stderr).contains("2 malformed line(s) skipped"),
"text note missing: {} ||| {}",
t.stdout,
t.stderr
);
}
#[test]
fn reserialized_spaced_json_records_are_full_citizens() {
let h = Home::new();
let enc = "-Users-test-Projects-spaced";
let sess = "eeeeeeee-aaaa-4bbb-8ccc-00000005aced";
h.write(
&format!("{enc}/{sess}.jsonl"),
concat!(
r#"{"type": "user", "uuid": "u1", "timestamp": "2026-06-07T05:00:00.000Z", "message": {"role": "user", "content": "SPACED_ALPHA question"}}"#,
"\n",
r#"{"type": "assistant", "uuid": "a1", "timestamp": "2026-06-07T05:00:01.000Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "SPACED_BETA answer"}]}}"#,
"\n",
),
);
let at = format!("@{sess}");
let s = h.run(&["search", "SPACED_BETA", &at, "--no-subagents", "-c"]);
assert!(s.success, "stderr: {}", s.stderr);
assert_eq!(
s.stdout.trim(),
"1",
"a spaced record must match: {}",
s.stdout
);
let st = h.run(&["stats", &at, "--no-subagents", "--format", "json"]);
assert!(st.success, "stderr: {}", st.stderr);
let row = &json_rows(&st.stdout, "session")[0];
assert_eq!(row["user_records"], 1, "{}", st.stdout);
assert_eq!(row["assistant_records"], 1, "{}", st.stdout);
let l = h.run(&["list", &at, "--no-subagents", "--format", "json"]);
assert!(l.success, "stderr: {}", l.stderr);
let lr = &json_rows(&l.stdout, "session")[0];
assert!(
lr["first_user"]["excerpt"]
.as_str()
.is_some_and(|e| e.contains("SPACED_ALPHA")),
"{}",
l.stdout
);
assert!(
lr["last_agent"]["excerpt"]
.as_str()
.is_some_and(|e| e.contains("SPACED_BETA")),
"{}",
l.stdout
);
assert_eq!(json_summary(&l.stdout)["skipped_lines"], 0, "{}", l.stdout);
}
#[test]
fn clean_run_notes_are_absent_in_terminal_modes() {
let h = Home::new();
let _ = header_collision_scenario(&h); let l = h.run(&["search", "SEEDWORD", "-l"]);
assert!(l.success, "stderr: {}", l.stderr);
assert!(
!l.stderr.contains("note:"),
"-l prints no notes on a clean uncapped run: {}",
l.stderr
);
let cb = h.run(&["search", "SEEDWORD", "--count-by", "label"]);
assert!(cb.success, "stderr: {}", cb.stderr);
assert!(
!cb.stderr.contains("dropped") && !cb.stderr.contains("malformed"),
"--count-by prints no drop/malformed notes on a clean run: {}",
cb.stderr
);
let raw = h.run(&["search", "SEEDWORD", "--raw"]);
assert!(raw.success, "stderr: {}", raw.stderr);
assert!(
!raw.stderr.contains("note:"),
"--raw prints no notes on a clean uncapped run: {}",
raw.stderr
);
}
#[test]
fn empty_out_never_clobbers_or_lies() {
let h = populated_home();
let scratch = h.root.join("precious.md");
let seed = "PRECIOUS USER CONTENT\n";
std::fs::write(&scratch, seed).unwrap();
let out = h.run(&[
"recover",
at(SESS).as_str(),
"--file",
"/tmp/no_such_file_xyz.md",
"--patches",
"--out",
scratch.to_str().unwrap(),
]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(
!out.stdout.contains("wrote concatenated patches"),
"false write line:\n{}",
out.stdout
);
assert!(
out.stderr.contains("left untouched"),
"missing untouched note:\n{}",
out.stderr
);
assert_eq!(
std::fs::read_to_string(&scratch).unwrap(),
seed,
"recover --patches clobbered --out"
);
std::fs::write(&scratch, seed).unwrap();
let out = h.run(&[
"recover",
at(SESS).as_str(),
"--file",
"/tmp/no_such_file_xyz.md",
"--at",
"1w",
"--out",
scratch.to_str().unwrap(),
]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(
!out.stdout.contains("wrote partial snapshot"),
"false write line:\n{}",
out.stdout
);
assert_eq!(
std::fs::read_to_string(&scratch).unwrap(),
seed,
"recover --at clobbered --out"
);
std::fs::write(&scratch, seed).unwrap();
let out = h.run(&[
"verbatim",
at(SESS).as_str(),
"--budget",
"5",
"--out",
scratch.to_str().unwrap(),
]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(
!out.stdout.contains("wrote full reconstruction"),
"false write line:\n{}",
out.stdout
);
assert_eq!(
std::fs::read_to_string(&scratch).unwrap(),
seed,
"turns clobbered --out"
);
std::fs::write(&scratch, seed).unwrap();
let out = h.run(&[
"verbatim",
at(SESS).as_str(),
"--out",
scratch.to_str().unwrap(),
]);
assert!(out.success, "stderr: {}", out.stderr);
assert!(
out.stdout.contains("wrote full reconstruction"),
"real write missing:\n{}",
out.stdout
);
let written = std::fs::read_to_string(&scratch).unwrap();
assert_ne!(
written, seed,
"a non-empty turns reconstruction must overwrite --out"
);
assert!(!written.is_empty(), "written artifact is empty");
}