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 body_newline_positions_locates_original_newlines_in_the_rendered_body() {
let u = rec(
r#"{"type":"user","message":{"role":"user","content":"line one\nline two\nline three"}}"#,
);
let text = u.genuine_user_text().expect("genuine body");
assert_eq!(text, "line one line two line three");
assert_eq!(body_newline_positions(&u, &text), vec![8, 17]);
let a = rec(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"first"},{"type":"text","text":"second"}]}}"#,
);
let atext = a.agent_text().expect("agent body");
assert_eq!(atext, "first second");
assert_eq!(body_newline_positions(&a, &atext), vec![5]);
let bare = rec(r#"{"type":"system","subtype":"x"}"#);
assert!(body_newline_positions(&bare, "anything").is_empty());
let nocontent = rec(r#"{"type":"user","message":{"role":"user"}}"#);
assert!(body_newline_positions(&nocontent, "anything").is_empty());
let oneline = rec(r#"{"type":"user","message":{"role":"user","content":"one line only"}}"#);
assert!(body_newline_positions(&oneline, "one line only").is_empty());
}
#[test]
fn body_newline_positions_refuses_a_fabricated_body() {
let r = rec(
r#"{"type":"user","message":{"role":"user","content":"<task-notification>\n<task-id>b1234abcd</task-id>\n<summary>Background command \"build\" completed</summary>\n</task-notification>"}}"#,
);
let label = r.automation_label().expect("an automation label");
assert!(!label.contains("task-id"), "the label is composed: {label}");
assert!(body_newline_positions(&r, &label).is_empty());
assert!(raw_body(&r).expect("a body").contains('\n'));
}
#[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, &view_of(&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, &view_of(&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, &view_of(&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, &view_of(&records), &[]);
assert!(summaries.is_empty(), "block-bodied summary not captured");
assert_eq!(turns[0].compactions_before, 0);
}
#[test]
fn body_newline_positions_block_with_empty_text_blocks() {
let r = rec(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":" "},{"type":"text","text":"real\nline"}]}}"#,
);
let t = r.agent_text().expect("agent body");
assert_eq!(body_newline_positions(&r, &t), vec![4]);
let r2 = rec(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t","name":"Bash","input":{}},{"type":"text","text":"a\nb\nc"}]}}"#,
);
let t2 = r2.agent_text().expect("agent body");
assert_eq!(body_newline_positions(&r2, &t2), vec![1, 3]);
}
#[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, &view_of(&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_none_when_no_message_and_no_content() {
let no_msg = rec(r#"{"type":"system","subtype":"x"}"#);
assert!(raw_body(&no_msg).is_none());
let no_content = rec(r#"{"type":"user","message":{"role":"user"}}"#);
assert!(raw_body(&no_content).is_none());
}
#[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, &view_of(&records), &[]);
assert_eq!(turns.len(), 1);
assert!(turns[0].is_round_trip());
assert!(summaries.is_empty());
}