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 tests for `solver::token_shapes`. These guarantee the
//! oracle never exhibits surprising classification behaviour under
//! random input — a regression here looks like decoy tokens silently
//! passing as Plausible, which would let a fooled solver report
//! success on a hostile vendor response.

use captchaforge::solver::token_shapes::{for_vendor, TokenShape};
use proptest::prelude::*;

const VENDORS: &[&str] = &[
    "cloudflare-turnstile",
    "recaptcha-v2",
    "recaptcha-v3",
    "recaptcha-enterprise",
    "hcaptcha",
];

proptest! {
    /// The empty string is never Plausible — a successful solver
    /// would never have produced it. Failing this check means the
    /// chain might silently accept "" as a valid token from a buggy
    /// solver implementation.
    #[test]
    fn empty_string_is_never_plausible(vendor in proptest::sample::select(VENDORS)) {
        let oracle = for_vendor(vendor).unwrap();
        prop_assert_ne!(oracle.classify(""), TokenShape::Plausible);
    }

    /// Whitespace-only strings are never Plausible.
    #[test]
    fn whitespace_only_is_never_plausible(
        vendor in proptest::sample::select(VENDORS),
        ws in "[ \t\n]{1,40}",
    ) {
        let oracle = for_vendor(vendor).unwrap();
        prop_assert_ne!(oracle.classify(&ws), TokenShape::Plausible);
    }

    /// The literal sentinel "FAILED" is always Decoy across all
    /// vendors — common solver-fallback string we've seen in stub
    /// code. Must never appear as Plausible.
    #[test]
    fn failed_sentinel_is_always_decoy(vendor in proptest::sample::select(VENDORS)) {
        let oracle = for_vendor(vendor).unwrap();
        prop_assert_eq!(oracle.classify("FAILED"), TokenShape::Decoy);
    }

    /// Single-byte tokens are always at most Suspect — far too short
    /// to be a real captcha token. (Most vendors emit 200+ chars.)
    #[test]
    fn single_char_tokens_are_not_plausible(
        vendor in proptest::sample::select(VENDORS),
        c in "[a-zA-Z0-9]",
    ) {
        let oracle = for_vendor(vendor).unwrap();
        prop_assert_ne!(oracle.classify(&c), TokenShape::Plausible);
    }

    /// Classifications are deterministic. Calling classify twice
    /// with the same input returns the same answer — no hidden
    /// global state.
    #[test]
    fn classify_is_deterministic(
        vendor in proptest::sample::select(VENDORS),
        token in "[A-Za-z0-9_\\-=\\.]{0,400}",
    ) {
        let oracle = for_vendor(vendor).unwrap();
        let first = oracle.classify(&token);
        let second = oracle.classify(&token);
        prop_assert_eq!(first, second);
    }

    /// classify never panics on arbitrary unicode bytes — defensive
    /// invariant since hostile vendor responses might contain
    /// non-ASCII or even invalid sequences (we receive `&str` so
    /// they'll be valid UTF-8 by typing, but exotic codepoints
    /// shouldn't cause an oracle panic).
    #[test]
    fn classify_does_not_panic_on_unicode(
        vendor in proptest::sample::select(VENDORS),
        token in ".{0,200}",
    ) {
        let oracle = for_vendor(vendor).unwrap();
        let _ = oracle.classify(&token);
    }

    /// for_vendor is case-stable — exact-string match is the
    /// contract, so unrecognised vendors return None instead of
    /// silently aliasing to a different oracle.
    #[test]
    fn unknown_vendors_return_none(name in "[a-z][a-z0-9]{0,20}") {
        prop_assume!(!matches!(
            name.as_str(),
            "cloudflare-turnstile"
                | "turnstile"
                | "recaptcha-v2"
                | "recaptcha"
                | "recaptcha-v3"
                | "recaptcha-enterprise"
                | "hcaptcha"
        ));
        prop_assert!(for_vendor(&name).is_none());
    }
}

#[test]
fn vendor_names_are_stable() {
    // for_vendor matches via exact string equality. If a refactor
    // changes the canonical names without updating consumers, the
    // chain silently stops calling the oracle. Lock the canonical
    // set so a renaming attempt fails CI.
    for v in VENDORS {
        let oracle = for_vendor(v).unwrap_or_else(|| panic!("oracle missing for {v}"));
        // vendor() returns the canonical name; for the short alias
        // forms ("turnstile", "recaptcha") it returns the canonical
        // form, not the alias.
        assert!(!oracle.vendor().is_empty());
    }
}