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 = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
# Exclude from parent workspace (this is a standalone WASM component)
|
||||||
|
[workspace]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
# Optimize for size
|
# Optimize for size
|
||||||
opt-level = "s"
|
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
|
// Determine what to emit to the agent.
|
||||||
let content_to_emit = if cleaned_text.is_empty() && content.trim().starts_with('/') {
|
// - `/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()
|
"[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() {
|
} else if cleaned_text.is_empty() {
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@@ -1159,6 +1168,77 @@ mod tests {
|
|||||||
assert_eq!(clean_message_text("@MyBot", Some("MyBot")), "");
|
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]
|
#[test]
|
||||||
fn test_config_with_owner_id() {
|
fn test_config_with_owner_id() {
|
||||||
let json = r#"{"owner_id": 123456789}"#;
|
let json = r#"{"owner_id": 123456789}"#;
|
||||||
|
|||||||
Reference in New Issue
Block a user