use crate::brain::tools::bash::{
RECENT_BASH_MAX_BLOCKS, RECENT_BASH_TTL, RECENT_BASH_WINDOW, check_recent_failure,
record_bash_block, record_bash_outcome, record_bash_outcome_at, recorded_at_of,
};
use std::time::{Duration, Instant};
use uuid::Uuid;
#[test]
fn fresh_session_has_no_history() {
let sid = Uuid::new_v4();
assert!(check_recent_failure(sid, "ls -la").is_none());
}
#[test]
fn successful_run_does_not_block_retry() {
let sid = Uuid::new_v4();
record_bash_outcome(sid, "git status".to_string(), false, None);
assert!(check_recent_failure(sid, "git status").is_none());
}
#[test]
fn failed_run_blocks_exact_retry() {
let sid = Uuid::new_v4();
record_bash_outcome(
sid,
"git push origin main".to_string(),
true,
Some("rejected: non-fast-forward".to_string()),
);
let msg = check_recent_failure(sid, "git push origin main").expect("should block");
assert!(msg.contains("already ran this exact command"));
assert!(msg.contains("Previous error:"));
assert!(msg.contains("non-fast-forward"));
}
#[test]
fn whitespace_normalization_catches_cosmetic_retries() {
let sid = Uuid::new_v4();
record_bash_outcome(
sid,
"git pull".to_string(),
true,
Some("conflict".to_string()),
);
assert!(check_recent_failure(sid, " git pull ").is_some());
}
#[test]
fn different_command_is_not_blocked() {
let sid = Uuid::new_v4();
record_bash_outcome(sid, "git push origin main".to_string(), true, None);
assert!(check_recent_failure(sid, "git push origin main --force-with-lease").is_none());
assert!(check_recent_failure(sid, "git push origin foo").is_none());
}
#[test]
fn move_to_front_keeps_one_entry_per_unique_command() {
let sid = Uuid::new_v4();
record_bash_outcome(sid, "cmd_a".to_string(), true, Some("err1".to_string()));
record_bash_outcome(sid, "cmd_b".to_string(), false, None);
record_bash_outcome(sid, "cmd_a".to_string(), true, Some("err2".to_string()));
let msg = check_recent_failure(sid, "cmd_a").expect("should block");
assert!(msg.contains("err2"));
assert!(!msg.contains("err1"));
}
#[test]
fn outcome_can_flip_from_fail_to_success() {
let sid = Uuid::new_v4();
record_bash_outcome(sid, "make build".to_string(), true, Some("err".to_string()));
assert!(check_recent_failure(sid, "make build").is_some());
record_bash_outcome(sid, "make build".to_string(), false, None);
assert!(check_recent_failure(sid, "make build").is_none());
}
#[test]
fn old_failure_falls_off_window() {
let sid = Uuid::new_v4();
record_bash_outcome(
sid,
"old_failing_cmd".to_string(),
true,
Some("err".to_string()),
);
for i in 0..RECENT_BASH_WINDOW {
record_bash_outcome(sid, format!("filler_cmd_{}", i), false, None);
}
assert!(check_recent_failure(sid, "old_failing_cmd").is_none());
}
#[test]
fn sessions_are_isolated() {
let sid_a = Uuid::new_v4();
let sid_b = Uuid::new_v4();
record_bash_outcome(
sid_a,
"broken_cmd".to_string(),
true,
Some("err".to_string()),
);
assert!(check_recent_failure(sid_a, "broken_cmd").is_some());
assert!(check_recent_failure(sid_b, "broken_cmd").is_none());
}
#[test]
fn rejection_message_names_the_window_size() {
let sid = Uuid::new_v4();
record_bash_outcome(sid, "x".to_string(), true, None);
let msg = check_recent_failure(sid, "x").expect("should block");
assert!(msg.contains(&RECENT_BASH_WINDOW.to_string()));
}
#[test]
fn rejection_message_carries_the_shared_variation_directive() {
let sid = Uuid::new_v4();
record_bash_outcome(
sid,
"oc-deploy ship --sha abc123".to_string(),
true,
Some("already shipped by sibling lane".to_string()),
);
let msg = check_recent_failure(sid, "oc-deploy ship --sha abc123").expect("should block");
assert!(msg.contains("Do not re-issue the same call"), "{msg}");
assert!(msg.contains("result you already have"), "{msg}");
assert!(msg.contains("report completion"), "{msg}");
assert!(msg.contains("already ran this exact command"), "{msg}");
assert!(msg.contains("Previous error:"), "{msg}");
}
#[test]
fn stale_failure_stops_blocking() {
let sid = Uuid::new_v4();
let past = Instant::now()
.checked_sub(RECENT_BASH_TTL + Duration::from_secs(1))
.expect("monotonic clock underflow");
record_bash_outcome_at(
sid,
"failing-cmd".to_string(),
true,
Some("transient error".to_string()),
past,
);
assert!(
check_recent_failure(sid, "failing-cmd").is_none(),
"stale failure older than TTL should yield execution"
);
}
#[test]
fn block_does_not_refresh_the_clock() {
let sid = Uuid::new_v4();
let initial_t = Instant::now()
.checked_sub(Duration::from_secs(60))
.expect("monotonic clock underflow");
record_bash_outcome_at(
sid,
"failing-cmd".to_string(),
true,
Some("transient error".to_string()),
initial_t,
);
let t_before = recorded_at_of(sid, "failing-cmd").expect("entry must exist");
assert_eq!(t_before, initial_t);
record_bash_block(sid, "failing-cmd");
let t_after = recorded_at_of(sid, "failing-cmd").expect("entry must exist");
assert_eq!(
t_after, initial_t,
"record_bash_block must NOT refresh recorded_at"
);
}
#[test]
fn repeated_blocks_eventually_yield() {
let sid = Uuid::new_v4();
record_bash_outcome(
sid,
"failing-cmd".to_string(),
true,
Some("transient error".to_string()),
);
for i in 0..RECENT_BASH_MAX_BLOCKS {
assert!(
check_recent_failure(sid, "failing-cmd").is_some(),
"attempt {i} should be blocked within budget"
);
record_bash_block(sid, "failing-cmd");
}
assert!(
check_recent_failure(sid, "failing-cmd").is_none(),
"after budget exhausted, check_recent_failure must yield one real execution"
);
}
#[test]
fn real_outcome_resets_the_block_budget() {
let sid = Uuid::new_v4();
record_bash_outcome(
sid,
"failing-cmd".to_string(),
true,
Some("transient error".to_string()),
);
for _ in 0..RECENT_BASH_MAX_BLOCKS {
record_bash_block(sid, "failing-cmd");
}
assert!(check_recent_failure(sid, "failing-cmd").is_none());
record_bash_outcome(
sid,
"failing-cmd".to_string(),
true,
Some("persisting error".to_string()),
);
assert!(
check_recent_failure(sid, "failing-cmd").is_some(),
"new real failure outcome must re-arm block budget"
);
}
#[test]
fn stale_entry_does_not_block_a_different_command() {
let sid = Uuid::new_v4();
record_bash_outcome(
sid,
"first-cmd".to_string(),
true,
Some("error 1".to_string()),
);
assert!(check_recent_failure(sid, "different-cmd").is_none());
}
#[test]
fn a_successful_non_bash_call_clears_the_recorded_failure() {
let sid = Uuid::new_v4();
record_bash_outcome(
sid,
"cat release.cfg".to_string(),
true,
Some("cat: release.cfg: No such file or directory".to_string()),
);
assert!(check_recent_failure(sid, "cat release.cfg").is_some());
crate::brain::tools::bash::note_environment_changed(sid);
assert!(
check_recent_failure(sid, "cat release.cfg").is_none(),
"the missing file now exists, so the recorded error is obsolete"
);
}
#[test]
fn a_new_turn_clears_the_recorded_failure() {
let sid = Uuid::new_v4();
record_bash_outcome(
sid,
"cat release.cfg".to_string(),
true,
Some("cat: release.cfg: No such file or directory".to_string()),
);
crate::brain::tools::bash::note_environment_changed(sid);
assert!(check_recent_failure(sid, "cat release.cfg").is_none());
}
#[test]
fn clearing_one_session_leaves_another_sessions_history_intact() {
let cleared = Uuid::new_v4();
let other = Uuid::new_v4();
record_bash_outcome(
cleared,
"make build".to_string(),
true,
Some("error".to_string()),
);
record_bash_outcome(
other,
"make build".to_string(),
true,
Some("error".to_string()),
);
crate::brain::tools::bash::note_environment_changed(cleared);
assert!(check_recent_failure(cleared, "make build").is_none());
assert!(
check_recent_failure(other, "make build").is_some(),
"another session's failure history is not this session's business"
);
}
#[test]
fn a_loop_still_blocks_after_a_clear_because_bash_never_clears_itself() {
let sid = Uuid::new_v4();
record_bash_outcome(
sid,
"cargo build".to_string(),
true,
Some("error[E0308]".to_string()),
);
assert!(check_recent_failure(sid, "cargo build").is_some());
record_bash_block(sid, "cargo build");
assert!(
check_recent_failure(sid, "cargo build").is_some(),
"back-to-back bash retries must stay blocked"
);
}
mod registry_wiring {
use super::*;
use crate::brain::tools::{
Tool, ToolCapability, ToolExecutionContext, ToolRegistry, ToolResult,
};
use async_trait::async_trait;
use serde_json::Value;
use std::sync::Arc;
struct Stub {
name: &'static str,
capability: ToolCapability,
}
#[async_trait]
impl Tool for Stub {
fn name(&self) -> &str {
self.name
}
fn description(&self) -> &str {
"stub"
}
fn input_schema(&self) -> Value {
serde_json::json!({"type": "object", "properties": {}})
}
fn capabilities(&self) -> Vec<ToolCapability> {
vec![self.capability]
}
fn requires_approval(&self) -> bool {
false
}
async fn execute(
&self,
_input: Value,
_context: &ToolExecutionContext,
) -> crate::brain::tools::Result<ToolResult> {
Ok(ToolResult::success("ok".to_string()))
}
}
fn registry_with(name: &'static str, capability: ToolCapability) -> ToolRegistry {
let registry = ToolRegistry::new();
registry.register(Arc::new(Stub { name, capability }));
registry
}
async fn dispatch(registry: &ToolRegistry, name: &str, session_id: Uuid) {
let context = ToolExecutionContext::new(session_id).with_auto_approve(true);
registry
.execute(name, serde_json::json!({}), &context)
.await
.expect("stub dispatch");
}
#[tokio::test]
async fn a_successful_write_clears_the_window() {
let sid = Uuid::new_v4();
record_bash_outcome(
sid,
"cat release.cfg".to_string(),
true,
Some("cat: release.cfg: No such file or directory".to_string()),
);
let registry = registry_with("write_file", ToolCapability::WriteFiles);
dispatch(®istry, "write_file", sid).await;
assert!(
check_recent_failure(sid, "cat release.cfg").is_none(),
"the file now exists, so the retry must be allowed"
);
}
#[tokio::test]
async fn a_read_does_not_clear_the_window() {
let sid = Uuid::new_v4();
record_bash_outcome(
sid,
"cat release.cfg".to_string(),
true,
Some("cat: release.cfg: No such file or directory".to_string()),
);
let registry = registry_with("read_file", ToolCapability::ReadFiles);
dispatch(®istry, "read_file", sid).await;
assert!(
check_recent_failure(sid, "cat release.cfg").is_some(),
"nothing changed on disk, so this is still the loop the guard stops"
);
}
}