use super::*;
use std::io::Write as _;
use std::sync::atomic::{AtomicU64, Ordering};
#[test]
fn truncate_short_excerpt_unchanged() {
assert_eq!(truncate_excerpt("hello"), "hello");
}
#[test]
fn truncate_long_excerpt_marks_dropped_count() {
let s = "x".repeat(EXCERPT_MAX + 5);
let out = truncate_excerpt(&s);
assert!(out.ends_with("… (+5 chars)"), "got: {out}");
assert!(out.starts_with(&"x".repeat(EXCERPT_MAX)));
}
#[test]
fn truncate_counts_chars_not_bytes() {
let s = "🛠".repeat(EXCERPT_MAX + 2);
let out = truncate_excerpt(&s);
assert!(out.ends_with("… (+2 chars)"), "got: {out}");
}
#[test]
fn format_timestamp_uses_system_local_canonical_marker() {
let raw = "2026-06-07T05:48:22.880Z";
let out = format_timestamp(Some(raw));
let ts: jiff::Timestamp = raw.parse().expect("parseable instant");
let local = ts
.to_zoned(crate::timez::local_tz())
.strftime("%Y-%m-%d %H:%M:%S")
.to_string();
assert!(out.contains(&local), "expected local {local:?} in {out:?}");
assert!(out.contains("(UTC"), "marker missing: {out}");
assert!(!out.contains(raw), "the raw-UTC copy must be gone: {out}");
}
#[test]
fn format_timestamp_missing_is_em_dash() {
assert_eq!(format_timestamp(None), "—");
}
#[test]
fn format_timestamp_unparseable_surfaces_raw() {
let out = format_timestamp(Some("not-a-time"));
assert!(out.contains("not-a-time"));
assert!(out.contains("unparsed"));
}
#[test]
fn local_iso_matches_system_tz_offset() {
let raw = "2026-06-07T05:48:22.880Z";
let out = local_iso(raw).expect("local iso");
let ts: jiff::Timestamp = raw.parse().expect("parseable instant");
let expected = ts
.to_zoned(crate::timez::local_tz())
.strftime("%Y-%m-%dT%H:%M:%S%:z")
.to_string();
assert_eq!(out, expected);
}
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn tmp_session(name_stem: &str, lines: &[&str]) -> PathBuf {
let p = std::env::temp_dir().join(format!(
"csift-sess-{}-{}-{name_stem}.jsonl",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
));
let mut f = std::fs::File::create(&p).unwrap();
for l in lines {
writeln!(f, "{l}").unwrap();
}
p
}
#[test]
fn capture_identity_if_empty_only_fills_blanks() {
let rec: Record = serde_json::from_str(
r#"{"type":"user","cwd":"/c","version":"9.9","gitBranch":"b","sessionId":"sid","message":{"role":"user","content":"x"}}"#,
)
.unwrap();
let mut cwd = None;
let mut version = Some("keep".to_string()); let mut branch = None;
let mut sid = None;
capture_identity_if_empty(&rec, &mut cwd, &mut version, &mut branch, &mut sid);
assert_eq!(cwd.as_deref(), Some("/c"));
assert_eq!(version.as_deref(), Some("keep"), "pre-set value preserved");
assert_eq!(branch.as_deref(), Some("b"));
assert_eq!(sid.as_deref(), Some("sid"));
}
#[test]
fn summarize_head_first_user_captures_identity() {
let p = tmp_session(
"head",
&[
r#"{"type":"user","cwd":"/Users/testuser/Projects/foo","version":"2.1.0","gitBranch":"main","sessionId":"sid-data","message":{"role":"user","content":"first q"}}"#,
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"mid agent"}]}}"#,
r#"{"type":"user","message":{"role":"user","content":"last q"}}"#,
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"last agent"}]}}"#,
],
);
let s = summarize_session(&p).unwrap();
std::fs::remove_file(&p).ok();
assert_eq!(s.cwd.as_deref(), Some("/Users/testuser/Projects/foo"));
assert_eq!(s.version.as_deref(), Some("2.1.0"));
assert_eq!(s.git_branch.as_deref(), Some("main"));
assert_eq!(s.first_user.as_ref().unwrap().excerpt, "first q");
assert_eq!(s.last_user.as_ref().unwrap().excerpt, "last q");
assert_eq!(s.last_agent.as_ref().unwrap().excerpt, "last agent");
}
#[test]
fn summarize_backfills_identity_from_tail_when_head_user_lacks_it() {
let p = tmp_session(
"tailfill",
&[
r#"{"type":"user","message":{"role":"user","content":"first q, no identity"}}"#,
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"a"}]}}"#,
r#"{"type":"user","cwd":"/tail/cwd","version":"3.0","gitBranch":"dev","sessionId":"sid-tail","message":{"role":"user","content":"last q, has identity"}}"#,
],
);
let s = summarize_session(&p).unwrap();
std::fs::remove_file(&p).ok();
assert_eq!(
s.first_user.as_ref().unwrap().excerpt,
"first q, no identity"
);
assert_eq!(
s.last_user.as_ref().unwrap().excerpt,
"last q, has identity"
);
assert_eq!(s.cwd.as_deref(), Some("/tail/cwd"));
assert_eq!(s.version.as_deref(), Some("3.0"));
assert_eq!(s.git_branch.as_deref(), Some("dev"));
}
#[test]
fn summarize_session_id_from_data_when_filename_has_no_stem() {
let p = tmp_session(
"dataid",
&[r#"{"type":"user","sessionId":"sid-data-xyz","message":{"role":"user","content":"hi"}}"#],
);
let s = summarize_session(&p).unwrap();
std::fs::remove_file(&p).ok();
assert!(!s.session_id.is_empty());
}
#[test]
fn summarize_head_skips_non_genuine_records_before_first_user() {
let p = tmp_session(
"headnoise",
&[
r#"{"type":"last-prompt","leafUuid":"x"}"#,
r#"{"type":"user","isMeta":true,"message":{"role":"user","content":"Continue from where you left off."}}"#,
r#"{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"x","content":"carrier"}]}}"#,
r#"{"type":"user","message":{"role":"user","content":"the genuine first question"}}"#,
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"a"}]}}"#,
],
);
let s = summarize_session(&p).unwrap();
std::fs::remove_file(&p).ok();
assert_eq!(
s.first_user.as_ref().unwrap().excerpt,
"the genuine first question"
);
}
#[test]
fn top_level_summary_is_not_subagent_and_is_its_own_parent() {
let p = tmp_session(
"toplevel",
&[r#"{"type":"user","message":{"role":"user","content":"hi"}}"#],
);
let s = summarize_session(&p).unwrap();
let stem = p.file_stem().unwrap().to_str().unwrap().to_string();
std::fs::remove_file(&p).ok();
assert!(!s.is_subagent);
assert_eq!(s.parent_session_id, stem);
assert_eq!(s.parent_session_id, s.session_id);
}
#[test]
fn subagent_summary_carries_is_subagent_and_refeedable_parent() {
let dir = std::env::temp_dir().join(format!(
"csift-subdir-{}-{}/11111111-2222-3333-4444-555555555555/subagents/workflows/wf_z",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
));
std::fs::create_dir_all(&dir).unwrap();
let p = dir.join("agent-deadbeefcafe1234.jsonl");
{
let mut f = std::fs::File::create(&p).unwrap();
writeln!(
f,
r#"{{"type":"user","message":{{"role":"user","content":"sub work"}}}}"#
)
.unwrap();
}
let s = summarize_session(&p).unwrap();
std::fs::remove_file(&p).ok();
assert_eq!(
s.session_id, "deadbeefcafe1234",
"bare hex (agent- stripped)"
);
assert!(s.is_subagent, "a subagents/ path is a subagent transcript");
assert_eq!(
s.parent_session_id, "11111111-2222-3333-4444-555555555555",
"parent is the re-feedable uuid dir before subagents/"
);
}
#[test]
fn summarize_session_id_is_filename_stem() {
let p = tmp_session(
"stemid",
&[r#"{"type":"user","sessionId":"DATA-ID","message":{"role":"user","content":"hi"}}"#],
);
let s = summarize_session(&p).unwrap();
let stem = p.file_stem().unwrap().to_str().unwrap().to_string();
std::fs::remove_file(&p).ok();
assert_eq!(s.session_id, stem);
assert_ne!(s.session_id, "DATA-ID");
}