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
//! Property-based tests for the solver chain and result types.

use captchaforge::solver::{
    CaptchaPattern, CaptchaSolveResult, CaptchaType, PatternStore, SolveConfig, SolveMethod,
};
use proptest::prelude::*;
use proptest::test_runner::TestRunner;

#[test]
fn failure_result_never_success() {
    let mut runner = TestRunner::default();
    runner
        .run(&(any_solve_method(), 0u64..10_000), |(method, time_ms)| {
            let result = CaptchaSolveResult::failure(method.clone(), time_ms);
            prop_assert!(!result.success);
            prop_assert!(result.solution.is_empty());
            prop_assert_eq!(result.confidence, 0.0);
            prop_assert_eq!(result.method, method);
            prop_assert_eq!(result.time_ms, time_ms);
            Ok(())
        })
        .unwrap();
}

#[test]
fn unsolved_result_never_success() {
    let mut runner = TestRunner::default();
    runner
        .run(&(0u64..10_000), |time_ms| {
            let result = CaptchaSolveResult::unsolved(time_ms, None);
            prop_assert!(!result.success);
            prop_assert!(result.solution.is_empty());
            prop_assert_eq!(result.confidence, 0.0);
            prop_assert_eq!(result.time_ms, time_ms);
            Ok(())
        })
        .unwrap();
}

#[test]
fn confidence_in_range() {
    let mut runner = TestRunner::default();
    runner
        .run(&(0.0f32..=1.0f32), |confidence| {
            let result = CaptchaSolveResult {
                solution: "test".to_string(),
                confidence,
                method: SolveMethod::BehavioralBypass,
                time_ms: 100,
                success: true,
                screenshot: None,
                cookies: Vec::new(),
                verified_outcome: None,
            };
            prop_assert!(result.confidence >= 0.0);
            prop_assert!(result.confidence <= 1.0);
            Ok(())
        })
        .unwrap();
}

#[test]
fn time_ms_non_negative() {
    let mut runner = TestRunner::default();
    runner
        .run(&(0u64..u64::MAX), |time_ms| {
            let result = CaptchaSolveResult {
                solution: String::new(),
                confidence: 0.0,
                method: SolveMethod::BehavioralBypass,
                time_ms,
                success: false,
                screenshot: None,
                cookies: Vec::new(),
                verified_outcome: None,
            };
            prop_assert_eq!(result.time_ms, time_ms);
            Ok(())
        })
        .unwrap();
}

#[test]
fn solve_config_defaults_positive() {
    let cfg = SolveConfig::default();
    assert!(cfg.checkbox_poll_interval_ms > 0);
    assert!(cfg.checkbox_max_attempts > 0);
    assert!(cfg.token_poll_interval_ms > 0);
    assert!(cfg.token_max_attempts > 0);
    assert!(cfg.audio_button_delay_ms > 0);
    assert!(cfg.audio_submit_delay_ms > 0);
    assert!(cfg.vlm_http_timeout_ms > 0);
    assert!(cfg.client_http_timeout_ms > 0);
}

#[test]
fn pattern_success_rate_bounded() {
    let mut runner = TestRunner::default();
    runner
        .run(
            &proptest::collection::vec((proptest::bool::ANY, 10u64..1000u64), 0..200),
            |records| {
                let mut pattern = CaptchaPattern::new(
                    "example.com".to_string(),
                    CaptchaType::CloudflareTurnstile,
                    SolveMethod::BehavioralBypass,
                );
                for (success, time_ms) in records {
                    pattern.record(success, time_ms, SolveMethod::BehavioralBypass);
                    prop_assert!(pattern.success_rate >= 0.0);
                    prop_assert!(pattern.success_rate <= 1.0);
                    prop_assert!(pattern.sample_count > 0);
                }
                Ok(())
            },
        )
        .unwrap();
}

