use crate::channels::voice::service::*;
use proptest::prelude::*;
#[test]
fn split_short_text_returns_single_chunk() {
let text = "Hello world, this is short.";
let chunks = split_for_tts(text, 1500);
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0], text);
}
#[test]
fn split_long_text_breaks_at_sentences() {
let text = "First sentence here. Second sentence here. Third sentence is much longer and goes on for a while to push us over the limit.";
let chunks = split_for_tts(text, 50);
assert!(chunks.len() > 1);
for chunk in &chunks {
assert!(chunk.len() <= 50, "chunk too long: {} chars", chunk.len());
}
let rejoined: String = chunks.join(" ");
assert!(rejoined.contains("First sentence"));
assert!(rejoined.contains("Second sentence"));
assert!(rejoined.contains("Third sentence"));
}
#[test]
fn split_respects_question_and_exclamation() {
let text = "Is this working? Yes it is! Great news indeed. Another sentence.";
let chunks = split_for_tts(text, 30);
assert!(chunks.len() > 1);
for chunk in &chunks {
assert!(chunk.len() <= 30, "chunk too long: {} chars", chunk.len());
}
}
#[test]
fn split_falls_back_to_word_boundary() {
let text = "word ".repeat(100); let chunks = split_for_tts(&text, 100);
assert!(chunks.len() > 1);
for chunk in &chunks {
assert!(chunk.len() <= 100, "chunk too long: {} chars", chunk.len());
}
}
#[test]
fn find_sentence_break_finds_last_period() {
assert_eq!(find_sentence_break("Hello. World"), Some(7)); assert_eq!(find_sentence_break("Hello! World"), Some(7));
assert_eq!(find_sentence_break("Hello? World"), Some(7));
assert_eq!(find_sentence_break("No break here"), None);
}
#[test]
fn split_preserves_newlines() {
let text =
"Paragraph one.\n\nParagraph two.\n\nParagraph three with more text to exceed limit.";
let chunks = split_for_tts(text, 30);
assert!(chunks.len() > 1);
}
#[test]
fn split_empty_text() {
let chunks = split_for_tts("", 1500);
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0], "");
}
#[test]
fn split_exactly_at_limit() {
let text = "a".repeat(1500);
let chunks = split_for_tts(&text, 1500);
assert_eq!(chunks.len(), 1);
}
#[test]
fn split_one_over_limit() {
let text = "a".repeat(1501);
let chunks = split_for_tts(&text, 1500);
assert_eq!(chunks.len(), 2);
}
#[test]
fn split_realistic_long_response() {
let base = "Here is a detailed explanation of how Rust's borrow checker works. The borrow checker is a compile-time feature that ensures memory safety. It tracks the lifetime of references and ensures they don't outlive the data they point to. ";
let text = base.repeat(10); let chunks = split_for_tts(&text, 1500);
assert!(
chunks.len() >= 2,
"expected >= 2 chunks, got {}",
chunks.len()
);
for chunk in &chunks {
assert!(chunk.len() <= 1500, "chunk too long: {} chars", chunk.len());
assert!(!chunk.trim().is_empty(), "empty chunk");
}
}
fn collapse(text: &str) -> String {
text.split_whitespace().collect()
}
fn with_deadline<T: Send + 'static>(what: &str, f: impl FnOnce() -> T + Send + 'static) -> T {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(f());
});
rx.recv_timeout(std::time::Duration::from_secs(10))
.unwrap_or_else(|_| panic!("{what} never returned: the progress guard is gone (#2051)"))
}
#[test]
fn split_does_not_panic_on_a_multibyte_char_at_the_cut() {
let mut text = "a".repeat(1499);
text.push('\u{2192}');
text.push_str(&"b".repeat(200));
assert!(
!text.is_char_boundary(1500),
"premise: byte 1500 must land inside the arrow"
);
let chunks = split_for_tts(&text, 1500);
assert!(chunks.len() >= 2, "expected a split, got {chunks:?}");
for chunk in &chunks {
assert!(
chunk.len() <= 1500,
"chunk of {} bytes exceeds the budget",
chunk.len()
);
}
assert_eq!(collapse(&chunks.concat()), collapse(&text));
}
#[test]
fn split_does_not_panic_on_multibyte_whitespace_at_the_cut() {
let mut text = "x".repeat(100);
text.push('\u{00a0}');
text.push_str(&"y".repeat(100));
let nbsp = text.find('\u{00a0}').expect("nbsp is in the text");
assert!(
!text.is_char_boundary(nbsp + 1),
"premise: the byte after the nbsp is inside it"
);
let chunks = split_for_tts(&text, 150);
for chunk in &chunks {
assert!(chunk.len() <= 150, "chunk too long: {} bytes", chunk.len());
}
assert_eq!(collapse(&chunks.concat()), collapse(&text));
}
#[test]
fn split_makes_progress_when_a_char_is_wider_than_the_budget() {
let crabs = with_deadline("split_for_tts with a sub-char budget", || {
split_for_tts("\u{1f980}\u{1f980}\u{1f980}\u{1f980}", 1)
});
assert_eq!(crabs.len(), 4, "one chunk per crab: {crabs:?}");
for chunk in &crabs {
assert_eq!(chunk.chars().count(), 1);
}
let ascii = with_deadline("split_for_tts with a zero budget", || {
split_for_tts("abc", 0)
});
assert_eq!(
ascii.len(),
3,
"a zero budget must still terminate: {ascii:?}"
);
}
proptest! {
#![proptest_config(ProptestConfig { cases: 256, ..ProptestConfig::default() })]
#[test]
fn split_survives_arbitrary_unicode(
chars in proptest::collection::vec(any::<char>(), 0..300),
max_bytes in 1usize..64,
) {
let text: String = chars.into_iter().collect();
let chunks = split_for_tts(&text, max_bytes);
for chunk in &chunks {
prop_assert!(
chunk.len() <= max_bytes || chunk.chars().count() <= 1,
"chunk of {} bytes over a {}-byte budget: {:?}",
chunk.len(),
max_bytes,
chunk
);
}
prop_assert_eq!(collapse(&chunks.concat()), collapse(&text));
}
}
const CHUNKERS: [(&str, &str); 4] = [
(
"src/channels/voice/service.rs",
include_str!("../channels/voice/service.rs"),
),
(
"src/channels/telegram/markdown.rs",
include_str!("../channels/telegram/markdown.rs"),
),
(
"src/channels/trello/handler.rs",
include_str!("../channels/trello/handler.rs"),
),
(
"src/cron/scheduler.rs",
include_str!("../cron/scheduler.rs"),
),
];
#[test]
fn no_chunker_slices_at_a_raw_byte_budget() {
let offenders: Vec<String> = CHUNKERS
.iter()
.flat_map(|(path, source)| {
source
.lines()
.enumerate()
.filter(|(_, line)| !line.trim_start().starts_with("//"))
.filter(|(_, line)| line.contains("[..max"))
.map(move |(i, line)| format!("{path}:{}: {}", i + 1, line.trim()))
})
.collect();
assert!(
offenders.is_empty(),
"a chunker byte-slices at a raw budget and will panic on a multibyte \
char (#2051): {offenders:#?}"
);
}
#[test]
fn every_chunker_keeps_its_char_boundary_guard() {
let guards = [
(
"src/channels/voice/service.rs",
"truncate_str(remaining, max_bytes)",
),
("src/channels/telegram/markdown.rs", ".nth(max_len)"),
(
"src/channels/trello/handler.rs",
"is_char_boundary(safe_max)",
),
("src/cron/scheduler.rs", "truncate_str(content, MAX_LEN)"),
];
for (path, guard) in guards {
let source = CHUNKERS
.iter()
.find(|(p, _)| *p == path)
.map(|(_, s)| *s)
.unwrap_or_else(|| panic!("{path} is missing from CHUNKERS"));
assert!(
source.contains(guard),
"{path} lost its char boundary guard ({guard})"
);
}
}