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());
}