use crate::brain::agent::service::tool_loop::{
build_tool_result_content, extract_path_for_recent_buffer, is_user_correction,
strip_ansi_output,
};
use serde_json::json;
use std::path::PathBuf;
#[test]
fn strip_ansi_output_removes_basic_color_codes() {
let raw = "\x1b[31merror\x1b[0m: tool failed";
let cleaned = strip_ansi_output(raw);
assert_eq!(cleaned, "error: tool failed");
}
#[test]
fn strip_ansi_output_handles_no_ansi() {
let raw = "plain output, no escapes here";
let cleaned = strip_ansi_output(raw);
assert_eq!(cleaned, raw);
}
#[test]
fn strip_ansi_output_handles_empty_string() {
assert_eq!(strip_ansi_output(""), "");
}
#[test]
fn strip_ansi_output_strips_cursor_movement() {
let raw = "\x1b[2J\x1b[Hready";
assert_eq!(strip_ansi_output(raw), "ready");
}
#[test]
fn strip_ansi_output_preserves_unicode() {
let raw = "🦀 OpenCrabs";
assert_eq!(strip_ansi_output(raw), "🦀 OpenCrabs");
}
#[test]
fn strip_ansi_output_strips_24bit_truecolor() {
let raw = "\x1b[38;2;255;100;0morange\x1b[0m";
assert_eq!(strip_ansi_output(raw), "orange");
}
fn cwd() -> PathBuf {
std::env::temp_dir().join("opencrabs-tool-loop-test")
}
#[test]
fn extract_path_returns_none_for_unrelated_tools() {
for tool in &["bash", "glob", "web_search", "generate_image", "task"] {
let result = extract_path_for_recent_buffer(tool, &json!({}), &cwd());
assert!(
result.is_none(),
"tool '{tool}' should never contribute to recent_paths"
);
}
}
#[test]
fn extract_path_returns_none_when_path_missing() {
let result = extract_path_for_recent_buffer("read_file", &json!({}), &cwd());
assert!(result.is_none());
}
#[test]
fn extract_path_returns_none_for_empty_path() {
for empty in &["", " ", " \t\n "] {
let result = extract_path_for_recent_buffer("read_file", &json!({ "path": empty }), &cwd());
assert!(
result.is_none(),
"empty path '{empty:?}' must not be recorded"
);
}
}
#[test]
fn extract_path_resolves_relative_against_cwd() {
let result =
extract_path_for_recent_buffer("read_file", &json!({ "path": "src/main.rs" }), &cwd());
let path = result.expect("relative path must resolve");
assert!(
path.is_absolute(),
"stored path should be absolute, got {path:?}"
);
assert!(
path.ends_with("src/main.rs"),
"tail must preserve the original path, got {path:?}"
);
}
#[test]
#[cfg(unix)]
fn extract_path_passes_through_absolute_path() {
let result =
extract_path_for_recent_buffer("read_file", &json!({ "path": "/etc/hosts" }), &cwd());
assert_eq!(result, Some(PathBuf::from("/etc/hosts")));
}
#[test]
#[cfg(windows)]
fn extract_path_passes_through_absolute_path() {
let result = extract_path_for_recent_buffer(
"read_file",
&json!({ "path": "C:\\Windows\\System32\\drivers\\etc\\hosts" }),
&cwd(),
);
assert_eq!(
result,
Some(PathBuf::from("C:\\Windows\\System32\\drivers\\etc\\hosts"))
);
}
#[test]
#[cfg(unix)]
fn extract_path_covers_all_documented_tools() {
for tool in &["read_file", "edit_file", "write_file", "ls", "grep"] {
let result = extract_path_for_recent_buffer(tool, &json!({ "path": "/abs/file" }), &cwd());
assert_eq!(
result,
Some(PathBuf::from("/abs/file")),
"tool '{tool}' must contribute to recent_paths"
);
}
}
#[test]
#[cfg(windows)]
fn extract_path_covers_all_documented_tools() {
for tool in &["read_file", "edit_file", "write_file", "ls", "grep"] {
let result =
extract_path_for_recent_buffer(tool, &json!({ "path": "C:\\abs\\file" }), &cwd());
assert_eq!(
result,
Some(PathBuf::from("C:\\abs\\file")),
"tool '{tool}' must contribute to recent_paths"
);
}
}
#[test]
fn user_correction_detects_short_no_phrases() {
for msg in &[
"no, that's wrong",
"no.",
"no!",
"no that's not what I asked",
"nope",
"wrong",
"that's not right",
"that's wrong",
"thats wrong",
"not what i wanted",
"try again",
"redo",
"revert",
"undo",
"you broke it",
"broke it",
"doesn't work",
"doesnt work",
"didn't work",
"didnt work",
"not working",
"stop",
"don't do that",
"dont do that",
"i said no",
"i asked for X",
"not correct",
"fix it",
"fix this",
] {
assert!(
is_user_correction(msg),
"message {msg:?} should be detected as a correction"
);
}
}
#[test]
fn user_correction_ignores_long_messages() {
let long = "I have a new task involving the API surface for our \
ingestion pipeline. Please don't do the same approach as \
last time — that hit a deadlock under concurrent load. \
Instead, write a server in Rust that responds to GET \
requests with JSON. Use Tokio + axum. Implement /health, \
/metrics, /readyz, /livez, and /version endpoints. Don't \
include any frontend assets, TLS termination, or auth \
middleware. Stop after the server is running locally and \
report back with the full route table, the listening \
address, and the binary footprint. Make sure tests pass.";
assert!(
long.len() > 500,
"test fixture must actually be long ({}) for the gate to be exercised",
long.len()
);
assert!(
!is_user_correction(long),
"long instruction must NOT be classified as correction"
);
}
#[test]
fn user_correction_ignores_extremely_short_messages() {
for msg in &["", "?"] {
assert!(
!is_user_correction(msg),
"extremely short message {msg:?} must not trigger correction path"
);
}
}
#[test]
fn user_correction_is_case_insensitive() {
for msg in &["NO!", "WRONG", "Try Again", "FIX IT"] {
assert!(
is_user_correction(msg),
"uppercase {msg:?} must be detected"
);
}
}
#[test]
fn user_correction_does_not_fire_on_neutral_prose() {
for msg in &[
"what is the capital of France?",
"show me the diff",
"let's add a new feature",
"explain how this works",
"thanks",
"ok",
"got it",
] {
assert!(
!is_user_correction(msg),
"neutral message {msg:?} must NOT be classified as correction"
);
}
}
#[test]
fn user_correction_only_scans_first_300_chars() {
let prefix = "x".repeat(300);
let buried = format!("{prefix} stop please");
assert!(buried.len() <= 500);
assert!(
!is_user_correction(&buried),
"trigger word past 300-char window must not match"
);
}
#[test]
fn build_tool_result_success_returns_output() {
let content = build_tool_result_content(true, None, "command output");
assert_eq!(content, "command output");
}
#[test]
fn build_tool_result_failure_includes_error_and_output() {
let content = build_tool_result_content(
false,
Some("Command exited with code 1".to_string()),
"stderr: file not found",
);
assert!(content.contains("Command exited with code 1"));
assert!(content.contains("-- output captured before error --"));
assert!(content.contains("stderr: file not found"));
}
#[test]
fn build_tool_result_failure_strips_ansi_from_error() {
let content = build_tool_result_content(
false,
Some("\x1b[31merror\x1b[0m: build failed".to_string()),
"",
);
assert!(!content.contains("\x1b["));
assert!(content.contains("error: build failed"));
}
#[test]
fn build_tool_result_failure_strips_ansi_from_output() {
let content = build_tool_result_content(
false,
Some("error".to_string()),
"\x1b[32msuccess\x1b[0m: compiled",
);
assert!(!content.contains("\x1b["));
assert!(content.contains("success: compiled"));
}
#[test]
fn build_tool_result_failure_caps_output_at_8000_chars() {
let large_output = "x".repeat(10000);
let content = build_tool_result_content(false, Some("error".to_string()), &large_output);
assert!(content.contains("(output truncated)"));
assert!(content.contains("xxx"));
}
#[test]
fn build_tool_result_failure_no_truncation_for_small_output() {
let content = build_tool_result_content(false, Some("error".to_string()), "small output");
assert!(!content.contains("(output truncated)"));
assert!(content.contains("small output"));
}
#[test]
fn build_tool_result_failure_default_error_message() {
let content = build_tool_result_content(false, None, "some output");
assert!(content.contains("Tool execution failed"));
}