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
//! Truth tests for the `BehavioralCaptchaSolver` same-origin grid pre-pass:
//! assert it (a) acts on the CORRECT tiles and (b) delivers TRUSTED clicks
//! (`isTrusted === true`) to both the tiles and the verify button.
//!
//! These cover a previously shape-only-tested path (the old tests only checked
//! "solve() runs") and pin the contract that a same-origin custom/in-house
//! captcha which gates on `isTrusted` is actually solvable — not just clicked
//! with a synthetic event the page can reject.
//!
//! Live: requires Firefox (via `common::launch_browser`), same harness as the
//! other integration tests.

use captchaforge::captcha_detect::{CaptchaInfo, DetectedCaptcha};
use captchaforge::solver::{BehavioralCaptchaSolver, CaptchaSolver};

mod common;

/// Read `window.__clicks` = `[{id, trusted}]` recorded by a fixture's tile
/// listeners.
async fn clicks(page: &captchaforge::Page) -> Vec<(String, bool)> {
    let raw = page
        .evaluate("JSON.stringify(window.__clicks || [])")
        .await
        .ok()
        .and_then(|e| e.into_value::<String>().ok())
        .unwrap_or_default();
    serde_json::from_str::<Vec<serde_json::Value>>(&raw)
        .unwrap_or_default()
        .into_iter()
        .filter_map(|v| Some((v["id"].as_str()?.to_string(), v["trusted"].as_bool().unwrap_or(false))))
        .collect()
}

/// Standard tile-recorder script appended to each fixture: records each tile
/// click `{id, trusted}` and the verify button's `isTrusted`.
const RECORDER: &str = r#"<script>
window.__clicks = [];
window.__verifyTrusted = null;
document.querySelectorAll('.cell, .icon-item, .color-item').forEach(function(el){
  el.addEventListener('click', function(e){ window.__clicks.push({ id: el.id, trusted: e.isTrusted }); });
});
var vb = document.querySelector('#recaptcha-verify-button, #submit');
if (vb) vb.addEventListener('click', function(e){ window.__verifyTrusted = e.isTrusted; });
</script>"#;

/// Drive the behavioral pre-pass against `body_html`, then assert the `want`
/// tiles were clicked TRUSTED, the `never` tiles were not clicked, and the
/// verify button click was trusted.
async fn assert_grid_trusted(body_html: &str, want: &[&str], never: &[&str]) {
    let html = format!(
        "<!doctype html><html><head><meta charset=\"utf-8\"><title>grid</title></head><body>{body_html}{RECORDER}</body></html>"
    );
    let browser = common::launch_browser().await;
    let addr = common::serve_once(html).await;
    let url = format!("http://{addr}/");
    let page = browser.new_page(&url).await.expect("navigate");

    let info = CaptchaInfo {
        kind: DetectedCaptcha::ImageCaptcha,
        site_key: None,
        page_url: url.clone(),
        container_selector: Some("#widget".into()),
    };
    let _ = BehavioralCaptchaSolver::new().solve(page, &info).await;

    let recorded = clicks(page).await;
    for w in want {
        assert!(
            recorded.iter().any(|(id, _)| id == w),
            "tile {w} must be clicked; recorded={recorded:?}"
        );
    }
    for n in never {
        assert!(
            !recorded.iter().any(|(id, _)| id == n),
            "non-matching tile {n} must NOT be clicked; recorded={recorded:?}"
        );
    }
    for (id, trusted) in &recorded {
        assert!(
            *trusted,
            "tile {id} click must be isTrusted === true (a same-origin captcha can \
             gate on it); recorded={recorded:?}"
        );
    }
    let verify_trusted = page
        .evaluate("window.__verifyTrusted")
        .await
        .ok()
        .and_then(|e| e.into_value::<Option<bool>>().ok())
        .flatten();
    assert_eq!(verify_trusted, Some(true), "verify button click must be isTrusted === true");

    let _ = browser.close().await;
}

fn cell(id: &str, emoji: &str) -> String {
    format!(r#"<div class="cell" id="{id}" style="display:inline-block;width:64px;height:64px">{emoji}</div>"#)
}
fn icon(id: &str, emoji: &str) -> String {
    format!(r#"<div class="icon-item" id="{id}" style="display:inline-block;width:64px;height:64px">{emoji}</div>"#)
}
fn color(id: &str, rgb: &str) -> String {
    format!(r#"<div class="color-item" id="{id}" style="display:inline-block;width:64px;height:64px;background-color:{rgb}"></div>"#)
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn behavioral_image_grid_clicks_correct_tiles_trusted() {
    let cells = [
        cell("c0", "🚦"), cell("c1", "🌳"), cell("c2", "🚦"),
        cell("c3", "🍎"), cell("c4", "🚦"), cell("c5", "🐶"),
        cell("c6", "🚗"), cell("c7", "🏠"), cell("c8", "🚦"),
    ]
    .concat();
    let body = format!(
        r#"<div id="widget"><div class="prompt">Select all traffic lights</div>
        <div class="grid">{cells}</div>
        <button id="recaptcha-verify-button">Verify</button></div>"#
    );
    assert_grid_trusted(&body, &["c0", "c2", "c4", "c8"], &["c1", "c3", "c5", "c6", "c7"]).await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn behavioral_icon_grid_clicks_correct_tiles_trusted() {
    let icons = [
        icon("i0", "🚗"), icon("i1", "🐶"), icon("i2", "🚌"),
        icon("i3", "🍎"), icon("i4", "🏍"), icon("i5", "🌳"),
    ]
    .concat();
    let body = format!(
        r#"<div id="widget"><div class="prompt">Select all vehicles</div>
        <div class="grid">{icons}</div>
        <button id="submit">Verify</button></div>"#
    );
    assert_grid_trusted(&body, &["i0", "i2", "i4"], &["i1", "i3", "i5"]).await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn behavioral_color_grid_clicks_correct_tiles_trusted() {
    let tiles = [
        color("k0", "rgb(220,20,20)"),   // red
        color("k1", "rgb(20,120,220)"),  // blue
        color("k2", "rgb(220,30,30)"),   // red
        color("k3", "rgb(30,160,40)"),   // green
        color("k4", "rgb(200,20,20)"),   // red
        color("k5", "rgb(240,220,40)"),  // yellow
    ]
    .concat();
    let body = format!(
        r#"<div id="widget"><div class="prompt">Select all red tiles</div>
        <div class="grid">{tiles}</div>
        <button id="submit">Verify</button></div>"#
    );
    assert_grid_trusted(&body, &["k0", "k2", "k4"], &["k1", "k3", "k5"]).await;
}