use crate::brain::agent::service::tool_repeat::{
REPEAT_NUDGE_AT, RepeatVerdict, ToolRepeatTracker, observe_round, signature,
};
use serde_json::json;
fn sig(name: &str, input: serde_json::Value) -> String {
signature(name, &input)
}
#[test]
fn a_first_call_is_fresh() {
let mut t = ToolRepeatTracker::new();
assert_eq!(t.observe("bash|{}"), RepeatVerdict::Fresh);
assert_eq!(t.consecutive(), 1);
}
#[test]
fn a_different_call_resets_the_run() {
let mut t = ToolRepeatTracker::new();
t.observe("bash|{\"a\":1}");
t.observe("bash|{\"a\":1}");
assert_eq!(t.observe("bash|{\"a\":2}"), RepeatVerdict::Fresh);
assert_eq!(t.consecutive(), 1);
}
#[test]
fn the_nudge_fires_exactly_at_the_threshold() {
let mut t = ToolRepeatTracker::new();
let s = "bash|{}";
for i in 1..REPEAT_NUDGE_AT {
let verdict = t.observe(s);
assert!(
!matches!(verdict, RepeatVerdict::NudgeNow(_)),
"must not fire at {i}, below the threshold"
);
}
assert_eq!(
t.observe(s),
RepeatVerdict::NudgeNow(REPEAT_NUDGE_AT),
"fires on the call that reaches the threshold"
);
}
#[test]
fn it_nudges_once_per_run_not_every_round() {
let mut t = ToolRepeatTracker::new();
let s = "bash|{}";
for _ in 0..REPEAT_NUDGE_AT {
t.observe(s);
}
for _ in 0..5 {
assert!(!matches!(t.observe(s), RepeatVerdict::NudgeNow(_)));
}
}
#[test]
fn a_new_run_can_be_nudged_again() {
let mut t = ToolRepeatTracker::new();
for _ in 0..REPEAT_NUDGE_AT {
t.observe("bash|{\"a\":1}");
}
t.observe("bash|{\"a\":2}");
for _ in 2..REPEAT_NUDGE_AT {
t.observe("bash|{\"a\":2}");
}
assert!(matches!(
t.observe("bash|{\"a\":2}"),
RepeatVerdict::NudgeNow(_)
));
}
#[test]
fn a_reset_clears_the_run_so_a_replay_cannot_inflate_it() {
let mut t = ToolRepeatTracker::new();
let s = "bash|{}";
for _ in 1..REPEAT_NUDGE_AT {
t.observe(s);
}
t.reset();
assert_eq!(t.consecutive(), 0);
assert_eq!(t.observe(s), RepeatVerdict::Fresh);
}
#[test]
fn argument_key_order_cannot_change_identity() {
let a = sig("bash", json!({"command": "ls", "cwd": "/tmp"}));
let b = sig("bash", json!({"cwd": "/tmp", "command": "ls"}));
assert_eq!(a, b);
}
#[test]
fn nested_object_key_order_also_cannot_change_identity() {
let a = sig("run", json!({"opts": {"x": 1, "y": 2}}));
let b = sig("run", json!({"opts": {"y": 2, "x": 1}}));
assert_eq!(a, b);
}
#[test]
fn array_order_still_distinguishes_calls() {
let a = sig("run", json!({"args": ["a", "b"]}));
let b = sig("run", json!({"args": ["b", "a"]}));
assert_ne!(a, b);
}
#[test]
fn a_different_tool_with_identical_arguments_is_a_different_call() {
assert_ne!(sig("bash", json!({"x": 1})), sig("read", json!({"x": 1})));
}
#[test]
fn an_empty_round_is_ignored() {
let mut t = ToolRepeatTracker::new();
assert!(observe_round(&mut t, "", None).is_none());
assert_eq!(t.consecutive(), 0);
}
#[test]
fn the_correction_names_the_tool_and_offers_a_way_out() {
let mut t = ToolRepeatTracker::new();
let s = sig("bash", json!({"command": "ls"}));
let mut nudge = None;
for _ in 0..REPEAT_NUDGE_AT {
nudge = observe_round(&mut t, &s, Some("bash".to_string()));
}
let nudge = nudge.expect("the threshold round yields a correction");
assert!(nudge.contains("bash"), "names the offending tool");
assert!(
nudge.contains("cannot return anything different"),
"states the mechanism rather than scolding"
);
assert!(
nudge.contains("explain what you need"),
"offers an exit that is not another tool call"
);
}
#[test]
fn a_parallel_round_compares_as_a_unit() {
let round = [
sig("bash", json!({"command": "ls"})),
sig("read", json!({"path": "/tmp/x"})),
]
.join("\n");
let mut t = ToolRepeatTracker::new();
assert_eq!(t.observe(&round), RepeatVerdict::Fresh);
assert!(matches!(t.observe(&round), RepeatVerdict::Repeating(2)));
}