captchaforge 0.2.36

[DO NOT USE — UNDER ACTIVE DEVELOPMENT, NOT PRODUCTION-READY] Captcha solver scaffolding for chromiumoxide-driven browsers. The architecture is in place (vendor solvers, retry-loop iframe walking, VLM provider abstraction, real-WAF bench harness) but the live-vendor success rate is still 0% — Cloudflare Turnstile / hCaptcha / reCAPTCHA detect us at a TLS / CDP fingerprint layer that no flag-based stealth has cleared. Watch the repo; do not depend on this for any real workload.
Documentation
//! Chaos tests for solver edge cases.

use captchaforge::solver::{
    CaptchaPattern, CaptchaSolveResult, CaptchaType, PatternStore, SolveConfig, SolveMethod,
};

#[test]
fn solve_config_with_max_values() {
    let config = SolveConfig {
        checkbox_poll_interval_ms: u64::MAX,
        checkbox_max_attempts: u32::MAX,
        token_poll_interval_ms: u64::MAX,
        token_max_attempts: u32::MAX,
        audio_button_delay_ms: u64::MAX,
        audio_submit_delay_ms: u64::MAX,
        vlm_http_timeout_ms: u64::MAX,
        client_http_timeout_ms: u64::MAX,
    };
    assert_eq!(config.checkbox_poll_interval_ms, u64::MAX);
    assert_eq!(config.checkbox_max_attempts, u32::MAX);
}

#[test]
fn pattern_record_with_u64_max_time() {
    let mut pattern = CaptchaPattern::new(
        "example.com",
        CaptchaType::CloudflareTurnstile,
        SolveMethod::BehavioralBypass,
    );
    pattern.record(true, u64::MAX, SolveMethod::BehavioralBypass);
    assert_eq!(pattern.avg_solve_time, u64::MAX);
}

#[test]
fn pattern_with_1million_records() {
    let mut pattern = CaptchaPattern::new(
        "example.com",
        CaptchaType::CloudflareTurnstile,
        SolveMethod::BehavioralBypass,
    );
    for i in 0..1_000_000 {
        pattern.record(i % 2 == 0, 100, SolveMethod::BehavioralBypass);
    }
    assert_eq!(pattern.sample_count, 1_000_000);
    // Under bounded-α EMA, alternating success/fail does NOT converge
    // to an exact 0.5 — it oscillates in a 2-cycle. With α = 0.2 the
    // steady-state values are (1-α)/(2-α) ≈ 0.444 and ≈ 0.556. Assert
    // the EMA stays inside that band rather than exact-equality at 0.5
    // (which is a cumulative-mean property the EMA intentionally drops
    // in exchange for vendor-migration responsiveness).
    assert!(
        pattern.success_rate >= 0.4 && pattern.success_rate <= 0.6,
        "EMA in alternating steady state should bounce in [0.4, 0.6]; got {}",
        pattern.success_rate
    );
}

#[test]
fn store_with_1000_domains() {
    let store = PatternStore::default();
    for i in 0..1000 {
        let domain = format!("domain{}.com", i);
        store.record(
            &domain,
            &CaptchaType::RecaptchaV2,
            true,
            100,
            SolveMethod::BehavioralBypass,
        );
    }
    assert_eq!(store.all_patterns().len(), 1000);
}

#[test]
fn store_same_domain_different_types() {
    let store = PatternStore::default();
    store.record(
        "example.com",
        &CaptchaType::RecaptchaV2,
        true,
        100,
        SolveMethod::BehavioralBypass,
    );
    store.record(
        "example.com",
        &CaptchaType::RecaptchaV3,
        true,
        200,
        SolveMethod::VisionLLM,
    );
    store.record(
        "example.com",
        &CaptchaType::HCaptcha,
        false,
        500,
        SolveMethod::AudioBypass,
    );

    assert_eq!(
        store.best_method("example.com", &CaptchaType::RecaptchaV2),
        Some(SolveMethod::BehavioralBypass)
    );
    assert_eq!(
        store.best_method("example.com", &CaptchaType::RecaptchaV3),
        Some(SolveMethod::VisionLLM)
    );
    assert_eq!(
        store.best_method("example.com", &CaptchaType::HCaptcha),
        Some(SolveMethod::AudioBypass)
    );
}

#[test]
fn result_serde_with_1mb_solution() {
    let big = "x".repeat(1_000_000);
    let result = CaptchaSolveResult {
        solution: big.clone(),
        confidence: 0.5,
        method: SolveMethod::BehavioralBypass,
        time_ms: 1,
        success: true,
        screenshot: None,
        cookies: Vec::new(),
        verified_outcome: None,
    };
    let json = serde_json::to_string(&result).unwrap();
    let back: CaptchaSolveResult = serde_json::from_str(&json).unwrap();
    assert_eq!(back.solution.len(), 1_000_000);
}

#[test]
fn result_serde_with_unicode_solution() {
    let result = CaptchaSolveResult {
        solution: "日本語テスト🎉".into(),
        confidence: 0.99,
        method: SolveMethod::BehavioralBypass,
        time_ms: 100,
        success: true,
        screenshot: None,
        cookies: Vec::new(),
        verified_outcome: None,
    };
    let json = serde_json::to_string(&result).unwrap();
    let back: CaptchaSolveResult = serde_json::from_str(&json).unwrap();
    assert_eq!(back.solution, "日本語テスト🎉");
}

#[test]
fn pattern_store_concurrent_high_contention() {
    use std::thread;

    let store = std::sync::Arc::new(PatternStore::default());
    let mut handles = vec![];

    for t in 0..50 {
        let s = store.clone();
        handles.push(thread::spawn(move || {
            for i in 0..1000 {
                let domain = format!("thread{}-domain{}.com", t, i % 10);
                let ct = if i % 2 == 0 {
                    CaptchaType::RecaptchaV2
                } else {
                    CaptchaType::CloudflareTurnstile
                };
                s.record(
                    &domain,
                    &ct,
                    i % 3 == 0,
                    i as u64 * 10,
                    SolveMethod::BehavioralBypass,
                );
            }
        }));
    }

    for h in handles {
        h.join().unwrap();
    }

    assert!(!store.all_patterns().is_empty());
}