Merge pull request #865 from agorevski/feat/systematic-test-coverage-852

test: add systematic test coverage for 7 bug pattern groups (#852)
This commit is contained in:
Alex Gorevski 2026-02-19 07:02:20 -08:00 committed by GitHub
commit dce7280812
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 2272 additions and 8 deletions

View file

@ -843,4 +843,110 @@ mod tests {
// Should have UUID dashes
assert!(id.contains('-'));
}
// ─────────────────────────────────────────────────────────────────────
// TG6: Channel platform limit edge cases for Discord (2000 char limit)
// Prevents: Pattern 6 — issues #574, #499
// ─────────────────────────────────────────────────────────────────────
#[test]
fn split_message_code_block_at_boundary() {
// Code block that spans the split boundary
let mut msg = String::new();
msg.push_str("```rust\n");
msg.push_str(&"x".repeat(1990));
msg.push_str("\n```\nMore text after code block");
let parts = split_message_for_discord(&msg);
assert!(parts.len() >= 2, "code block spanning boundary should split");
for part in &parts {
assert!(
part.len() <= DISCORD_MAX_MESSAGE_LENGTH,
"each part must be <= {DISCORD_MAX_MESSAGE_LENGTH}, got {}",
part.len()
);
}
}
#[test]
fn split_message_single_long_word_exceeds_limit() {
// A single word longer than 2000 chars must be hard-split
let long_word = "a".repeat(2500);
let parts = split_message_for_discord(&long_word);
assert!(parts.len() >= 2, "word exceeding limit must be split");
for part in &parts {
assert!(
part.len() <= DISCORD_MAX_MESSAGE_LENGTH,
"hard-split part must be <= {DISCORD_MAX_MESSAGE_LENGTH}, got {}",
part.len()
);
}
// Reassembled content should match original
let reassembled: String = parts.join("");
assert_eq!(reassembled, long_word);
}
#[test]
fn split_message_exactly_at_limit_no_split() {
let msg = "a".repeat(DISCORD_MAX_MESSAGE_LENGTH);
let parts = split_message_for_discord(&msg);
assert_eq!(parts.len(), 1, "message exactly at limit should not split");
assert_eq!(parts[0].len(), DISCORD_MAX_MESSAGE_LENGTH);
}
#[test]
fn split_message_one_over_limit_splits() {
let msg = "a".repeat(DISCORD_MAX_MESSAGE_LENGTH + 1);
let parts = split_message_for_discord(&msg);
assert!(parts.len() >= 2, "message 1 char over limit must split");
}
#[test]
fn split_message_many_short_lines() {
// Many short lines should be batched into chunks under the limit
let msg: String = (0..500).map(|i| format!("line {i}\n")).collect();
let parts = split_message_for_discord(&msg);
for part in &parts {
assert!(
part.len() <= DISCORD_MAX_MESSAGE_LENGTH,
"short-line batch must be <= limit"
);
}
// All content should be preserved
let reassembled: String = parts.join("");
assert_eq!(reassembled.trim(), msg.trim());
}
#[test]
fn split_message_only_whitespace() {
let msg = " \n\n\t ";
let parts = split_message_for_discord(msg);
// Should handle gracefully without panic
assert!(parts.len() <= 1);
}
#[test]
fn split_message_emoji_at_boundary() {
// Emoji are multi-byte; ensure we don't split mid-emoji
let mut msg = "a".repeat(1998);
msg.push_str("🎉🎊"); // 2 emoji at the boundary (2000 chars total)
let parts = split_message_for_discord(&msg);
for part in &parts {
// The function splits on character count, not byte count
assert!(
part.chars().count() <= DISCORD_MAX_MESSAGE_LENGTH,
"emoji boundary split must respect limit"
);
}
}
#[test]
fn split_message_consecutive_newlines_at_boundary() {
let mut msg = "a".repeat(1995);
msg.push_str("\n\n\n\n\n");
msg.push_str(&"b".repeat(100));
let parts = split_message_for_discord(&msg);
for part in &parts {
assert!(part.len() <= DISCORD_MAX_MESSAGE_LENGTH);
}
}
}

View file

