use super::*;
#[test]
fn detects_period_8_triple_repeat() {
let pat: Vec<u32> = (1..=8).collect();
let mut tokens: Vec<u32> = (0..40).collect();
tokens.extend(pat.iter());
tokens.extend(pat.iter());
tokens.extend(pat.iter());
assert!(detect_thinking_token_loop(
&tokens,
WatchdogParams::default()
));
}
#[test]
fn rejects_two_repeats() {
let pat: Vec<u32> = (100..=104).collect();
let mut tokens: Vec<u32> = (0u32..50).collect();
tokens.extend(pat.iter());
tokens.extend(pat.iter());
assert!(!detect_thinking_token_loop(
&tokens,
WatchdogParams::default()
));
}
#[test]
fn rejects_numbered_list_reasoning() {
let tokens: Vec<u32> = (0u32..80).collect();
assert!(!detect_thinking_token_loop(
&tokens,
WatchdogParams::default()
));
}
#[test]
fn detects_short_period_fence_loop() {
let pat: Vec<u32> = vec![7, 6, 5, 4, 3, 2, 1, 0, 9, 8];
let mut tokens: Vec<u32> = (100u32..150).collect();
for _ in 0..4 {
tokens.extend(pat.iter());
}
assert!(detect_thinking_token_loop(
&tokens,
WatchdogParams::default()
));
}
#[test]
fn rejects_fence_body_with_varying_prefixes() {
let fence: Vec<u32> = vec![100, 101, 102, 103, 104, 105, 106, 107, 108, 109];
let prefixes: [&[u32]; 4] = [
&[200, 201],
&[202, 203],
&[204, 205, 206],
&[207],
];
let mut tokens: Vec<u32> = (0..30).collect();
for pre in prefixes.iter() {
tokens.extend(pre.iter());
tokens.extend(fence.iter());
}
assert!(
!detect_thinking_token_loop(&tokens, WatchdogParams::default()),
"end-anchored detector intentionally does not fire on varying-prefix patterns"
);
}
#[test]
fn content_loop_detects_sentence_triple_repeat() {
let sentence: Vec<u32> = (1000..1022).collect();
let mut tokens: Vec<u32> = (0..100).collect();
tokens.extend(sentence.iter());
tokens.extend(sentence.iter());
tokens.extend(sentence.iter());
assert!(
detect_content_token_loop(&tokens),
"22-token sentence repeating 3× must trigger content-loop watchdog"
);
}
#[test]
fn content_loop_rejects_short_responses() {
let pat: Vec<u32> = (1..=10).collect();
let prior_len = (CONTENT_LOOP_MIN_TOKENS as usize).saturating_sub(3 * pat.len() + 4);
let mut tokens: Vec<u32> = (50u32..50 + prior_len as u32).collect();
tokens.extend(pat.iter());
tokens.extend(pat.iter());
tokens.extend(pat.iter());
assert!(
tokens.len() < CONTENT_LOOP_MIN_TOKENS as usize,
"test setup error: tokens.len()={} exceeds MIN_TOKENS={}",
tokens.len(),
CONTENT_LOOP_MIN_TOKENS,
);
assert!(
!detect_content_token_loop(&tokens),
"responses under {} tokens must not trigger watchdog",
CONTENT_LOOP_MIN_TOKENS
);
}
#[test]
fn content_loop_rejects_legitimate_prose() {
let tokens: Vec<u32> = (0u32..200).collect();
assert!(
!detect_content_token_loop(&tokens),
"legitimate prose with no repeat must not trigger watchdog"
);
}
#[test]
fn content_loop_accepts_three_repeats() {
let sentence: Vec<u32> = (500..530).collect();
let mut tokens: Vec<u32> = (0..100).collect();
tokens.extend(sentence.iter());
tokens.extend(sentence.iter());
tokens.extend(sentence.iter());
assert!(
detect_content_token_loop(&tokens),
"three byte-exact 30-token repeats must trigger watchdog at MIN_REPEATS={}",
CONTENT_LOOP_MIN_REPEATS,
);
}
#[test]
fn content_loop_rejects_two_repeats() {
let sentence: Vec<u32> = (500..530).collect();
let mut tokens: Vec<u32> = (0..100).collect();
tokens.extend(sentence.iter());
tokens.extend(sentence.iter());
assert!(
!detect_content_token_loop(&tokens),
"two byte-exact 30-token repeats must NOT trigger at MIN_REPEATS={}",
CONTENT_LOOP_MIN_REPEATS,
);
}
#[test]
fn content_loop_rejects_single_occurrence() {
let sentence: Vec<u32> = (500..530).collect();
let mut tokens: Vec<u32> = (0..100).collect();
tokens.extend(sentence.iter());
assert!(
!detect_content_token_loop(&tokens),
"single occurrence (no repeat) must not trigger watchdog"
);
}
fn numeric_mask() -> Vec<bool> {
let mut m = vec![false; 1100];
for (i, slot) in m.iter_mut().enumerate() {
*slot = (100..=199).contains(&i);
}
m
}
fn varying_template_stream(repeats: u32) -> Vec<u32> {
let mut t: Vec<u32> = (900u32..990).collect();
for k in 0..repeats {
t.extend([1, 2, 3, 4, 5, 6]);
t.push(100 + k);
t.extend([7, 8, 9, 10, 11]);
}
t
}
#[test]
fn norm_fires_on_varying_numeric_template() {
let t = varying_template_stream(5);
let mask = numeric_mask();
assert!(
!detect_content_token_loop(&t),
"exact detector must miss: integer tokens differ every repeat"
);
assert!(
detect_content_token_loop_normalized(&t, &mask),
"normalized detector must catch the fixed template (5 repeats >= 4)"
);
}
#[test]
fn norm_rejects_3item_list_and_pure_columns() {
let mask = numeric_mask();
let three = varying_template_stream(3);
assert!(
!detect_content_token_loop_normalized(&three, &mask),
"3 repeats < NORM_MIN_REPEATS=4 must not fire"
);
let mut col: Vec<u32> = (900u32..990).collect();
for k in 0..6 {
col.extend([100 + k; 12]);
}
assert!(
!detect_content_token_loop_normalized(&col, &mask),
"pure-number period (no structural token) is the exact path's job"
);
let mut prose: Vec<u32> = (900u32..960).collect();
for _ in 0..6 {
prose.extend([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
}
assert!(
!detect_content_token_loop_normalized(&prose, &mask),
"pure-prose period (no numeric) must defer to exact detector"
);
}
#[test]
fn exact_prose_loop_still_caught_regression() {
let mut t: Vec<u32> = (900u32..990).collect();
for _ in 0..4 {
t.extend([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
}
assert!(
detect_content_token_loop(&t),
"exact byte-identical content loop must still be caught"
);
}
#[test]
fn norm_fires_on_variable_length_digit_runs() {
let mask = numeric_mask();
let mut t: Vec<u32> = (900u32..990).collect();
for k in 0..5u32 {
t.extend([1, 2, 3]);
t.extend(std::iter::repeat_n(100 + (k % 10), 2 + (k % 2) as usize));
t.extend([4, 5]);
t.extend(std::iter::repeat_n(101 + (k % 9), 9 + k as usize));
t.extend([6, 7, 8]);
}
assert!(
!detect_content_token_loop(&t),
"exact detector misses: digit-run lengths differ every line"
);
assert!(
detect_content_token_loop_normalized(&t, &mask),
"run-collapse must catch the variable-length digit-run template"
);
}
#[test]
fn norm_inert_with_empty_mask() {
let t = varying_template_stream(5);
assert!(
!detect_content_token_loop_normalized(&t, &[]),
"empty mask must make the normalized path inert (fail-open)"
);
}
#[test]
fn override_loosens_content_loop_threshold() {
let sentence: Vec<u32> = (1000..1022).collect();
let mut tokens: Vec<u32> = (0..100).collect();
tokens.extend(sentence.iter());
tokens.extend(sentence.iter());
tokens.extend(sentence.iter());
assert!(
detect_content_token_loop_with(&tokens, None),
"default thresholds must still fire on 22-token × 3 repeat"
);
let strict = crate::api::inference_types::RepetitionDetectionParams {
min_pattern_size: 2,
max_pattern_size: 64,
min_count: 4,
};
assert!(
!detect_content_token_loop_with(&tokens, Some(strict)),
"stricter min_count=4 override must suppress 3-repeat firing"
);
}
#[test]
fn override_tightens_content_loop_threshold() {
let pat: Vec<u32> = vec![42, 43, 44, 45, 46];
let mut tokens: Vec<u32> = (0u32..50).collect();
for _ in 0..5 {
tokens.extend(pat.iter());
}
let permissive = crate::api::inference_types::RepetitionDetectionParams {
min_pattern_size: 5,
max_pattern_size: 5,
min_count: 3,
};
assert!(
detect_content_token_loop_with(&tokens, Some(permissive)),
"override (period=5, min_count=3) must catch 5×period-5 tail"
);
}
#[test]
fn override_applies_to_thinking_loop() {
let pat: Vec<u32> = vec![7, 6, 5, 4, 3, 2, 1, 0, 9, 8];
let mut tokens: Vec<u32> = (100u32..150).collect();
for _ in 0..4 {
tokens.extend(pat.iter());
}
assert!(
detect_thinking_token_loop_with(&tokens, None, WatchdogParams::default()),
"default thinking-loop thresholds must still fire on 4× period-10"
);
let strict = crate::api::inference_types::RepetitionDetectionParams {
min_pattern_size: 4,
max_pattern_size: 20,
min_count: 6,
};
assert!(
!detect_thinking_token_loop_with(&tokens, Some(strict), WatchdogParams::default()),
"stricter min_count=6 override must suppress 4-repeat firing"
);
}
#[test]
fn forced_token_fastpath_default_enabled() {
assert!(parse_forced_token_fastpath(None));
}
#[test]
fn forced_token_fastpath_disabled_by_truthy() {
assert!(!parse_forced_token_fastpath(Some("1")));
assert!(!parse_forced_token_fastpath(Some("true")));
assert!(!parse_forced_token_fastpath(Some("TRUE")));
assert!(!parse_forced_token_fastpath(Some(" true ")));
}
#[test]
fn forced_token_fastpath_enabled_by_falsy_or_junk() {
assert!(parse_forced_token_fastpath(Some("0")));
assert!(parse_forced_token_fastpath(Some("false")));
assert!(parse_forced_token_fastpath(Some("")));
assert!(parse_forced_token_fastpath(Some("yes")));
}
#[test]
fn watchdog_defaults_arm_the_think_loop_watchdog() {
let d = WatchdogParams::default();
assert!(
d.enable_think_loop_watchdog,
"think-loop watchdog must default ON (opt-out per model)"
);
}