captchaforge 0.2.38

[DO NOT USE — UNDER ACTIVE DEVELOPMENT, NOT PRODUCTION-READY] Captcha solver scaffolding for Firefox + BiDi-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 / BiDi 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 vision backend.
//!
//! Verifies that inference is robust against edge-case inputs:
//! - Extreme image sizes
//! - Random noise images
//! - Empty / tiny images
//! - Invalid model paths
//!
//! Run with: cargo test --features vision --test vision_property

#[cfg(feature = "vision")]
use captchaforge::vision;
use proptest::prelude::*;

// ---------------------------------------------------------------------------
// YOLO Detector — robustness against arbitrary images
// ---------------------------------------------------------------------------

#[cfg(feature = "vision")]
fn arbitrary_rgb_image() -> impl Strategy<Value = image::RgbImage> {
    (1u32..800u32, 1u32..800u32).prop_flat_map(|(w, h)| {
        proptest::collection::vec(any::<u8>(), (w * h * 3) as usize).prop_map(move |bytes| {
            image::RgbImage::from_raw(w, h, bytes).unwrap_or_else(|| image::RgbImage::new(w, h))
        })
    })
}

proptest! {
    #[test]
    #[cfg(feature = "vision")]
    fn yolo_detect_never_panics_on_random_images(img in arbitrary_rgb_image()) {
        let rt = tokio::runtime::Runtime::new().unwrap();
        let hub = vision::ModelHub::new();
        let path = rt.block_on(async { hub.resolve(vision::ModelId::YoloV8n).await }).unwrap();
        let mut detector = vision::YoloDetector::load(&path).unwrap();
        let dyn_img = image::DynamicImage::ImageRgb8(img);
        // Must not panic — returns Ok or Err, never unwinds.
        let _ = detector.detect(&dyn_img);
    }
}

#[test]
#[cfg(feature = "vision")]
fn yolo_detect_empty_image_returns_no_detections() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    let hub = vision::ModelHub::new();
    let path = rt.block_on(async { hub.resolve(vision::ModelId::YoloV8n).await }).unwrap();
    let mut detector = vision::YoloDetector::load(&path).unwrap();
    let img = image::DynamicImage::new_rgb8(640, 480);
    let dets = detector.detect(&img).unwrap();
    assert!(dets.is_empty(), "solid-color image should yield no detections");
}

#[test]
#[cfg(feature = "vision")]
fn yolo_detect_small_image_still_runs() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    let hub = vision::ModelHub::new();
    let path = rt.block_on(async { hub.resolve(vision::ModelId::YoloV8n).await }).unwrap();
    let mut detector = vision::YoloDetector::load(&path).unwrap();
    let img = image::DynamicImage::new_rgb8(10, 10);
    // Should not panic — letterboxing handles arbitrary sizes.
    let _ = detector.detect(&img);
}

// ---------------------------------------------------------------------------
// CRNN Recognizer — robustness
// ---------------------------------------------------------------------------

proptest! {
    #[test]
    #[cfg(feature = "vision")]
    fn crnn_recognize_never_panics_on_random_images(img in arbitrary_rgb_image()) {
        let rt = tokio::runtime::Runtime::new().unwrap();
        let hub = vision::ModelHub::new();
        let path = rt.block_on(async { hub.resolve(vision::ModelId::CrnnText).await });
        prop_assume!(path.is_ok(), "CRNN model not available — skipping");
        let path = path.unwrap();
        let charset = vision::crnn::default_charset();
        let mut recognizer = vision::crnn::CrnnRecognizer::load(&path, charset).unwrap();
        let dyn_img = image::DynamicImage::ImageRgb8(img);
        let _ = recognizer.recognize(&dyn_img);
    }
}

// ---------------------------------------------------------------------------
// Task-to-COCO mapping — coverage
// ---------------------------------------------------------------------------

#[test]
#[cfg(feature = "vision")]
fn task_mapping_is_exhaustive_for_common_captcha_tasks() {
    use vision::yolo::task_to_coco_classes;

    let known_tasks = [
        ("Select all images with traffic lights", true),
        ("Select all images with buses", true),
        ("Select all images with cars", true),
        ("Select all images with vehicles", true),
        ("Select all images with bicycles", true),
        ("Select all images with motorcycles", true),
        ("Select all images with trains", true),
        ("Select all images with boats", true),
        ("Select all images with fire hydrants", true),
        ("Select all images with cats", true),
        ("Select all images with dogs", true),
        ("Select all images with people", true),
        ("Select all images with crosswalks", false),
        ("Select all images with chimneys", false),
        ("Select all images with stairs", false),
        ("Select all images with palm trees", false),
        ("Select all images with bridges", false),
    ];

    for (task, expected_known) in &known_tasks {
        let result = task_to_coco_classes(task);
        if *expected_known {
            assert!(
                result.is_some(),
                "task '{}' should map to COCO classes",
                task
            );
        } else {
            assert!(
                result.is_none(),
                "task '{}' should NOT map to COCO classes (needs VLM fallback)",
                task
            );
        }
    }
}

// ---------------------------------------------------------------------------
// Model Hub — path handling
// ---------------------------------------------------------------------------

#[test]
#[cfg(feature = "vision")]
fn model_hub_with_custom_root() {
    let tmp = tempfile::tempdir().unwrap();
    let hub = vision::ModelHub::with_root(tmp.path());
    // No models cached yet.
    assert!(hub.local_path(vision::ModelId::YoloV8n).is_none());
}

#[test]
#[cfg(feature = "vision")]
fn model_hub_detects_cached_yolov8n() {
    let hub = vision::ModelHub::new();
    // After previous tests, YOLOv8n should be cached.
    let path = hub.local_path(vision::ModelId::YoloV8n);
    assert!(path.is_some(), "YOLOv8n should be cached after download");
}