use super::*;
#[test]
fn compact_summary_body_reads_string_content_only() {
let s = rec(
r#"{"type":"user","isCompactSummary":true,"message":{"role":"user","content":"the summary body text"}}"#,
);
assert_eq!(
compact_summary_body(&s).as_deref(),
Some("the summary body text")
);
let blocks = rec(
r#"{"type":"user","isCompactSummary":true,"message":{"role":"user","content":[{"type":"text","text":"x"}]}}"#,
);
assert!(compact_summary_body(&blocks).is_none());
let bare = rec(r#"{"type":"user","isCompactSummary":true}"#);
assert!(compact_summary_body(&bare).is_none());
}
#[test]
fn raw_body_newlines_counts_user_and_assistant_bodies() {
let u = rec(
r#"{"type":"user","message":{"role":"user","content":"line one\nline two\nline three"}}"#,
);
assert_eq!(raw_body_newlines(&u), 2);
let a = rec(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"first"},{"type":"text","text":"second"}]}}"#,
);
assert_eq!(raw_body_newlines(&a), 1);
let bare = rec(r#"{"type":"system","subtype":"x"}"#);
assert_eq!(raw_body_newlines(&bare), 0);
let nocontent = rec(r#"{"type":"user","message":{"role":"user"}}"#);
assert_eq!(raw_body_newlines(&nocontent), 0);
}
#[test]
fn build_produces_round_trip_with_tool_count_and_compaction() {
let records: Vec<(usize, Record)> = vec![
(
1,
rec(
r#"{"type":"user","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"first ask about café"}}"#,
),
),
(
2,
rec(
r#"{"type":"assistant","timestamp":"2026-06-07T05:00:01.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"t1","name":"Read","input":{}},{"type":"tool_use","id":"t2","name":"Bash","input":{}}]}}"#,
),
),
(
3,
rec(
r#"{"type":"assistant","timestamp":"2026-06-07T05:00:02.000Z","message":{"role":"assistant","content":[{"type":"text","text":"the reply to the first ask"}]}}"#,
),
),
(
4,
rec(
r#"{"type":"user","isCompactSummary":true,"message":{"role":"user","content":"6. All user messages:\n - \"first ask about café\""}}"#,
),
),
(
5,
rec(
r#"{"type":"user","timestamp":"2026-06-07T06:00:00.000Z","message":{"role":"user","content":"second ask after compaction"}}"#,
),
),
(
6,
rec(
r#"{"type":"assistant","timestamp":"2026-06-07T06:00:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"the second reply"}]}}"#,
),
),
];
let (turns, summaries) = build(&records, &[]);
assert_eq!(turns.len(), 2);
assert!(turns[0].is_round_trip());
assert_eq!(turns[0].tool_calls, 2);
assert_eq!(turns[0].compactions_before, 1);
assert_eq!(turns[0].user.as_ref().unwrap().line_no, 1);
assert_eq!(turns[0].assistant_eot().unwrap().line_no, 3);
assert_eq!(turns[1].compactions_before, 0);
assert_eq!(turns[1].tool_calls, 0);
assert_eq!(summaries.len(), 1);
assert_eq!(summaries[0].line_no, 4);
assert!(summaries[0]
.fingerprints
.iter()
.any(|f| f.starts_with("first ask about café")));
}
#[test]
fn build_pure_tool_call_turn_has_no_assistant_eot() {
let records: Vec<(usize, Record)> = vec![
(
1,
rec(r#"{"type":"user","message":{"role":"user","content":"do the thing"}}"#),
),
(
2,
rec(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t","name":"Bash","input":{}}]}}"#,
),
),
];
let (turns, _s) = build(&records, &[]);
assert_eq!(turns.len(), 1);
assert!(turns[0].user.is_some());
assert!(
turns[0].assistant_eot().is_none(),
"pure tool-call turn has no EOT text"
);
assert_eq!(turns[0].tool_calls, 1);
assert!(!turns[0].is_round_trip());
}
#[test]
fn build_orphan_assistant_lead_has_no_user() {
let records: Vec<(usize, Record)> = vec![
(
1,
rec(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"orphan lead reply"}]}}"#,
),
),
(
2,
rec(r#"{"type":"user","message":{"role":"user","content":"the first real ask"}}"#),
),
(
3,
rec(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"real reply"}]}}"#,
),
),
];
let (turns, _s) = build(&records, &[]);
assert_eq!(turns.len(), 1);
assert!(turns[0].user.is_some());
assert!(turns[0].assistant_eot().is_some());
}
#[test]
fn build_summary_with_block_body_is_not_captured() {
let records: Vec<(usize, Record)> = vec![
(
1,
rec(r#"{"type":"user","message":{"role":"user","content":"ask"}}"#),
),
(
2,
rec(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"reply"}]}}"#,
),
),
(
3,
rec(
r#"{"type":"user","isCompactSummary":true,"message":{"role":"user","content":[{"type":"text","text":"block body summary"}]}}"#,
),
),
];
let (turns, summaries) = build(&records, &[]);
assert!(summaries.is_empty(), "block-bodied summary not captured");
assert_eq!(turns[0].compactions_before, 0);
}
#[test]
fn raw_body_newlines_block_with_empty_text_blocks() {
let r = rec(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":" "},{"type":"text","text":"real\nline"}]}}"#,
);
assert_eq!(raw_body_newlines(&r), 1);
let r2 = rec(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t","name":"Bash","input":{}},{"type":"text","text":"a\nb\nc"}]}}"#,
);
assert_eq!(raw_body_newlines(&r2), 2);
}
#[test]
fn build_skips_non_genuine_user_opener() {
let records: Vec<(usize, Record)> = vec![
(
1,
rec(r#"{"type":"user","message":{"role":"user","content":"real opener"}}"#),
),
(
2,
rec(
r#"{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"x","content":"carrier"}]}}"#,
),
),
(
3,
rec(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"reply"}]}}"#,
),
),
];
let (turns, _s) = build(&records, &[]);
assert_eq!(turns.len(), 1);
assert_eq!(turns[0].user.as_ref().unwrap().text, "real opener");
assert!(turns[0].assistant_eot().is_some());
}
#[test]
fn raw_body_newlines_no_message_and_no_content() {
let no_msg = rec(r#"{"type":"system","subtype":"x"}"#);
assert_eq!(raw_body_newlines(&no_msg), 0);
let no_content = rec(r#"{"type":"user","message":{"role":"user"}}"#);
assert_eq!(raw_body_newlines(&no_content), 0);
}
#[test]
fn compact_summary_body_none_when_no_message() {
let bare = rec(r#"{"type":"user","isCompactSummary":true}"#);
assert!(compact_summary_body(&bare).is_none());
}
#[test]
fn build_skips_non_candidate_records_in_scan() {
let records: Vec<(usize, Record)> = vec![
(
1,
rec(r#"{"type":"user","message":{"role":"user","content":"ask"}}"#),
),
(
2,
rec(r#"{"type":"system","subtype":"turn_duration","durationMs":5}"#),
),
(
3,
rec(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"reply"}]}}"#,
),
),
];
let (turns, summaries) = build(&records, &[]);
assert_eq!(turns.len(), 1);
assert!(turns[0].is_round_trip());
assert!(summaries.is_empty());
}