Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,8 +579,14 @@ fn cmd_auth(config_path: Option<std::path::PathBuf>, auth_cmd: AuthCommand) -> a
} else {
eprintln!("Anthropic OAuth: valid (expires in {}m)", expires_min);
}
eprintln!(" access token: {}...", &creds.access_token[..20]);
eprintln!(" refresh token: {}...", &creds.refresh_token[..20]);
eprintln!(
" access token: <redacted> ({} bytes)",
creds.access_token.len()
);
eprintln!(
" refresh token: <redacted> ({} bytes)",
creds.refresh_token.len()
);
Comment thread
l33t0 marked this conversation as resolved.
eprintln!(
" credentials file: {}",
spacebot::auth::credentials_path(&instance_dir).display()
Expand Down
83 changes: 83 additions & 0 deletions src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -992,4 +992,87 @@ mod tests {
assert_eq!(truncate_utf8_ellipsis(text, 10), "🙂...");
assert!(truncate_output(text, 5).starts_with("🙂"));
}

#[test]
fn truncate_cyrillic_does_not_panic() {
// Cyrillic chars are 2 bytes each in UTF-8
let text = "Привет, мир!"; // "Hello, world!" in Russian
// Cutting at byte 5 would land inside 'и' (bytes 4..5) - this must not panic
let result = truncate_utf8_ellipsis(text, 5);
assert!(!result.is_empty());
// "П" (2 bytes) + "..." (3 bytes) = 5, fits exactly
assert_eq!(result, "П...");

let result = truncate_utf8_ellipsis(text, 10);
assert!(!result.is_empty());
// Should truncate to a valid char boundary and append "..."
assert!(result.ends_with("..."));
}

#[test]
fn truncate_cjk_does_not_panic() {
// CJK chars are 3 bytes each in UTF-8
let text = "你好世界测试"; // "Hello world test" in Chinese
// Cutting at byte 4 would land inside '好' (bytes 3..5)
let result = truncate_utf8_ellipsis(text, 4);
assert_eq!(result, "你"); // only 3 bytes fit, no room for "..."

let result = truncate_utf8_ellipsis(text, 10);
assert!(!result.is_empty());
// 2 CJK chars (6 bytes) + "..." (3 bytes) = 9 bytes fits in 10
assert_eq!(result, "你好...");
}

#[test]
fn truncate_emoji_does_not_panic() {
// Emoji are 4 bytes each in UTF-8
let text = "Hello 😀🎉🚀 World";
let result = truncate_utf8_ellipsis(text, 10);
assert!(!result.is_empty());
// "Hello " (6 bytes) + "😀" won't fit with "..." in 10 bytes
// "Hello " (6 bytes) + "..." (3 bytes) = 9 bytes fits
assert_eq!(result, "Hello ...");

// Larger budget
let result = truncate_utf8_ellipsis(text, 15);
assert!(!result.is_empty());
assert!(result.ends_with("..."));
}

#[test]
fn truncate_mixed_multibyte_content() {
// Mix of ASCII, Cyrillic (2-byte), CJK (3-byte), and emoji (4-byte)
let text = "Hi Привет 你好 😀";
let result = truncate_utf8_ellipsis(text, 20);
assert!(!result.is_empty());
assert!(result.ends_with("..."));
// Verify it's valid UTF-8 by iterating chars
assert!(result.chars().count() > 0);
}

#[test]
fn truncate_output_multibyte_does_not_panic() {
let cyrillic = "Привет, мир! Это тестовая строка для проверки.";
let result = truncate_output(cyrillic, 15);
assert!(!result.is_empty());

let cjk = "你好世界,这是一个测试字符串。";
let result = truncate_output(cjk, 10);
assert!(!result.is_empty());

let emoji = "🎉🚀😀🌍💻🔥";
let result = truncate_output(emoji, 6);
assert!(!result.is_empty());
}

#[test]
fn truncate_at_exact_char_boundary_works() {
let text = "абв"; // 3 Cyrillic chars, 6 bytes total
// Exactly at a char boundary (4 bytes = 2 chars)
let result = truncate_utf8_ellipsis(text, 6);
assert_eq!(result, "абв"); // fits entirely

let result = truncate_utf8_ellipsis(text, 7);
assert_eq!(result, "абв"); // also fits, no truncation needed
}
}
18 changes: 3 additions & 15 deletions src/tools/browser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,11 +529,7 @@ fn render_snapshot_node(node: &SnapshotNode, depth: usize, output: &mut String)
if !node.name.is_empty() {
output.push_str(" \"");
// Truncate very long names for context efficiency.
let display_name = if node.name.len() > 200 {
format!("{}...", &node.name[..200])
} else {
node.name.clone()
};
let display_name = super::truncate_utf8_ellipsis(&node.name, 200);
output.push_str(&display_name.replace('"', "\\\""));
output.push('"');
}
Expand Down Expand Up @@ -574,11 +570,7 @@ fn render_snapshot_node(node: &SnapshotNode, depth: usize, output: &mut String)

// Value (e.g., text input current value)
if let Some(ref value) = node.value {
let display_value = if value.len() > 100 {
format!("{}...", &value[..100])
} else {
value.clone()
};
let display_value = super::truncate_utf8_ellipsis(value, 100);
output.push_str(&format!(
" value=\"{}\"",
display_value.replace('"', "\\\"")
Expand Down Expand Up @@ -1588,11 +1580,7 @@ impl Tool for BrowserTypeTool {
args.secret.as_deref().unwrap_or("unknown")
)
} else {
let display_text = if text_value.len() > 50 {
format!("{}...", &text_value[..50])
} else {
text_value
};
let display_text = super::truncate_utf8_ellipsis(&text_value, 50);
format!("Typed '{display_text}' into element at {label}")
};

Expand Down
Loading