mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
fix: Telegram control commands being stripped (#135)
* Fix Telegram control commands being stripped The `clean_message_text()` function was returning an empty string for bare slash commands like `/interrupt`, `/stop`, `/help`, etc. This caused the commands to be replaced with "[User started the bot]" placeholder which broke command parsing in the agent. Changes: - Line 1079: Return the command unchanged instead of empty string - Line 1042: Only replace with placeholder for `/start` specifically - Add test coverage for control commands This fixes the issue where `/interrupt` doesn't work when bot is stuck waiting for approval. Co-Authored-By: Claude Sonnet 4.5 <[email protected]> * Add workspace declaration to Telegram package Fixes workspace conflict when building WASM component standalone. * Fix content_to_emit logic for bare control commands Addresses code review feedback: keep clean_message_text() returning empty for bare commands (its job is to extract user text, not pass commands through). Instead, fix the caller to distinguish: - /start (no args) → welcome placeholder - Other bare /commands → pass raw command to Submission::parse() - Commands with args → pass cleaned args - Empty/whitespace → skip Add comprehensive test_content_to_emit_logic() covering all edge cases including /start, control commands, args, plain text, and empty input. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: ubuntu <ubuntu@tyo-dev> Co-authored-by: Claude Sonnet 4.5 <[email protected]> Co-authored-by: firat.sertgoz <[email protected]> Co-authored-by: Illia Polosukhin <[email protected]>
This commit is contained in:
co-authored by
ubuntu
Claude Sonnet 4.5
firat.sertgoz
Illia Polosukhin
parent
c18f6730f8
commit
5416866bcf
@@ -16,6 +16,9 @@ wit-bindgen = "0.36"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
# Exclude from parent workspace (this is a standalone WASM component)
|
||||
[workspace]
|
||||
|
||||
[profile.release]
|
||||
# Optimize for size
|
||||
opt-level = "s"
|
||||
|
||||
@@ -1038,9 +1038,18 @@ fn handle_message(message: TelegramMessage) {
|
||||
},
|
||||
);
|
||||
|
||||
// For /start with no args, emit placeholder so agent can respond with welcome
|
||||
let content_to_emit = if cleaned_text.is_empty() && content.trim().starts_with('/') {
|
||||
// Determine what to emit to the agent.
|
||||
// - `/start` (no args): emit a welcome placeholder so the agent greets the user
|
||||
// - Other bare `/commands` (e.g. /interrupt, /help): pass the raw command through
|
||||
// so Submission::parse() can handle it
|
||||
// - Commands with args (e.g. `/start hello`): cleaned_text already has the args
|
||||
// - Plain text: pass through as-is
|
||||
let trimmed_content = content.trim();
|
||||
let content_to_emit = if trimmed_content.eq_ignore_ascii_case("/start") {
|
||||
"[User started the bot]".to_string()
|
||||
} else if cleaned_text.is_empty() && trimmed_content.starts_with('/') {
|
||||
// Bare control command like /interrupt, /stop, /help — pass through raw
|
||||
trimmed_content.to_string()
|
||||
} else if cleaned_text.is_empty() {
|
||||
return;
|
||||
} else {
|
||||
@@ -1159,6 +1168,77 @@ mod tests {
|
||||
assert_eq!(clean_message_text("@MyBot", Some("MyBot")), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clean_message_text_bare_commands() {
|
||||
// Bare commands return empty (the caller decides what to emit)
|
||||
assert_eq!(clean_message_text("/start", None), "");
|
||||
assert_eq!(clean_message_text("/interrupt", None), "");
|
||||
assert_eq!(clean_message_text("/stop", None), "");
|
||||
assert_eq!(clean_message_text("/help", None), "");
|
||||
assert_eq!(clean_message_text("/undo", None), "");
|
||||
assert_eq!(clean_message_text("/ping", None), "");
|
||||
|
||||
// Commands with args: command prefix stripped, args returned
|
||||
assert_eq!(clean_message_text("/start hello", None), "hello");
|
||||
assert_eq!(clean_message_text("/help me please", None), "me please");
|
||||
assert_eq!(clean_message_text("/model claude-opus-4-6", None), "claude-opus-4-6");
|
||||
}
|
||||
|
||||
/// Tests for the content_to_emit logic in handle_message.
|
||||
/// Since handle_message uses WASM host calls, we test the decision logic inline.
|
||||
#[test]
|
||||
fn test_content_to_emit_logic() {
|
||||
// Simulates the content_to_emit decision for various inputs.
|
||||
// This mirrors the logic in handle_message after clean_message_text.
|
||||
fn resolve_content(content: &str) -> Option<String> {
|
||||
let cleaned_text = clean_message_text(content, None);
|
||||
let trimmed_content = content.trim();
|
||||
if trimmed_content.eq_ignore_ascii_case("/start") {
|
||||
Some("[User started the bot]".to_string())
|
||||
} else if cleaned_text.is_empty() && trimmed_content.starts_with('/') {
|
||||
Some(trimmed_content.to_string())
|
||||
} else if cleaned_text.is_empty() {
|
||||
None // would return/skip in handle_message
|
||||
} else {
|
||||
Some(cleaned_text)
|
||||
}
|
||||
}
|
||||
|
||||
// /start → welcome placeholder
|
||||
assert_eq!(resolve_content("/start"), Some("[User started the bot]".to_string()));
|
||||
assert_eq!(resolve_content("/Start"), Some("[User started the bot]".to_string()));
|
||||
assert_eq!(resolve_content(" /start "), Some("[User started the bot]".to_string()));
|
||||
|
||||
// /start with args → pass args through
|
||||
assert_eq!(resolve_content("/start hello"), Some("hello".to_string()));
|
||||
|
||||
// Control commands → pass through raw so Submission::parse() can match
|
||||
assert_eq!(resolve_content("/interrupt"), Some("/interrupt".to_string()));
|
||||
assert_eq!(resolve_content("/stop"), Some("/stop".to_string()));
|
||||
assert_eq!(resolve_content("/help"), Some("/help".to_string()));
|
||||
assert_eq!(resolve_content("/undo"), Some("/undo".to_string()));
|
||||
assert_eq!(resolve_content("/redo"), Some("/redo".to_string()));
|
||||
assert_eq!(resolve_content("/ping"), Some("/ping".to_string()));
|
||||
assert_eq!(resolve_content("/tools"), Some("/tools".to_string()));
|
||||
assert_eq!(resolve_content("/compact"), Some("/compact".to_string()));
|
||||
assert_eq!(resolve_content("/clear"), Some("/clear".to_string()));
|
||||
assert_eq!(resolve_content("/version"), Some("/version".to_string()));
|
||||
|
||||
// Commands with args → cleaned text (command stripped)
|
||||
assert_eq!(resolve_content("/help me please"), Some("me please".to_string()));
|
||||
|
||||
// Plain text → pass through
|
||||
assert_eq!(resolve_content("hello world"), Some("hello world".to_string()));
|
||||
assert_eq!(resolve_content("just text"), Some("just text".to_string()));
|
||||
|
||||
// Empty / whitespace → skip (None)
|
||||
assert_eq!(resolve_content(""), None);
|
||||
assert_eq!(resolve_content(" "), None);
|
||||
|
||||
// Bare @mention without bot → skip
|
||||
assert_eq!(resolve_content("@botname"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_with_owner_id() {
|
||||
let json = r#"{"owner_id": 123456789}"#;
|
||||
|
||||
Reference in New Issue
Block a user