use super::*;
use crate::llm::{MessageContent, Role};
fn user(text: &str) -> Message {
Message {
role: Role::User,
content: vec![MessageContent::Text {
text: text.to_string(),
}]
.into(),
}
}
#[test]
fn append_then_snapshot() {
let h = VecHistory::new();
h.append(user("hi"));
h.append(user("there"));
let snap = h.snapshot();
assert_eq!(snap.len(), 2);
}
#[test]
fn token_estimate_none_when_empty() {
let h = VecHistory::new();
assert!(h.token_estimate().is_none());
}
#[test]
fn token_estimate_char_heuristic_without_baseline() {
let h = VecHistory::new();
h.append(user(&"a".repeat(40))); assert_eq!(h.token_estimate(), Some(10));
}
#[test]
fn record_input_tokens_becomes_baseline_plus_increment() {
let h = VecHistory::new();
h.append(user("seed"));
h.record_input_tokens(1_000);
assert_eq!(h.token_estimate(), Some(1_000));
h.append(user(&"b".repeat(40))); assert_eq!(h.token_estimate(), Some(1_010));
}
#[test]
fn record_input_tokens_refreshes_baseline_and_resets_increment() {
let h = VecHistory::new();
h.record_input_tokens(1_000);
h.append(user(&"b".repeat(40))); assert_eq!(h.token_estimate(), Some(1_010));
h.record_input_tokens(2_000);
assert_eq!(h.token_estimate(), Some(2_000));
}
#[test]
fn replace_swaps_messages_and_clears_baseline() {
let h = VecHistory::new();
h.append(user("old one"));
h.append(user("old two"));
h.record_input_tokens(5_000);
assert_eq!(h.token_estimate(), Some(5_000));
h.replace(vec![user(&"c".repeat(80))]); let snap = h.snapshot();
assert_eq!(snap.len(), 1);
assert_eq!(h.token_estimate(), Some(20));
}
fn assistant(text: &str) -> Message {
Message {
role: Role::Assistant,
content: vec![MessageContent::Text {
text: text.to_string(),
}]
.into(),
}
}
#[test]
fn splice_prefix_replaces_head_keeps_tail() {
let h = VecHistory::new();
h.append(user("turn one"));
h.append(assistant("reply one"));
h.append(user("turn two"));
h.append(assistant("reply two"));
let dropped = h.splice_prefix(2, assistant("[summary]"));
assert_eq!(dropped, 2);
let snap = h.snapshot();
assert_eq!(snap.len(), 3); assert_eq!(snap[0].role, Role::Assistant);
assert!(matches!(
&snap[0].content[0],
MessageContent::Text { text } if text == "[summary]"
));
assert!(matches!(
&snap[1].content[0],
MessageContent::Text { text } if text == "turn two"
));
}
#[test]
fn splice_prefix_preserves_tail_appended_during_flight() {
let h = VecHistory::new();
h.append(user("old one"));
h.append(assistant("old reply"));
h.append(user("new one"));
h.append(assistant("new reply"));
let dropped = h.splice_prefix(2, assistant("[summary]"));
assert_eq!(dropped, 2);
let snap = h.snapshot();
assert_eq!(snap.len(), 3);
assert!(matches!(
&snap[1].content[0],
MessageContent::Text { text } if text == "new one"
));
assert!(matches!(
&snap[2].content[0],
MessageContent::Text { text } if text == "new reply"
));
}
#[test]
#[should_panic(expected = "splice_prefix invariant violated")]
fn splice_prefix_overlong_drop_count_trips_invariant_in_debug() {
let h = VecHistory::new();
h.append(user("only one"));
let _ = h.splice_prefix(99, assistant("[summary]"));
}
#[test]
fn splice_prefix_clears_baseline() {
let h = VecHistory::new();
h.append(user("seed one"));
h.append(user("seed two"));
h.record_input_tokens(5_000);
assert_eq!(h.token_estimate(), Some(5_000));
h.splice_prefix(1, assistant(&"c".repeat(80))); assert_eq!(h.token_estimate(), Some(22));
}