use super::*;
#[test]
fn fingerprint_normalizes_lowercases_and_caps_at_80() {
let fp = fingerprint(" Hello WORLD café ");
assert_eq!(fp, "hello world café");
let long = "a".repeat(200);
assert_eq!(fingerprint(&long).chars().count(), 80);
assert_eq!(fingerprint(""), "");
assert_eq!(fingerprint(" "), "");
}
#[test]
fn quoted_inner_extracts_first_quoted_run() {
assert_eq!(
quoted_inner(r#"- "initial greeting about café" (note)"#).as_deref(),
Some("initial greeting about café")
);
assert_eq!(quoted_inner("no quotes here"), None);
assert_eq!(quoted_inner(r#"one " only"#), None); }
#[test]
fn summary_fingerprints_pulls_bullets_and_quotes() {
let body = "\
6. All user messages:
- \"the very first ask about the carry logic\"
- \"second ask: explain the panic path now please\"
9. Optional Next Step:
The assistant said \"I will run the budget walk and report counts\".";
let fps = summary_fingerprints(body);
assert!(fps.iter().any(|f| f.starts_with("the very first ask")));
assert!(fps.iter().any(|f| f.starts_with("second ask")));
assert!(fps.iter().any(|f| f.contains("run the budget walk")));
}
#[test]
fn unit_matches_summary_prefix_either_direction() {
let summary_fps = vec![fingerprint(
"the very first ask about the carry logic in detail",
)];
let short = unit(Role::User, 1, "the very first ask", 0);
assert!(unit_matches_summary(&short, &summary_fps));
let long = unit(
Role::User,
1,
"the very first ask about the carry logic in detail, and then much more text",
0,
);
assert!(unit_matches_summary(&long, &summary_fps));
let other = unit(Role::User, 1, "a completely different question entirely", 0);
assert!(!unit_matches_summary(&other, &summary_fps));
let empty = unit(Role::User, 1, " ", 0);
assert!(!unit_matches_summary(&empty, &summary_fps));
}
#[test]
fn dedup_demotes_live_region_match_but_not_pre_boundary() {
let dup_text = "the very first ask about the carry logic";
let turns = vec![
mk_turn(0, Some(dup_text), Some("old reply"), 0, 1), mk_turn(1, Some(dup_text), Some("live reply"), 0, 0), ];
let sums = vec![summary(900, vec![dup_text], 12000)];
let sr = scan_with_turns(turns, sums);
let plan = plan_session(&sr, 40000, 0.5, 0, &cfg());
assert_eq!(
plan.dedup_demoted, 1,
"exactly the live-region match is demoted"
);
assert!(plan.selected.iter().any(|s| s.turn_index == 0));
}
#[test]
fn dedup_demoted_turn_is_selected_after_non_dups_not_dropped() {
let dup_text = "shared question text that the summary already has verbatim here";
let turns = vec![
mk_turn(
0,
Some("unique earlier question"),
Some("earlier reply"),
0,
0,
),
mk_turn(1, Some(dup_text), Some("a later reply"), 0, 0),
];
let sums = vec![summary(900, vec![dup_text], 9000)];
let sr = scan_with_turns(turns, sums);
let plan = plan_session(&sr, 40000, 0.5, 0, &cfg());
assert!(plan.dedup_demoted >= 1);
assert!(plan.selected.iter().any(|s| s.turn_index == 1));
assert!(plan.selected.iter().any(|s| s.turn_index == 0));
}
#[test]
fn summary_fingerprints_handles_bare_unquoted_bullet() {
let body = "6. All user messages:\n - a bare unquoted bullet about the carry";
let fps = summary_fingerprints(body);
assert!(
fps.iter().any(|f| f.starts_with("a bare unquoted bullet")),
"bare bullet fingerprinted: {fps:?}"
);
}
#[test]
fn dedup_no_op_when_no_summary() {
let turns = vec![mk_turn(0, Some("ask"), Some("reply"), 0, 0)];
let sr = scan_with_turns(turns, Vec::new());
let plan = plan_session(&sr, 40000, 0.5, 0, &cfg());
assert_eq!(plan.dedup_demoted, 0);
assert!(plan.newest_summary_line.is_none());
}
#[test]
fn end_to_end_live_dedup_through_build_and_plan() {
let records: Vec<(usize, Record)> = vec![
(1, rec(r#"{"type":"user","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"pre-boundary ask"}}"#)),
(2, rec(r#"{"type":"assistant","timestamp":"2026-06-07T05:00:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"pre reply"}]}}"#)),
(3, rec("{\"type\":\"user\",\"isCompactSummary\":true,\"message\":{\"role\":\"user\",\"content\":\"6. All user messages:\\n - \\\"the live duplicate ask verbatim\\\"\\n9. Optional Next Step:\\n x\"}}")),
(4, rec(r#"{"type":"user","timestamp":"2026-06-07T06:00:00.000Z","message":{"role":"user","content":"the live duplicate ask verbatim"}}"#)),
(5, rec(r#"{"type":"assistant","timestamp":"2026-06-07T06:00:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply"}]}}"#)),
];
let (turns, summaries) = build(&records, &[]);
assert_eq!(summaries.len(), 1);
assert!(summaries[0]
.fingerprints
.iter()
.any(|f| f.starts_with("the live duplicate ask verbatim")));
assert_eq!(turns[0].compactions_before, 1);
assert_eq!(turns[1].compactions_before, 0);
let sr = scan_with_turns(turns, summaries);
let plan = plan_session(&sr, 40000, 0.5, 0, &cfg());
assert_eq!(
plan.dedup_demoted, 1,
"exactly the live-region match is demoted"
);
assert_eq!(plan.newest_summary_line, Some(3));
}
#[test]
fn dedup_skips_empty_text_unit() {
let summary_fps = vec![fingerprint("a real summary bullet about the carry")];
let blank = unit(Role::User, 1, " ", 0);
assert!(!unit_matches_summary(&blank, &summary_fps));
}
#[test]
fn summary_fingerprints_skips_empty_quote() {
let body = "6. All user messages:\n - \"\"\n - ";
let fps = summary_fingerprints(body);
assert!(
fps.is_empty(),
"empty bullets produce no fingerprints: {fps:?}"
);
}
#[test]
fn dedup_demoted_turn_sorts_after_non_dup_in_phase1() {
let dup_text = "the duplicate ask the summary already has verbatim in full here";
let dup_turn = mk_turn(0, Some(dup_text), Some("dup reply"), 0, 0);
let big_ask = format!("a unique fresh ask {}", "q".repeat(700));
let uniq_turn = mk_turn(1, Some(&big_ask), Some("unique reply"), 0, 0);
let one_pair = turn_cost(&uniq_turn, SelSides::Both, &cfg());
let dup_pair = turn_cost(&dup_turn, SelSides::Both, &cfg());
let sums = vec![summary(900, vec![dup_text], 9000)];
let sr = scan_with_turns(vec![dup_turn, uniq_turn], sums);
let want_available = one_pair + dup_pair / 2;
let mut budget = want_available + doc_header_block_max_chars(&sr, 40000);
budget = want_available + doc_header_block_max_chars(&sr, budget);
let available = budget - doc_header_block_max_chars(&sr, budget);
assert!(
available >= one_pair && available < one_pair + dup_pair,
"available {available} must fit exactly one pair (one={one_pair}, dup={dup_pair})"
);
let plan = plan_session(&sr, budget, 0.5, 0, &cfg());
assert_eq!(plan.dedup_demoted, 1);
assert!(
plan.selected.iter().any(|s| s.turn_index == 1),
"non-dup turn wins the tight budget: {:?}",
plan.selected
.iter()
.map(|s| s.turn_index)
.collect::<Vec<_>>()
);
}
#[test]
fn unit_matches_summary_no_match_returns_false() {
let summary_fps = vec![fingerprint(
"a summary bullet entirely unrelated to the unit",
)];
let u = unit(
Role::User,
1,
"a completely different question about something else",
0,
);
assert!(!unit_matches_summary(&u, &summary_fps));
let with_empty = vec![String::new(), fingerprint("real bullet text here")];
assert!(!unit_matches_summary(&u, &with_empty));
}
#[test]
fn summary_fingerprints_skips_empty_prose_quote() {
let body = "9. Optional Next Step:\n The assistant said \"\".";
let fps = summary_fingerprints(body);
assert!(
fps.is_empty(),
"empty prose quote yields no fingerprint: {fps:?}"
);
}
#[test]
fn dedup_flagged_middle_still_richness_gated() {
let mut agents = vec![
agent_msg(10, "the opening plan", 0, 0),
agent_msg(20, "found 12 in src/x.rs:9", 0, 0), agent_msg(30, "let me a", 0, 0),
agent_msg(40, "let me b", 0, 0),
agent_msg(50, "let me c", 0, 0),
agent_msg(60, "let me d", 0, 0),
agent_msg(70, "let me e", 0, 0),
agent_msg(80, "final", 0, 0),
];
agents[1].unit.also_in_summary = true;
assign_positions(&mut agents);
let t = TurnSlice {
turn_index: 0,
user: None,
tool_calls: 0,
image_ids: Vec::new(),
agents,
compactions_before: 0,
is_automation: false,
automation: None,
};
let lane = select_agent_messages(&t, &rich_cfg());
let kept_flagged = lane
.iter()
.any(|r| matches!(r, AgentRender::Kept(a) if a.unit.also_in_summary));
assert!(
kept_flagged,
"a rich dedup-flagged middle is kept carrying its flag"
);
}