use crate::config::{Config, normalize_toml_key};
use crate::db::Database;
#[test]
fn config_load_recovers_from_last_known_good() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
let good_path = dir.path().join("config.last_good.toml");
std::fs::write(
&good_path,
r#"
[agent]
context_limit = 100000
max_tokens = 8192
"#,
)
.unwrap();
std::fs::write(&config_path, "{{{{ broken toml !@#$%").unwrap();
assert!(Config::load_from_path(&config_path).is_err());
let good = Config::load_from_path(&good_path).unwrap();
assert_eq!(good.agent.context_limit, 100_000);
assert_eq!(good.agent.max_tokens, 8192);
}
#[test]
fn config_load_from_valid_file_succeeds() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(
&path,
r#"
[agent]
context_limit = 50000
max_tokens = 4096
[providers.anthropic]
enabled = true
"#,
)
.unwrap();
let config = Config::load_from_path(&path).unwrap();
assert_eq!(config.agent.context_limit, 50_000);
assert_eq!(config.agent.max_tokens, 4096);
assert!(config.providers.anthropic.unwrap().enabled);
}
#[test]
fn config_load_from_broken_file_fails() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(&path, "not valid toml {{{{").unwrap();
assert!(Config::load_from_path(&path).is_err());
}
#[test]
fn config_known_top_level_keys_are_accepted() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(
&path,
r#"
[crabrace]
[database]
[logging]
[debug]
[providers]
[channels]
[agent]
[daemon]
[a2a]
[image]
[cron]
"#,
)
.unwrap();
let config = Config::load_from_path(&path);
assert!(config.is_ok());
}
#[test]
fn config_gateway_alias_maps_to_a2a() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(
&path,
r#"
[gateway]
enabled = true
port = 9999
"#,
)
.unwrap();
let config = Config::load_from_path(&path).unwrap();
assert!(config.a2a.enabled);
assert_eq!(config.a2a.port, 9999);
}
#[tokio::test]
async fn db_integrity_check_passes_on_clean_db() {
let db = Database::connect_in_memory().await.unwrap();
db.run_migrations().await.unwrap();
assert!(!crate::db::db_integrity_failed());
}
#[tokio::test]
async fn db_in_memory_migrations_succeed() {
let db = Database::connect_in_memory().await.unwrap();
let result = db.run_migrations().await;
assert!(result.is_ok(), "Migrations failed: {:?}", result.err());
}
#[test]
fn normalize_toml_key_lowercases() {
assert_eq!(normalize_toml_key("Qwen"), "qwen");
assert_eq!(normalize_toml_key("OLLAMA"), "ollama");
assert_eq!(normalize_toml_key("DeepSeek"), "deepseek");
}
#[test]
fn normalize_toml_key_replaces_separators_with_hyphens() {
assert_eq!(normalize_toml_key("Qwen_2.5_4B"), "qwen-2-5-4b");
assert_eq!(normalize_toml_key("my_provider"), "my-provider");
assert_eq!(normalize_toml_key("My Provider"), "my-provider");
assert_eq!(normalize_toml_key("a.b.c"), "a-b-c");
}
#[test]
fn normalize_toml_key_strips_special_chars() {
assert_eq!(normalize_toml_key("model@v2!"), "modelv2");
assert_eq!(normalize_toml_key("test#123"), "test123");
}
#[test]
fn normalize_toml_key_trims_hyphens() {
assert_eq!(normalize_toml_key("_leading_"), "leading");
assert_eq!(normalize_toml_key(" spaces "), "spaces");
assert_eq!(normalize_toml_key("__double__"), "double");
}
#[test]
fn normalize_toml_key_preserves_clean_names() {
assert_eq!(normalize_toml_key("ollama"), "ollama");
assert_eq!(normalize_toml_key("nvidia"), "nvidia");
assert_eq!(normalize_toml_key("qwen-2-5-4b"), "qwen-2-5-4b");
}
#[test]
fn custom_provider_names_normalized_on_deserialize() {
let toml_str = r#"
[providers.custom.Qwen_2_5_4B]
enabled = true
base_url = "http://localhost:11434/v1"
default_model = "qwen2.5:4b"
[providers.custom.My_Other_Model]
enabled = false
base_url = "http://localhost:8080/v1"
"#;
let config: Config = toml::from_str(toml_str).unwrap();
let custom = config.providers.custom.unwrap();
assert!(
custom.contains_key("qwen-2-5-4b"),
"Keys: {:?}",
custom.keys().collect::<Vec<_>>()
);
assert!(
custom.contains_key("my-other-model"),
"Keys: {:?}",
custom.keys().collect::<Vec<_>>()
);
assert!(!custom.contains_key("Qwen_2_5_4B"));
assert!(!custom.contains_key("My_Other_Model"));
let qwen = custom.get("qwen-2-5-4b").unwrap();
assert!(qwen.enabled);
assert_eq!(qwen.base_url.as_deref(), Some("http://localhost:11434/v1"));
assert_eq!(qwen.default_model.as_deref(), Some("qwen2.5:4b"));
}
#[test]
fn custom_by_name_case_insensitive() {
let toml_str = r#"
[providers.custom.ollama]
enabled = true
base_url = "http://localhost:11434/v1"
"#;
let config: Config = toml::from_str(toml_str).unwrap();
assert!(config.providers.custom_by_name("ollama").is_some());
assert!(config.providers.custom_by_name("OLLAMA").is_some());
assert!(config.providers.custom_by_name("Ollama").is_some());
}
#[test]
fn config_write_key_normalizes_custom_provider_section() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let section = "providers.custom.Qwen_2_5_4B";
let parts: Vec<String> = section
.split('.')
.enumerate()
.map(|(i, p)| {
if i >= 2 && section.starts_with("providers.custom") {
normalize_toml_key(p)
} else {
p.to_string()
}
})
.collect();
assert_eq!(parts, vec!["providers", "custom", "qwen-2-5-4b"]);
}
#[tokio::test]
async fn agent_service_new_for_test_uses_defaults() {
use crate::brain::agent::AgentService;
use crate::brain::provider::PlaceholderProvider;
use std::sync::Arc;
let provider = Arc::new(PlaceholderProvider);
let db = Database::connect_in_memory().await.unwrap();
db.run_migrations().await.unwrap();
let ctx = crate::services::ServiceContext::new(db.pool().clone());
let agent = AgentService::new_for_test(provider, ctx).await;
let defaults = Config::default();
assert_eq!(agent.context_limit(), defaults.agent.context_limit);
assert_eq!(agent.max_tokens(), defaults.agent.max_tokens);
}
#[tokio::test]
async fn agent_service_new_uses_provided_config() {
use crate::brain::agent::AgentService;
use crate::brain::provider::PlaceholderProvider;
use std::sync::Arc;
let provider = Arc::new(PlaceholderProvider);
let db = Database::connect_in_memory().await.unwrap();
db.run_migrations().await.unwrap();
let ctx = crate::services::ServiceContext::new(db.pool().clone());
let mut config = Config::default();
config.agent.context_limit = 42_000;
config.agent.max_tokens = 1234;
let agent = AgentService::new(provider, ctx, &config).await;
assert_eq!(agent.context_limit(), 42_000);
assert_eq!(agent.max_tokens(), 1234);
}
#[test]
fn self_healing_alert_progress_event_carries_message() {
use crate::brain::agent::ProgressEvent;
let event = ProgressEvent::SelfHealingAlert {
message: "Emergency compaction: context too large".to_string(),
};
match event {
ProgressEvent::SelfHealingAlert { message } => {
assert!(message.contains("compaction"));
}
_ => panic!("Expected SelfHealingAlert variant"),
}
}
#[tokio::test]
async fn pending_requests_created_and_cleared() {
use crate::db::repository::PendingRequestRepository;
let db = Database::connect_in_memory().await.unwrap();
db.run_migrations().await.unwrap();
let repo = PendingRequestRepository::new(db.pool().clone());
let id = uuid::Uuid::new_v4();
let session_id = uuid::Uuid::new_v4();
repo.insert(id, session_id, "test message", "tui", None)
.await
.unwrap();
let interrupted = repo.get_interrupted().await.unwrap();
assert_eq!(interrupted.len(), 1);
assert_eq!(interrupted[0].session_id, session_id.to_string());
repo.clear_all().await.unwrap();
let interrupted = repo.get_interrupted().await.unwrap();
assert!(interrupted.is_empty());
}
#[tokio::test]
async fn pending_requests_deduplicate_by_session() {
use crate::db::repository::PendingRequestRepository;
let db = Database::connect_in_memory().await.unwrap();
db.run_migrations().await.unwrap();
let repo = PendingRequestRepository::new(db.pool().clone());
let session_id = uuid::Uuid::new_v4();
repo.insert(uuid::Uuid::new_v4(), session_id, "msg1", "tui", None)
.await
.unwrap();
repo.insert(uuid::Uuid::new_v4(), session_id, "msg2", "tui", None)
.await
.unwrap();
let interrupted = repo.get_interrupted().await.unwrap();
let unique_sessions: std::collections::HashSet<&String> =
interrupted.iter().map(|r| &r.session_id).collect();
assert_eq!(unique_sessions.len(), 1);
}
#[tokio::test]
async fn pending_request_stores_channel_and_chat_id() {
use crate::db::repository::PendingRequestRepository;
let db = Database::connect_in_memory().await.unwrap();
db.run_migrations().await.unwrap();
let repo = PendingRequestRepository::new(db.pool().clone());
let id = uuid::Uuid::new_v4();
let session_id = uuid::Uuid::new_v4();
repo.insert(id, session_id, "hello", "telegram", Some("-100123456"))
.await
.unwrap();
let interrupted = repo.get_interrupted().await.unwrap();
assert_eq!(interrupted.len(), 1);
assert_eq!(interrupted[0].channel, "telegram");
assert_eq!(
interrupted[0].channel_chat_id.as_deref(),
Some("-100123456")
);
}
#[tokio::test]
async fn pending_request_channel_chat_id_is_optional() {
use crate::db::repository::PendingRequestRepository;
let db = Database::connect_in_memory().await.unwrap();
db.run_migrations().await.unwrap();
let repo = PendingRequestRepository::new(db.pool().clone());
repo.insert(
uuid::Uuid::new_v4(),
uuid::Uuid::new_v4(),
"msg",
"tui",
None,
)
.await
.unwrap();
let interrupted = repo.get_interrupted().await.unwrap();
assert_eq!(interrupted.len(), 1);
assert_eq!(interrupted[0].channel, "tui");
assert!(interrupted[0].channel_chat_id.is_none());
}
#[tokio::test]
async fn pending_requests_multi_channel_coexistence() {
use crate::db::repository::PendingRequestRepository;
let db = Database::connect_in_memory().await.unwrap();
db.run_migrations().await.unwrap();
let repo = PendingRequestRepository::new(db.pool().clone());
let tui_sid = uuid::Uuid::new_v4();
let tg_sid = uuid::Uuid::new_v4();
let dc_sid = uuid::Uuid::new_v4();
let slack_sid = uuid::Uuid::new_v4();
repo.insert(uuid::Uuid::new_v4(), tui_sid, "tui msg", "tui", None)
.await
.unwrap();
repo.insert(
uuid::Uuid::new_v4(),
tg_sid,
"telegram msg",
"telegram",
Some("-100999"),
)
.await
.unwrap();
repo.insert(
uuid::Uuid::new_v4(),
dc_sid,
"discord msg",
"discord",
Some("123456789"),
)
.await
.unwrap();
repo.insert(
uuid::Uuid::new_v4(),
slack_sid,
"slack msg",
"slack",
Some("C01ABC"),
)
.await
.unwrap();
let all = repo.get_interrupted().await.unwrap();
assert_eq!(all.len(), 4);
let tui_only = repo.get_interrupted_for_channel("tui").await.unwrap();
assert_eq!(tui_only.len(), 1);
assert_eq!(tui_only[0].session_id, tui_sid.to_string());
let tg_only = repo.get_interrupted_for_channel("telegram").await.unwrap();
assert_eq!(tg_only.len(), 1);
assert_eq!(tg_only[0].channel_chat_id.as_deref(), Some("-100999"));
let dc_only = repo.get_interrupted_for_channel("discord").await.unwrap();
assert_eq!(dc_only.len(), 1);
let slack_only = repo.get_interrupted_for_channel("slack").await.unwrap();
assert_eq!(slack_only.len(), 1);
let wa = repo.get_interrupted_for_channel("whatsapp").await.unwrap();
assert!(wa.is_empty());
}
#[tokio::test]
async fn pending_requests_delete_ids() {
use crate::db::repository::PendingRequestRepository;
let db = Database::connect_in_memory().await.unwrap();
db.run_migrations().await.unwrap();
let repo = PendingRequestRepository::new(db.pool().clone());
let id1 = uuid::Uuid::new_v4();
let id2 = uuid::Uuid::new_v4();
let id3 = uuid::Uuid::new_v4();
repo.insert(id1, uuid::Uuid::new_v4(), "msg1", "tui", None)
.await
.unwrap();
repo.insert(id2, uuid::Uuid::new_v4(), "msg2", "telegram", Some("123"))
.await
.unwrap();
repo.insert(id3, uuid::Uuid::new_v4(), "msg3", "discord", Some("456"))
.await
.unwrap();
repo.delete_ids(vec![id1.to_string(), id2.to_string()])
.await
.unwrap();
let remaining = repo.get_interrupted().await.unwrap();
assert_eq!(remaining.len(), 1);
assert_eq!(remaining[0].channel, "discord");
}
#[tokio::test]
async fn pending_requests_delete_ids_empty_is_noop() {
use crate::db::repository::PendingRequestRepository;
let db = Database::connect_in_memory().await.unwrap();
db.run_migrations().await.unwrap();
let repo = PendingRequestRepository::new(db.pool().clone());
repo.insert(
uuid::Uuid::new_v4(),
uuid::Uuid::new_v4(),
"msg",
"tui",
None,
)
.await
.unwrap();
repo.delete_ids(vec![]).await.unwrap();
let remaining = repo.get_interrupted().await.unwrap();
assert_eq!(remaining.len(), 1);
}
#[tokio::test]
async fn pending_request_delete_removes_single_request() {
use crate::db::repository::PendingRequestRepository;
let db = Database::connect_in_memory().await.unwrap();
db.run_migrations().await.unwrap();
let repo = PendingRequestRepository::new(db.pool().clone());
let id1 = uuid::Uuid::new_v4();
let id2 = uuid::Uuid::new_v4();
repo.insert(id1, uuid::Uuid::new_v4(), "msg1", "telegram", Some("111"))
.await
.unwrap();
repo.insert(id2, uuid::Uuid::new_v4(), "msg2", "telegram", Some("222"))
.await
.unwrap();
repo.delete(id1).await.unwrap();
let remaining = repo.get_interrupted().await.unwrap();
assert_eq!(remaining.len(), 1);
assert_eq!(remaining[0].channel_chat_id.as_deref(), Some("222"));
}
#[test]
fn floor_char_boundary_prevents_emoji_panic() {
let mut s = "A".repeat(497); s.push('🔺'); s.push_str(&"B".repeat(100));
assert!(s.len() > 500);
let end = s.floor_char_boundary(500);
let truncated = &s[..end];
assert_eq!(end, 497);
assert_eq!(truncated.len(), 497);
assert!(truncated.is_char_boundary(truncated.len()));
}
#[test]
fn ceil_char_boundary_prevents_emoji_panic_from_end() {
let mut s = "X".repeat(100);
s.push('🔺'); s.push_str(&"Y".repeat(797));
let target = s.len() - 800;
let start = s.ceil_char_boundary(target);
let truncated = &s[start..];
assert!(s.is_char_boundary(start));
assert_eq!(start, 104);
assert!(truncated.starts_with('Y'));
}
#[test]
fn floor_char_boundary_handles_cjk_characters() {
let s = "中".repeat(200); assert_eq!(s.len(), 600);
let end = s.floor_char_boundary(500);
assert_eq!(end, 498);
let truncated = &s[..end];
assert!(truncated.is_char_boundary(truncated.len()));
}
#[test]
fn floor_char_boundary_ascii_is_identity() {
let s = "Hello, world! This is a plain ASCII string that is long enough.".repeat(10);
let end = s.floor_char_boundary(500);
assert_eq!(end, 500);
}
#[tokio::test]
async fn nested_spawn_catches_panic() {
let result = tokio::task::spawn(async {
panic!("simulated agent panic");
})
.await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.is_panic());
}
#[tokio::test]
async fn nested_spawn_returns_ok_on_success() {
let result = tokio::task::spawn(async { 42 }).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
}
#[tokio::test]
async fn session_delete_cascades_messages() {
use crate::services::{MessageService, ServiceContext, SessionService};
let db = Database::connect_in_memory().await.unwrap();
db.run_migrations().await.unwrap();
let ctx = ServiceContext::new(db.pool().clone());
let session_svc = SessionService::new(ctx.clone());
let msg_svc = MessageService::new(ctx.clone());
let session = session_svc
.create_session(Some("test".to_string()))
.await
.unwrap();
msg_svc
.create_message(session.id, "user".to_string(), "hello".to_string())
.await
.unwrap();
msg_svc
.create_message(session.id, "assistant".to_string(), "hi back".to_string())
.await
.unwrap();
let msgs = msg_svc.list_messages_for_session(session.id).await.unwrap();
assert_eq!(msgs.len(), 2);
session_svc.delete_session(session.id).await.unwrap();
let msgs = msg_svc.list_messages_for_session(session.id).await.unwrap();
assert!(msgs.is_empty());
}
#[test]
fn config_default_has_sane_values() {
let config = Config::default();
assert!(config.agent.context_limit > 0);
assert!(config.agent.max_tokens > 0);
assert!(!config.a2a.enabled);
}
#[test]
fn tool_call_entry_defaults_to_not_completed() {
use crate::tui::app::ToolCallEntry;
let entry = ToolCallEntry {
description: "Read file.rs".to_string(),
success: true,
details: None,
completed: false,
tool_input: serde_json::Value::Null,
};
assert!(!entry.completed);
assert!(entry.details.is_none());
}
#[test]
fn tool_call_entry_completed_independent_of_details() {
use crate::tui::app::ToolCallEntry;
let entry = ToolCallEntry {
description: "bash: ls".to_string(),
success: true,
details: None,
completed: true,
tool_input: serde_json::Value::Null,
};
assert!(entry.completed);
assert!(entry.details.is_none());
let entry2 = ToolCallEntry {
description: "Read foo.rs".to_string(),
success: true,
details: Some("42 lines".to_string()),
completed: true,
tool_input: serde_json::Value::Null,
};
assert!(entry2.completed);
assert!(entry2.details.is_some());
}
#[test]
fn format_tool_description_handles_camel_case_keys() {
use crate::tui::app::App;
let input = serde_json::json!({"filePath": "/tmp/test.rs"});
let desc = App::format_tool_description("read", &input);
assert_eq!(desc, "Read /tmp/test.rs");
let input2 = serde_json::json!({"file_path": "/tmp/test.rs"});
let desc2 = App::format_tool_description("read", &input2);
assert_eq!(desc2, "Read /tmp/test.rs");
let input3 = serde_json::json!({"path": "/tmp/test.rs"});
let desc3 = App::format_tool_description("read", &input3);
assert_eq!(desc3, "Read /tmp/test.rs");
}
#[test]
fn format_tool_description_case_insensitive_command() {
use crate::tui::app::App;
let input = serde_json::json!({"Command": "ls -la"});
let desc = App::format_tool_description("bash", &input);
assert_eq!(desc, "bash: ls -la");
}
#[test]
fn format_tool_description_case_insensitive_query() {
use crate::tui::app::App;
let input = serde_json::json!({"Query": "rust async"});
let desc = App::format_tool_description("web_search", &input);
assert_eq!(desc, "Search: rust async");
}
#[test]
fn detects_tools_arent_responding_preamble() {
use crate::brain::agent::service::is_gaslighting_preamble;
assert!(is_gaslighting_preamble(
"Tools aren't responding right now — `ls`, `bash`, and `read_file` all timed out"
));
assert!(is_gaslighting_preamble(
"Tools aren't responding in this session — might be a runtime hiccup"
));
}
#[test]
fn detects_tools_are_flaky_preamble() {
use crate::brain::agent::service::is_gaslighting_preamble;
assert!(is_gaslighting_preamble(
"Tools are still flaky — `config_manager` and `bash` both failed"
));
assert!(is_gaslighting_preamble(
"Tools are flaky right now but I have the fix ready"
));
assert!(is_gaslighting_preamble(
"Tools are flaky right now. Here's the exact patch"
));
}
#[test]
fn detects_isnt_actually_registered_preamble() {
use crate::brain::agent::service::is_gaslighting_preamble;
let text = "`analyze_image` isn't actually registered in this session's runtime, \
even though it appears in the tool schema. That's a mismatch between \
the advertised capabilities and what's loaded.";
assert!(is_gaslighting_preamble(text));
}
#[test]
fn detects_mismatch_advertised_capabilities() {
use crate::brain::agent::service::is_gaslighting_preamble;
assert!(is_gaslighting_preamble(
"There's a mismatch between the advertised capabilities and the runtime"
));
}
#[test]
fn detects_runtime_hiccup_phrases() {
use crate::brain::agent::service::is_gaslighting_preamble;
assert!(is_gaslighting_preamble(
"might be a runtime hiccup, let me retry"
));
assert!(is_gaslighting_preamble(
"There's an underlying system disruption affecting tools"
));
}
#[test]
fn detects_vision_tool_isnt_currently_available() {
use crate::brain::agent::service::is_gaslighting_preamble;
let text = "The vision tool isn't currently available despite being in my tool list. \
This might be a configuration issue. I can see you've attached an image, \
but I'm unable to analyze it at the moment.\n\n\
If you need image analysis, you could:\n\n\
Try uploading it again\n\
Check if the Google Gemini vision integration is properly configured\n\
Or just tell me what's in the image and I can help with that\n\n\
What's in the screenshot?";
assert!(is_gaslighting_preamble(text));
}
#[test]
fn does_not_strip_legit_screenshot_description() {
use crate::brain::agent::service::is_gaslighting_preamble;
let legit = "It's a terminal screenshot of your OpenCrabs TUI session.\n\n\
You're in the middle of a debug/commit flow:\n\
- Clippy confirmed checks passed and suggested git commands \
to commit changes related to forwarding `reasoning_content` \
as thinking tokens.\n\
- Crabsdev (you) was investigating modified files, noted that \
\"tools have died,\" and asked for manual git commands.";
assert!(
!is_gaslighting_preamble(legit),
"Legit screenshot description must NOT be stripped"
);
}
#[test]
fn does_not_strip_legit_cant_find_file() {
use crate::brain::agent::service::is_gaslighting_preamble;
assert!(!is_gaslighting_preamble(
"I can't find the file you mentioned at src/brain/helper.rs. \
Did you mean src/brain/helpers.rs?"
));
assert!(!is_gaslighting_preamble(
"I don't have access to the database credentials in this session."
));
}
#[test]
fn detects_01_25_cant_see_image_local_files() {
use crate::brain::agent::service::is_gaslighting_preamble;
assert!(is_gaslighting_preamble(
"I can't see the image directly as I don't have access to local \
files or an image analysis tool in this environment. If you can \
describe the image or paste its content, I can help you with it. \
Alternatively, if it's a screenshot of code or text, you might \
be able to copy and paste that here."
));
}
#[test]
fn detects_00_43_refusal_verbatim() {
use crate::brain::agent::service::is_gaslighting_preamble;
let text = "I can't see the image directly, but I can analyze it for you. \
Let me use the available tool to describe what's in that screenshot.\n\n\
I don't have access to an image analysis tool in my current environment. \
The `analyze_image` tool isn't available, and I can't directly view or \
process image files.\n\n\
If you can describe what's in the screenshot, I can help you with \
whatever it shows—code, error messages, UI elements, etc.";
assert!(
is_gaslighting_preamble(text),
"00:43 refusal text should be detected as gaslighting"
);
}
#[test]
fn detects_no_access_to_working_tool_preamble() {
use crate::brain::agent::service::is_gaslighting_preamble;
let text = "I don't have access to a working image analysis tool for local files \
right now. The `analyze_image` tool isn't available in my current \
environment.\n\nA few options:\n\n\
Upload the screenshot to a public URL (Imgur, GitHub, etc.) and I can \
try to analyze it via URL\n\
Describe what's in the screenshot and I can help you troubleshoot or \
discuss it\n\
Use `bash` to extract metadata (file type, dimensions) if that's useful";
assert!(is_gaslighting_preamble(text));
}
#[test]
fn detects_unable_to_execute_phrases() {
use crate::brain::agent::service::is_gaslighting_preamble;
assert!(is_gaslighting_preamble(
"I'm unable to execute the tool right now"
));
assert!(is_gaslighting_preamble(
"Tools appear to be unavailable in this session"
));
}
#[test]
fn gaslighting_predicate_is_case_insensitive() {
use crate::brain::agent::service::is_gaslighting_preamble;
assert!(is_gaslighting_preamble("TOOLS AREN'T RESPONDING"));
assert!(is_gaslighting_preamble("Tools Are Flaky"));
}
#[test]
fn gaslighting_predicate_ignores_empty_and_whitespace() {
use crate::brain::agent::service::is_gaslighting_preamble;
assert!(!is_gaslighting_preamble(""));
assert!(!is_gaslighting_preamble(" \n\t "));
}
#[test]
fn gaslighting_predicate_skips_long_narration() {
use crate::brain::agent::service::is_gaslighting_preamble;
let long = "Let me walk through what happened. ".repeat(50)
+ "At one point in history the tools are flaky but that was fixed months ago.";
assert!(long.len() > 1500);
assert!(!is_gaslighting_preamble(&long));
}
#[test]
fn gaslighting_predicate_keeps_legit_assistant_text() {
use crate::brain::agent::service::is_gaslighting_preamble;
assert!(!is_gaslighting_preamble(
"Running ls on the current directory to see what's there."
));
assert!(!is_gaslighting_preamble(
"I'll read the config file and check the provider settings."
));
assert!(!is_gaslighting_preamble(
"Here's what I found in the source code."
));
assert!(!is_gaslighting_preamble(
"I won't delete that file without your explicit confirmation."
));
}
#[test]
fn phantom_tool_intent_narrated_file_changes() {
use crate::brain::agent::service::has_phantom_tool_intent;
assert!(has_phantom_tool_intent(
"Now let me update the installation docs with per-system one-liners.\n\n\
Installation docs (docs/src/getting-started/installation.md):\n\n\
Replaced the messy multi-line script with clean per-system one-liners\n\
Linux: Added libgomp1 dependency"
));
assert!(has_phantom_tool_intent(
"Now let me fix both: the installation docs with per-system clean commands, \
and the landing page hero install section.\n\
Now update the landing page install section:\n\
src/main.rs changes applied."
));
assert!(has_phantom_tool_intent(
"I'll update src/scripts/setup.sh to include the missing libgomp1 package \
and also fix the Dockerfile runtime stage."
));
assert!(has_phantom_tool_intent(
"Here's what changed in README.md:\n\
- Added runtime dependency block for pre-built binaries\n\
- Updated Fedora/Arch build-from-source lines"
));
assert!(has_phantom_tool_intent(
"Let me create the new config.toml file with the rotation accounts \
and update src/brain/provider/factory.rs to use it."
));
}
#[test]
fn phantom_tool_intent_false_positives() {
use crate::brain::agent::service::has_phantom_tool_intent;
assert!(!has_phantom_tool_intent("OK, done."));
assert!(!has_phantom_tool_intent("Sure, I can help with that."));
assert!(!has_phantom_tool_intent(
"The file src/main.rs contains the entry point for the application. \
It initializes the TUI and starts the event loop. The config is loaded \
from ~/.opencrabs/config.toml on startup."
));
assert!(!has_phantom_tool_intent(
"Looking at src/brain/provider/factory.rs, the provider is created \
based on the config settings. The retry logic is in retry.rs."
));
assert!(!has_phantom_tool_intent(""));
assert!(!has_phantom_tool_intent(" "));
assert!(!has_phantom_tool_intent(
"Now let me update the database schema to include the new rotation fields. \
The migration should add a new column for account rotation status."
));
}
#[test]
fn phantom_no_tools_scope_is_prose_lead_in() {
use crate::brain::agent::service::has_phantom_tool_intent_no_tools;
let table_with_intent_in_cell = "\
| Date | Commit | Summary |\n\
|------|--------|---------|\n\
| 2026-04-17 | `ce86afd` | fix(heal): phantom detector lets 'Let me check…' loops slide after one retry |\n\
| 2026-04-17 | `3089213` | fix(heal): broaden phantom-retry gate for local providers + blunter nudge |\n";
assert!(
!has_phantom_tool_intent_no_tools(table_with_intent_in_cell),
"commit table starting with | must NOT phantom even if cells quote intent phrases"
);
let unsloth_style_markdown_bash = "\
Let me check the git log since 0.3.10.\n\n\
```bash\n\
cd /Users/adolfousierstudio/srv/rs/opencrabs && git log --oneline 0.3.10..HEAD\n\
```";
assert!(
has_phantom_tool_intent_no_tools(unsloth_style_markdown_bash),
"intent phrase followed by bash code block must phantom — model wrote code instead of calling the tool"
);
let code_block_no_lead_intent = "\
```rust\n\
// let me check if the user is admin\n\
fn is_admin(u: &User) -> bool { u.role == Role::Admin }\n\
```";
assert!(
!has_phantom_tool_intent_no_tools(code_block_no_lead_intent),
"code block starting the response must NOT phantom even if comment quotes intent phrase"
);
let list_intent_only_inside = "\
- anthropic — fast, reliable\n\
- openai — let me check that still works\n\
- gemini — free tier, generous\n\
- openrouter — 400+ models\n\
- minimax — vision strong\n\
- qwen — dashscope\n";
assert!(
!has_phantom_tool_intent_no_tools(list_intent_only_inside),
"list starting with a bullet must NOT phantom — intent phrase in a list item is not narration"
);
let real_phantom = "Let me check the git log to see recent changes. \
I'll look at the last few commits.";
assert!(
has_phantom_tool_intent_no_tools(real_phantom),
"pure narration with intent phrases must still phantom"
);
}
#[test]
fn phantom_now_gerund_status_then_action_drops() {
use crate::brain::agent::service::has_phantom_tool_intent;
assert!(has_phantom_tool_intent(
"dev is at ce21b098. The LandingPage fix is committed. Now cherry-picking to main and prod."
));
assert!(has_phantom_tool_intent(
"Slack dedup fix applied. Now updating the TESTING.md docs with the new coverage."
));
assert!(has_phantom_tool_intent(
"Build passed. Now committing the WhatsApp dedup changes to the branch."
));
assert!(has_phantom_tool_intent(
"Tests all green. Now pushing to origin main for the release."
));
assert!(has_phantom_tool_intent(
"CI is clean. Now merging the feature branch into main."
));
assert!(has_phantom_tool_intent(
"Config updated. Now deploying to the staging server."
));
assert!(has_phantom_tool_intent(
"Dependencies resolved. Now building the release binary."
));
assert!(has_phantom_tool_intent(
"Changes ready. Now testing the full suite before commit."
));
assert!(has_phantom_tool_intent(
"Patch applied. Now restarting the service to pick up changes."
));
assert!(has_phantom_tool_intent(
"Fix verified. Now amending the commit with the correct message."
));
assert!(has_phantom_tool_intent(
"Branch diverged. Now rebasing onto latest main."
));
}
#[test]
fn phantom_now_gerund_false_positives() {
use crate::brain::agent::service::has_phantom_tool_intent;
assert!(!has_phantom_tool_intent(
"Are you now checking the logs for errors?"
));
assert!(!has_phantom_tool_intent(
"I'm updating the docs later today when I have time."
));
assert!(!has_phantom_tool_intent(
"The build is now complete and all tests passed successfully."
));
assert!(!has_phantom_tool_intent(
"We are now testing the new feature in staging."
));
assert!(!has_phantom_tool_intent("Now updating."));
assert!(!has_phantom_tool_intent("Now fixing."));
}
#[test]
fn phantom_tool_intent_numbered_step_narration() {
use crate::brain::agent::service::has_phantom_tool_intent;
assert!(has_phantom_tool_intent(
"Here's what I need to do:\n\
1. Update src/config/types.rs to add the new field\n\
2. Fix the migration in src/db/migrations.rs\n\
3. Add tests for the new functionality"
));
assert!(has_phantom_tool_intent(
"I'll make these changes:\n\
1. Create the new provider file\n\
2. Modify the factory to use it\n\
3. Update the config schema"
));
}
#[test]
fn phantom_tool_intent_completion_claims() {
use crate::brain::agent::service::has_phantom_tool_intent;
assert!(has_phantom_tool_intent(
"I've updated src/brain/provider/factory.rs with the rotation logic \
and fixed the config schema. All changes have been applied."
));
assert!(has_phantom_tool_intent(
"I've made the changes to src/tui/render/dialogs.rs. The task is complete \
and the rotation UI should now work correctly."
));
assert!(has_phantom_tool_intent(
"Updated src/config/types.rs with the new qwen_accounts field. \
All done! The rotation config is ready."
));
assert!(has_phantom_tool_intent(
"Here's what I did:\n\
- Fixed the bug in src/brain/agent/service/tool_loop.rs\n\
- Updated the tests to match"
));
}
#[test]
fn phantom_tool_intent_git_amend_claim() {
use crate::brain::agent::service::has_phantom_tool_intent;
assert!(has_phantom_tool_intent(
"Let me amend that.\n\n\
Amended. Commit `4bd32a6` now says \"20 commits\" instead of \"18\"."
));
}
#[test]
fn phantom_tool_intent_multi_now_narration() {
use crate::brain::agent::service::has_phantom_tool_intent;
assert!(has_phantom_tool_intent(
"Now let me check the URL references at the bottom:\n\n\
Now I see the format. The changelog uses reference-style links at the bottom. I need to:\n\n\
Add the `[0.3.8]` section\n\
Update the `[Unreleased]` link to point to `v0.3.8`\n\
Add the `[0.3.8]` reference link at the bottom\n\n\
Now add the `[0.3.8]` reference link at the bottom:\n\n\
Now bump version to 0.3.8:\n\n\
Now run the full CI workflow:"
));
}
#[test]
fn phantom_tool_intent_backtick_code_reference() {
use crate::brain::agent::service::has_phantom_tool_intent;
assert!(has_phantom_tool_intent(
"Now let me add the `auth_invalidate_fn` field and builder method:"
));
assert!(has_phantom_tool_intent(
"Let me update the `PaneManager` struct to handle the new layout"
));
assert!(!has_phantom_tool_intent(
"The `auth_invalidate_fn` field controls token invalidation."
));
}
#[test]
fn phantom_tool_intent_past_tense_standalone() {
use crate::brain::agent::service::has_phantom_tool_intent;
assert!(has_phantom_tool_intent(
"Updated.\nFixed.\nThe version is now 0.3.8 and the changelog entry has been added for the release."
));
assert!(has_phantom_tool_intent(
"Amended. The commit message now reads correctly with the proper count.\n\
Committed. All changes are on the main branch and ready for push."
));
}
#[test]
fn strip_streamed_content_progress_event_carries_reason() {
use crate::brain::agent::ProgressEvent;
let event = ProgressEvent::StripStreamedContent {
bytes: 312,
reason: "gaslighting refusal preamble (312 bytes) stripped".to_string(),
};
match event {
ProgressEvent::StripStreamedContent { bytes, reason } => {
assert_eq!(bytes, 312);
assert!(reason.contains("gaslighting"));
assert!(reason.contains("312 bytes"));
}
_ => panic!("Expected StripStreamedContent variant"),
}
}
#[test]
fn has_investigative_intent_detects_original_phrases() {
use crate::brain::agent::service::has_investigative_intent;
assert!(has_investigative_intent("Let me dig into the issue"));
assert!(has_investigative_intent("I'll investigate this"));
assert!(has_investigative_intent("let me check the logs"));
assert!(has_investigative_intent("i'll search for"));
assert!(has_investigative_intent("let me look"));
}
#[test]
fn has_investigative_intent_detects_new_phrases() {
use crate::brain::agent::service::has_investigative_intent;
assert!(has_investigative_intent(
"Let me hunt down where LLM response text gets parsed"
));
assert!(has_investigative_intent("I'll trace the rendering path"));
assert!(has_investigative_intent("i'll track that down"));
assert!(has_investigative_intent(
"Let me look into the response parsing"
));
assert!(has_investigative_intent("I'll check into the tool output"));
assert!(has_investigative_intent("let me find out why"));
assert!(has_investigative_intent("I'll dig into the source"));
}
#[test]
fn has_investigative_intent_no_false_positives() {
use crate::brain::agent::service::has_investigative_intent;
assert!(!has_investigative_intent("That looks good"));
assert!(!has_investigative_intent("All done"));
assert!(!has_investigative_intent("The build succeeded"));
assert!(!has_investigative_intent("I think the issue is X"));
assert!(!has_investigative_intent("Great, thanks"));
assert!(!has_investigative_intent("I checked my email"));
assert!(!has_investigative_intent("The search button"));
assert!(!has_investigative_intent("A new look"));
}
#[test]
fn has_investigative_intent_mixed_case() {
use crate::brain::agent::service::has_investigative_intent;
assert!(has_investigative_intent("LET ME CHECK"));
assert!(has_investigative_intent("I'll Hunt For That"));
assert!(has_investigative_intent("Let Me Look Into"));
}
#[test]
fn has_investigative_intent_with_emoji() {
use crate::brain::agent::service::has_investigative_intent;
assert!(has_investigative_intent("Let me check 👍"));
assert!(has_investigative_intent("I'll find out what's going on 💪"));
}
#[test]
fn phantom_no_tools_catches_build_and_push() {
use crate::brain::agent::service::has_phantom_tool_intent_no_tools;
assert!(has_phantom_tool_intent_no_tools(
"Let me build and push the migrations now."
));
assert!(has_phantom_tool_intent_no_tools(
"I'll deploy the new schema to staging shortly."
));
assert!(has_phantom_tool_intent_no_tools(
"Now migrate the database to the new schema."
));
assert!(has_phantom_tool_intent_no_tools(
"Let's sync the live model catalog from the upstream API."
));
assert!(has_phantom_tool_intent_no_tools(
"Let me apply the patch and verify it lands cleanly."
));
}
#[test]
fn phantom_no_tools_catches_now_file_op_gerund() {
use crate::brain::agent::service::has_phantom_tool_intent_no_tools;
assert!(has_phantom_tool_intent_no_tools(
"Now creating the new tests file for signin/invitation error messages."
));
assert!(has_phantom_tool_intent_no_tools(
"Done with the previous step. Now writing the migration."
));
assert!(has_phantom_tool_intent_no_tools(
"Now editing the config to point at the new endpoint."
));
assert!(has_phantom_tool_intent_no_tools(
"Got it. Now reading the schema before I touch anything."
));
assert!(has_phantom_tool_intent_no_tools(
"Now installing the missing dependency and verifying."
));
}
#[test]
fn phantom_no_tools_catches_past_tense_completion_claim() {
use crate::brain::agent::service::has_phantom_tool_intent_no_tools;
assert!(has_phantom_tool_intent_no_tools(
"Pushed. Three new migrations added: enum_mappings, agency_integrations, property_sources."
));
assert!(has_phantom_tool_intent_no_tools(
"Deployed to prod. CI will run automatically."
));
assert!(has_phantom_tool_intent_no_tools(
"Merged to main. The change is live."
));
}
#[test]
fn phantom_no_tools_ignores_conversational_past_tense() {
use crate::brain::agent::service::has_phantom_tool_intent_no_tools;
assert!(!has_phantom_tool_intent_no_tools(
"I pushed the original branch up about three days ago and it has been sitting in CI ever since waiting for the reviewers to weigh in."
));
assert!(!has_phantom_tool_intent_no_tools(
"Yes, that approach should work fine for the use case you described."
));
}
#[test]
fn stuck_loop_catches_let_me_repetitions() {
use crate::brain::agent::service::{count_intent_line_starts, is_stuck_in_intent_loop};
let text = "\
Let me fetch issue #81 to understand what it's about.\n\n\
Let me pull up the issue to see what's really being reported.\n\n\
Let me fetch issue #81.\n\n\
OK, let me fetch issue #81 from GitHub.\n\n\
Let me pull up issue #81.";
assert_eq!(count_intent_line_starts(text), 5);
assert!(is_stuck_in_intent_loop(text));
}
#[test]
fn stuck_loop_threshold_is_three() {
use crate::brain::agent::service::{count_intent_line_starts, is_stuck_in_intent_loop};
let two = "Let me check the logs.\nI'll look at the diff.";
assert_eq!(count_intent_line_starts(two), 2);
assert!(!is_stuck_in_intent_loop(two), "2 reps is normal narration");
let three = "Let me check the logs.\nI'll look at the diff.\nNow let me run the tests.";
assert_eq!(count_intent_line_starts(three), 3);
assert!(is_stuck_in_intent_loop(three), "3 reps is stuck");
}
#[test]
fn stuck_loop_recognizes_mixed_openers() {
use crate::brain::agent::service::is_stuck_in_intent_loop;
let text = "Let me try one thing.\nI'll attempt the fetch.\nLet's pull the issue.\nNow check the issue.";
assert!(is_stuck_in_intent_loop(text));
}
#[test]
fn stuck_loop_handles_curly_apostrophe() {
use crate::brain::agent::service::count_intent_line_starts;
let text = "I’ll fetch the issue.\nI’ll look at the comments.\nI’ll summarize.";
assert_eq!(count_intent_line_starts(text), 3);
}
#[test]
fn stuck_loop_ignores_intent_inside_paragraphs() {
use crate::brain::agent::service::{count_intent_line_starts, is_stuck_in_intent_loop};
let text = "The plan is straightforward. Let me check the logs, then let me run the tests, and finally let me write up the results.";
assert_eq!(
count_intent_line_starts(text),
0,
"no leading intent — mid-paragraph occurrences don't count"
);
assert!(!is_stuck_in_intent_loop(text));
}
#[test]
fn stuck_loop_ignores_list_items_with_one_intent() {
use crate::brain::agent::service::is_stuck_in_intent_loop;
let text =
"- check the logs\n- review the diff\n- Let me know if anything's unclear\n- ship it";
assert!(!is_stuck_in_intent_loop(text));
}
#[test]
fn stuck_loop_normal_prose_does_not_trip() {
use crate::brain::agent::service::is_stuck_in_intent_loop;
let text = "Sure, I can help with that. The fix lives in `src/lib.rs` and only \
needs a small adjustment to the parser. Want me to apply it?";
assert!(!is_stuck_in_intent_loop(text));
}
#[test]
fn phantom_detects_russian_intent_narration() {
use crate::brain::agent::service::has_phantom_tool_intent_no_tools;
let text = "Давайте проверю логи и найдём ошибку в системе сейчас.";
assert!(
has_phantom_tool_intent_no_tools(text),
"Russian intent narration must phantom-detect: {text}"
);
}
#[test]
fn phantom_detects_spanish_intent_narration() {
use crate::brain::agent::service::has_phantom_tool_intent_no_tools;
let text = "Déjame revisar los logs y arreglar la configuración ¿de acuerdo?";
assert!(
has_phantom_tool_intent_no_tools(text),
"Spanish intent narration must phantom-detect: {text}"
);
}
#[test]
fn phantom_detects_portuguese_intent_narration() {
use crate::brain::agent::service::has_phantom_tool_intent_no_tools;
let text = "Deixe-me verificar o arquivo e corrigir a configuração agora.";
assert!(
has_phantom_tool_intent_no_tools(text),
"Portuguese intent narration must phantom-detect: {text}"
);
}
#[test]
fn phantom_detects_french_intent_narration() {
use crate::brain::agent::service::has_phantom_tool_intent_no_tools;
let text = "Laissez-moi vérifier le fichier et corriger l'erreur à l'instant même.";
assert!(
has_phantom_tool_intent_no_tools(text),
"French intent narration must phantom-detect: {text}"
);
}
#[test]
fn phantom_english_still_default_on_pure_ascii() {
use crate::brain::agent::service::has_phantom_tool_intent_no_tools;
let text = "Let me check the logs and find the issue in the system.";
assert!(
has_phantom_tool_intent_no_tools(text),
"Plain ASCII English must still detect via EN config: {text}"
);
}