@ -18,7 +18,7 @@ const TELEGRAM_BIND_COMMAND: &str = "/bind";
/// Split a message into chunks that respect Telegram's 4096 character limit.
/// Tries to split at word boundaries when possible, and handles continuation.
fn split_message_for_telegram(message: &str) -> Vec<String> {
if message.len() <= TELEGRAM_MAX_MESSAGE_LENGTH {
if message.chars().count() <= TELEGRAM_MAX_MESSAGE_LENGTH {
return vec![message.to_string()];
}
@ -26,29 +26,35 @@ fn split_message_for_telegram(message: &str) -> Vec<String> {
let mut remaining = message;
while !remaining.is_empty() {
let chunk_end = if remaining.len() <= TELEGRAM_MAX_MESSAGE_LENGTH {
remaining.len()
// Find the byte offset for the Nth character boundary.
let hard_split = remaining
.char_indices()
.nth(TELEGRAM_MAX_MESSAGE_LENGTH)
.map_or(remaining.len(), |(idx, _)| idx);
let chunk_end = if hard_split == remaining.len() {
hard_split
} else {
// Try to find a good break point (newline, then space)
let search_area = &remaining[..TELEGRAM_MAX_MESSAGE_LENGTH];
let search_area = &remaining[..hard_split];
// Prefer splitting at newline
if let Some(pos) = search_area.rfind('\n') {
// Don't split if the newline is too close to the start
if pos >= TELEGRAM_MAX_MESSAGE_LENGTH / 2 {
if search_area[..pos].chars().count() >= TELEGRAM_MAX_MESSAGE_LENGTH / 2 {
pos + 1
} else {
// Try space as fallback
search_area
.rfind(' ')
.unwrap_or(TELEGRAM_MAX_MESSAGE_LENGTH)
.unwrap_or(hard_split)
+ 1
}
} else if let Some(pos) = search_area.rfind(' ') {
pos + 1
} else {
// Hard split at the limit
TELEGRAM_MAX_MESSAGE_LENGTH
// Hard split at character boundary
hard_split
}
};
@ -2825,4 +2831,100 @@ mod tests {
let ch_disabled = TelegramChannel::new("token".into(), vec!["*".into()], false);
assert!(!ch_disabled.mention_only);
}
// ─────────────────────────────────────────────────────────────────────
// TG6: Channel platform limit edge cases for Telegram (4096 char limit)
// Prevents: Pattern 6 — issues #574, #499
// ─────────────────────────────────────────────────────────────────────
#[test]
fn telegram_split_code_block_at_boundary() {
let mut msg = String::new();
msg.push_str("```python\n");
msg.push_str(&"x".repeat(4085));
msg.push_str("\n```\nMore text after code block");
let parts = split_message_for_telegram(&msg);
assert!(parts.len() >= 2, "code block spanning boundary should split");
for part in &parts {
assert!(
part.len() <= TELEGRAM_MAX_MESSAGE_LENGTH,
"each part must be <= {TELEGRAM_MAX_MESSAGE_LENGTH}, got {}",
part.len()
);
}
}
#[test]
fn telegram_split_single_long_word() {
let long_word = "a".repeat(5000);
let parts = split_message_for_telegram(&long_word);
assert!(parts.len() >= 2, "word exceeding limit must be split");
for part in &parts {
assert!(
part.len() <= TELEGRAM_MAX_MESSAGE_LENGTH,
"hard-split part must be <= {TELEGRAM_MAX_MESSAGE_LENGTH}, got {}",
part.len()
);
}
let reassembled: String = parts.join("");
assert_eq!(reassembled, long_word);
}
#[test]
fn telegram_split_exactly_at_limit_no_split() {
let msg = "a".repeat(TELEGRAM_MAX_MESSAGE_LENGTH);
let parts = split_message_for_telegram(&msg);
assert_eq!(parts.len(), 1, "message exactly at limit should not split");
}
#[test]
fn telegram_split_one_over_limit() {
let msg = "a".repeat(TELEGRAM_MAX_MESSAGE_LENGTH + 1);
let parts = split_message_for_telegram(&msg);
assert!(parts.len() >= 2, "message 1 char over limit must split");
}
#[test]
fn telegram_split_many_short_lines() {
let msg: String = (0..1000).map(|i| format!("line {i}\n")).collect();
let parts = split_message_for_telegram(&msg);
for part in &parts {
assert!(
part.len() <= TELEGRAM_MAX_MESSAGE_LENGTH,
"short-line batch must be <= limit"
);
}
}
#[test]
fn telegram_split_only_whitespace() {
let msg = " \n\n\t ";
let parts = split_message_for_telegram(msg);
assert!(parts.len() <= 1);
}
#[test]
fn telegram_split_emoji_at_boundary() {
let mut msg = "a".repeat(4094);
msg.push_str("🎉🎊"); // 4096 chars total
let parts = split_message_for_telegram(&msg);
for part in &parts {
// The function splits on character count, not byte count
assert!(
part.chars().count() <= TELEGRAM_MAX_MESSAGE_LENGTH,
"emoji boundary split must respect limit"
);
}
}
#[test]
fn telegram_split_consecutive_newlines() {
let mut msg = "a".repeat(4090);
msg.push_str("\n\n\n\n\n\n");
msg.push_str(&"b".repeat(100));
let parts = split_message_for_telegram(&msg);
for part in &parts {
assert!(part.len() <= TELEGRAM_MAX_MESSAGE_LENGTH);
}
}
}