Compare commits

...
Author SHA1 Message Date
ZakiandClaude Opus 4.6 9cb64dd8a7 fix: clarify test comment and use exact assertions
Address Gemini review feedback:
- Fix misleading comment: \u{00e9} is precomposed e-acute, not combining accent
- Replace weak assertions (ends_with/is_empty) with exact assert_eq!

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-26 16:56:56 -07:00
ZakiandClaude Opus 4.6 6198c98673 fix: prevent UTF-8 panics in byte-index string truncation
Replace unsafe `&s[..n]` patterns with `floor_char_boundary(s, n)` at 3
production code sites where the truncation index could land mid-multibyte
character, panicking on non-ASCII input:

- src/llm/nearai_chat.rs: API response truncation in error message
- src/cli/memory.rs: memory content display truncation
- src/cli/config.rs: config value display truncation

All 3 sites operate on external or user-supplied strings that may contain
non-ASCII characters. The existing `crate::util::floor_char_boundary`
utility (used at 18 other call sites) walks back to the nearest char
boundary, preventing the panic.

Adds regression test with multi-byte characters (combining accents and
4-byte emoji) for truncate_content.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-26 16:34:14 -07:00
3 changed files with 18 additions and 3 deletions
+2 -1
View File
@@ -127,7 +127,8 @@ async fn list_settings(
}
let display_value = if value.len() > 60 {
format!("{}...", &value[..57])
let end = crate::util::floor_char_boundary(&value, 57);
format!("{}...", &value[..end])
} else {
value
};
+15 -1
View File
@@ -256,7 +256,8 @@ fn truncate_content(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len])
let end = crate::util::floor_char_boundary(s, max_len);
format!("{}...", &s[..end])
}
}
@@ -292,4 +293,17 @@ mod tests {
assert_eq!(truncate_content("hello", 10), "hello");
assert_eq!(truncate_content("hello world", 5), "hello...");
}
#[test]
fn test_truncate_content_multibyte_does_not_panic() {
// \u{00e9} is precomposed 'é' (2 bytes in UTF-8)
let s = "caf\u{00e9} au lait"; // "café au lait", é starts at byte 3
let result = truncate_content(s, 4); // byte 4 is inside 2-byte é
assert_eq!(result, "caf...");
// 4-byte emoji: slicing mid-emoji must not panic
let emoji = "Hi \u{1F600} there"; // 😀 is 4 bytes, starts at byte 3
let result = truncate_content(emoji, 4); // byte 4 is inside 😀
assert_eq!(result, "Hi ...");
}
}
+1 -1
View File
@@ -451,7 +451,7 @@ impl NearAiChatProvider {
provider: "nearai_chat".to_string(),
reason: format!(
"No model names found in response: {}",
&response_text[..response_text.len().min(300)]
&response_text[..crate::util::floor_char_boundary(&response_text, 300)]
),
})
}