captchaforge 0.2.39

Captcha detection and solving for Firefox and BiDi-driven browsers. Detection, vendor solver scaffolding, trusted cross-origin click delivery into nested OOPIFs, and stealth personas are implemented and tested; broad live-vendor solve rates are not yet benchmarked.
Documentation
use super::*;

#[test]
fn audio_solver_custom_endpoint() {
    let s = AudioCaptchaSolver::new().with_stt_endpoint("http://custom:9999/stt");
    assert_eq!(s.stt_endpoint, "http://custom:9999/stt");
}

#[test]
fn jittered_audio_delay_varies_and_stays_in_band() {
    // The whole point is to break the fixed-2000ms cadence: across many
    // samples the value must actually vary (not collapse to the nominal),
    // and every sample must land within ±AUDIO_DELAY_JITTER_SPREAD of the
    // configured nominal (so the operator's budget is honoured, not blown).
    let nominal_ms = 2_000_u64;
    let low = (nominal_ms as f64 * (1.0 - AUDIO_DELAY_JITTER_SPREAD)) as u128; // 1500
    let high = (nominal_ms as f64 * (1.0 + AUDIO_DELAY_JITTER_SPREAD)) as u128; // 2500
    let mut seen = std::collections::HashSet::new();
    for _ in 0..200 {
        let ms = jittered_audio_delay(nominal_ms).as_millis();
        assert!(
            ms >= low && ms <= high,
            "jittered delay {ms}ms out of band [{low},{high}] for nominal {nominal_ms}"
        );
        seen.insert(ms);
    }
    // 200 draws over a 1000ms-wide band must produce many distinct values 
    // a fixed sleep would yield exactly one.
    assert!(
        seen.len() > 20,
        "jitter barely varied ({} distinct of 200), cadence is still ~fixed",
        seen.len()
    );
}

#[test]
fn jittered_audio_delay_floor_is_at_least_1ms() {
    // A zero/tiny nominal must never sleep 0ms (which would be an instant,
    // inhuman action). jittered_step floors at 1ms.
    for nominal in [0_u64, 1, 2] {
        assert!(jittered_audio_delay(nominal).as_millis() >= 1);
    }
}