#[test]
fn pattern_store_concurrent_records_preserved() {
    let mut runner = TestRunner::default();
    // Use only non-Custom types so patterns.len() is bounded.
    let non_custom = prop_oneof![
        Just(CaptchaType::RecaptchaV2),
        Just(CaptchaType::RecaptchaV3),
        Just(CaptchaType::HCaptcha),
        Just(CaptchaType::CloudflareTurnstile),
        Just(CaptchaType::ImageGrid),
        Just(CaptchaType::TextCaptcha),
        Just(CaptchaType::AudioCaptcha),
        Just(CaptchaType::Slider),
    ];
    let strategy = proptest::collection::vec(
        (
            non_custom,
            any_solve_method(),
            proptest::bool::ANY,
            10u64..1000u64,
        ),
        1..50usize,
    );
    runner
        .run(&strategy, |records| {
            use std::sync::Arc;
            use std::thread;

            let store = Arc::new(PatternStore::default());
            let mut handles = Vec::new();

            for (captcha_type, method, success, time_ms) in records {
                let store = Arc::clone(&store);
                handles.push(thread::spawn(move || {
                    store.record("example.com", &captcha_type, success, time_ms, method);
                }));
            }

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

            let patterns = store.all_patterns();
            prop_assert!(patterns.len() <= 8); // One per non-Custom CaptchaType
            Ok(())
        })
        .unwrap();
}

#[test]
fn captcha_type_display_serde_consistent() {
    let mut runner = TestRunner::default();
    // Only test non-Custom variants since Custom serializes as a map.
    let non_custom = prop_oneof![
        Just(CaptchaType::RecaptchaV2),
        Just(CaptchaType::RecaptchaV3),
        Just(CaptchaType::HCaptcha),
        Just(CaptchaType::CloudflareTurnstile),
        Just(CaptchaType::ImageGrid),
        Just(CaptchaType::TextCaptcha),
        Just(CaptchaType::AudioCaptcha),
        Just(CaptchaType::Slider),
    ];
    runner
        .run(&non_custom, |captcha_type| {
            let display = captcha_type.to_string();
            let json = serde_json::to_string(&captcha_type).unwrap();
            let json_str: String = serde_json::from_str(&json).unwrap();
            prop_assert_eq!(display, json_str);
            Ok(())
        })
        .unwrap();
}

#[test]
fn solve_method_serde_roundtrip() {
    let mut runner = TestRunner::default();
    runner
        .run(&any_solve_method(), |method| {
            let json = serde_json::to_string(&method).unwrap();
            let back: SolveMethod = serde_json::from_str(&json).unwrap();
            prop_assert_eq!(method, back);
            Ok(())
        })
        .unwrap();
}

#[test]
fn empty_store_returns_none() {
    let mut runner = TestRunner::default();
    runner
        .run(&any_captcha_type(), |captcha_type| {
            let store = PatternStore::default();
            prop_assert!(store.best_method("unknown.com", &captcha_type).is_none());
            Ok(())
        })
        .unwrap();
}

#[test]
fn success_implies_method_set() {
    let mut runner = TestRunner::default();
    runner
        .run(&any_solve_method(), |method| {
            let result = CaptchaSolveResult {
                solution: "token".to_string(),
                confidence: 0.95,
                method: method.clone(),
                time_ms: 500,
                success: true,
                screenshot: None,
                cookies: Vec::new(),
                verified_outcome: None,
            };
            prop_assert!(result.success);
            prop_assert_eq!(result.method, method);
            Ok(())
        })
        .unwrap();
}

fn any_solve_method() -> impl Strategy<Value = SolveMethod> {
    prop_oneof![
        Just(SolveMethod::VisionLLM),
        Just(SolveMethod::AudioBypass),
        Just(SolveMethod::BehavioralBypass),
        Just(SolveMethod::ThirdPartyService),
        Just(SolveMethod::CrowdSourced),
    ]
}

fn any_captcha_type() -> impl Strategy<Value = CaptchaType> {
    prop_oneof![
        Just(CaptchaType::RecaptchaV2),
        Just(CaptchaType::RecaptchaV3),
        Just(CaptchaType::HCaptcha),
        Just(CaptchaType::CloudflareTurnstile),
        Just(CaptchaType::ImageGrid),
        Just(CaptchaType::TextCaptcha),
        Just(CaptchaType::AudioCaptcha),
        Just(CaptchaType::Slider),
        ("[a-z0-9_-]{1,32}").prop_map(CaptchaType::Custom),
    ]
}