use super::*;
#[test]
fn line_is_turn_candidate_superset_of_assistant_text() {
let pure_asst = br#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"just prose, no tools"}]}}"#;
assert!(line_is_turn_candidate(pure_asst));
let user = br#"{"type":"user","message":{"role":"user","content":"hi"}}"#;
assert!(line_is_turn_candidate(user));
let summary =
br#"{"type":"user","isCompactSummary":true,"message":{"role":"user","content":"s"}}"#;
assert!(line_is_turn_candidate(summary));
let sys = br#"{"type":"system","subtype":"turn_duration","durationMs":5}"#;
assert!(!line_is_turn_candidate(sys));
}
#[test]
fn window_admits_turn_range_and_time() {
let tr = Some((2usize, 5usize));
let unbounded = TimeWindow::default();
assert!(window_admits(
3,
Some("2026-06-07T05:00:00Z"),
tr,
&unbounded
));
assert!(!window_admits(
1,
Some("2026-06-07T05:00:00Z"),
tr,
&unbounded
));
assert!(!window_admits(6, None, tr, &unbounded));
let bounded = TimeWindow::from_args(Some("2026-06-01"), None).unwrap();
assert!(!window_admits(0, None, None, &bounded));
}
#[test]
fn parse_turn_range_parses_and_rejects() {
assert_eq!(
parse_turn_range("2..5").unwrap().resolve(100, false),
(2, 5)
);
assert!(parse_turn_range("5..2").is_err());
assert!(parse_turn_range("noformat").is_err());
assert!(parse_turn_range("a..b").is_err());
}
#[test]
fn line_is_turn_candidate_each_arm_in_isolation() {
assert!(line_is_turn_candidate(br#"x "role":"user" x"#)); assert!(line_is_turn_candidate(br#"x "role":"assistant" x"#)); assert!(line_is_turn_candidate(br#"x "type":"assistant" x"#)); assert!(line_is_turn_candidate(b"x isCompactSummary x")); assert!(line_is_turn_candidate(b"x tool_use x")); assert!(!line_is_turn_candidate(
b"{\"type\":\"system\",\"subtype\":\"x\"}"
));
assert!(!line_is_turn_candidate(b""));
}
#[test]
fn parse_turn_range_valid_range_hi_ge_lo() {
assert_eq!(
parse_turn_range("0..0").unwrap().resolve(100, false),
(0, 0)
);
assert_eq!(
parse_turn_range("10..20").unwrap().resolve(100, false),
(10, 20)
);
}
#[test]
fn shared_uuid_validator_distinguishes_uuid_from_bare_hex() {
assert!(crate::path::is_session_uuid(
"0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d"
));
assert!(!crate::path::is_session_uuid("a00ea52f023afd9ce"));
assert!(!crate::path::is_session_uuid("0a1b2c3d-4e5f-4a6b-8c7d"));
assert!(!crate::path::is_session_uuid(
"zzzzzzzz-4e5f-4a6b-8c7d-9e0f1a2b3c4d"
));
}
fn scan_named(session_id: &str) -> ScanResult {
let is_subagent = !crate::path::is_session_uuid(session_id);
ScanResult {
session_id: session_id.to_string(),
is_subagent,
parent_session_id: session_id.to_string(),
turns: vec![mk_turn(0, Some("ask"), Some("reply"), 1, 0)],
summaries: Vec::new(),
skipped_lines: 0,
}
}
#[test]
fn scope_summary_counts_top_level_and_subagents() {
let sessions = vec![
scan_named("0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d"),
scan_named("a00ea52f023afd9ce"),
scan_named("a01086fb826e1ab0e"),
];
let plans: Vec<SessionPlan> = sessions
.iter()
.map(|sr| plan_session(sr, 8000, 0.5, 0, &cfg()))
.collect();
let sc = scope_summary(&sessions, &plans);
assert_eq!(sc.in_scope, 3);
assert_eq!(sc.in_scope_top, 1);
assert_eq!(sc.in_scope_sub, 2);
assert_eq!(sc.rendered, 3);
}
#[test]
fn scope_summary_reports_true_scope_not_rendered() {
let mut empty = scan_named("0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d");
empty.turns.clear(); let rendered_sub = scan_named("a00ea52f023afd9ce");
let sessions = vec![empty, rendered_sub];
let plans: Vec<SessionPlan> = sessions
.iter()
.map(|sr| plan_session(sr, 8000, 0.5, 0, &cfg()))
.collect();
let sc = scope_summary(&sessions, &plans);
assert_eq!(sc.in_scope, 2);
assert_eq!(
sc.in_scope_top, 1,
"targeted top-level must NOT read as 0 top-level"
);
assert_eq!(sc.in_scope_sub, 1);
assert_eq!(sc.rendered, 1, "only the subagent fit the budget");
}
#[test]
fn slice_windows_concatenate_back_to_the_source() {
let doc = "line one\nline two\nthree\nfour five six\n";
let chunks = slice_into_windows(doc, 12);
assert_eq!(chunks.concat(), doc, "lossless reassembly across slices");
for c in &chunks {
assert!(c.chars().count() <= 12, "chunk over window: {c:?}");
}
assert!(chunks.len() > 1, "doc spans multiple windows");
}
#[test]
fn slice_windows_count_chars_not_bytes() {
let line = "🛠🛠🛠🛠🛠\n"; let chunks = slice_into_windows(line, 6);
assert_eq!(
chunks.len(),
1,
"6 chars fit one 6-char window despite 21 bytes"
);
assert_eq!(chunks[0], line);
}
#[test]
fn slice_windows_hard_split_an_oversized_line_on_char_boundaries() {
let line = "🛠🛠🛠🛠🛠";
let chunks = slice_into_windows(line, 2);
assert_eq!(chunks.concat(), line, "lossless even when hard-splitting");
for c in &chunks {
assert!(c.chars().count() <= 2);
assert!(c.chars().all(|ch| ch == '🛠'), "no broken char: {c:?}");
}
assert_eq!(chunks, vec!["🛠🛠", "🛠🛠", "🛠"]);
}
#[test]
fn slice_windows_empty_input_yields_no_chunks() {
assert!(slice_into_windows("", 10).is_empty());
}