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
//! Integration tests for the vision backend.
//!
//! These tests verify that:
//! 1. YOLOv8n model downloads and loads correctly
//! 2. CRNN model loads when available
//! 3. Inference runs without panic on synthetic images
//!
//! Run with: cargo test --features vision --test vision_integration

#[cfg(feature = "vision")]
use anyhow::Result;

#[tokio::test]
#[cfg(feature = "vision")]
async fn model_hub_downloads_yolov8n() -> Result<()> {
    let hub = captchaforge::vision::ModelHub::new();
    let path = hub.resolve(captchaforge::vision::ModelId::YoloV8n).await?;
    assert!(path.exists(), "YOLOv8n model should exist after download");
    assert!(path.metadata()?.len() > 1_000_000, "model should be > 1MB");
    Ok(())
}

#[test]
#[cfg(feature = "vision")]
fn yolov8n_detects_on_synthetic_image() -> Result<()> {
    let rt = tokio::runtime::Runtime::new()?;
    let hub = captchaforge::vision::ModelHub::new();
    let path = rt.block_on(async { hub.resolve(captchaforge::vision::ModelId::YoloV8n).await })?;

    let mut detector = captchaforge::vision::YoloDetector::load(&path)?;

    // Create a synthetic image with a red square (no real objects).
    let mut img = image::RgbImage::new(640, 480);
    for pixel in img.pixels_mut() {
        *pixel = image::Rgb([128, 128, 128]);
    }
    for y in 100..300 {
        for x in 100..300 {
            img.put_pixel(x, y, image::Rgb([255, 0, 0]));
        }
    }
    let dyn_img = image::DynamicImage::ImageRgb8(img);

    let detections = detector.detect(&dyn_img)?;
    // Synthetic image has no real COCO objects, but the inference must not panic.
    println!("detections: {}", detections.len());
    Ok(())
}

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

    // Known reCAPTCHA tasks that YOLO can handle.
    assert!(task_to_coco_classes("Select all images with traffic lights").is_some());
    assert!(task_to_coco_classes("Select all buses").is_some());
    assert!(task_to_coco_classes("Select all cars").is_some());
    assert!(task_to_coco_classes("Select all bicycles").is_some());
    assert!(task_to_coco_classes("Select all vehicles").is_some());
    assert!(task_to_coco_classes("Select all fire hydrants").is_some());

    // Unknown classes — solver should fall back to VLM.
    assert!(task_to_coco_classes("Select all crosswalks").is_none());
    assert!(task_to_coco_classes("Select all chimneys").is_none());
    assert!(task_to_coco_classes("Select all stairs").is_none());
    assert!(task_to_coco_classes("Select all palm trees").is_none());
}

#[test]
#[cfg(feature = "vision")]
fn crnn_recognizer_loads_when_model_available() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    let hub = captchaforge::vision::ModelHub::new();
    let path = rt.block_on(async { hub.resolve(captchaforge::vision::ModelId::CrnnText).await });

    if let Ok(path) = path {
        let charset = captchaforge::vision::crnn::default_charset();
        let result = captchaforge::vision::crnn::CrnnRecognizer::load(&path, charset);
        assert!(result.is_ok(), "CRNN should load when model is available");
    } else {
        println!("CRNN model not available — skipping load test (run scripts/train_crnn.py to generate)");
    }
}

#[test]
#[cfg(not(feature = "vision"))]
fn vision_module_compiles_without_feature() {
    use captchaforge::solver::CaptchaSolver;
    // When vision is disabled, the module should still compile and the
    // solvers should be inert.
    let yolo = captchaforge::solver::YoloGridSolver::new();
    assert!(!yolo.supports(&captchaforge::captcha_detect::DetectedCaptcha::RecaptchaV2));

    let crnn = captchaforge::solver::CrnnTextSolver::new();
    assert!(!crnn.supports(&captchaforge::captcha_detect::DetectedCaptcha::ImageCaptcha));
}