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
//! Validate the bundled community.toml rule pack against the public
//! JSON-Schema at `schemas/community-rules.schema.json`. Lock-step:
//! every rule that ships in the binary MUST pass schema validation,
//! and the schema MUST cover every field the rule loader accepts.

use captchaforge::detect::rules::built_in_rules;

#[test]
fn schema_file_is_well_formed_json() {
    let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("schemas")
        .join("community-rules.schema.json");
    let body = std::fs::read_to_string(&path).expect("read schema");
    let v: serde_json::Value = serde_json::from_str(&body).expect("schema is valid JSON");
    assert_eq!(v["$schema"], "https://json-schema.org/draft-07/schema#");
    assert_eq!(v["type"], "object");
    // provider items use $ref; dereference into definitions.
    let provider_def = &v["definitions"]["provider"];
    assert!(provider_def.is_object(), "definitions.provider missing");
    let required = &provider_def["required"];
    assert!(
        required
            .as_array()
            .map(|a| a.iter().any(|v| v == "name") && a.iter().any(|v| v == "priority"))
            .unwrap_or(false),
        "schema provider definition must require name + priority"
    );
}

#[test]
fn every_bundled_rule_passes_lightweight_schema_invariants() {
    let pack = built_in_rules().expect("bundled rules parse");
    let mut errors: Vec<String> = Vec::new();
    let mut seen_names = std::collections::HashSet::new();

    for rule in &pack.providers {
        // `name` matches the schema pattern.
        if !rule
            .name
            .chars()
            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
        {
            errors.push(format!(
                "rule {:?} name violates pattern ^[a-z][a-z0-9_]+$",
                rule.name
            ));
        }
        if rule.name.is_empty()
            || rule.name.len() > 64
            || !rule.name.chars().next().is_some_and(|c| c.is_ascii_lowercase())
        {
            errors.push(format!("rule name length / leading-char invalid: {:?}", rule.name));
        }
        // unique
        if !seen_names.insert(rule.name.clone()) {
            errors.push(format!("rule name {:?} duplicated in pack", rule.name));
        }
        // priority in [1, 999]
        if !(1..=999).contains(&rule.priority) {
            errors.push(format!(
                "rule {} priority {} outside [1, 999]",
                rule.name, rule.priority
            ));
        }
        // triggers MUST be non-empty across all five fields.
        let t = &rule.triggers;
        if t.selectors.is_empty()
            && t.window_globals.is_empty()
            && t.script_src_contains.is_empty()
            && t.title_contains.is_empty()
            && t.cookie_names.is_empty()
        {
            errors.push(format!("rule {} has no triggers at all", rule.name));
        }
    }

    assert!(
        errors.is_empty(),
        "bundled rules fail schema invariants:\n  {}",
        errors.join("\n  ")
    );
}

#[test]
fn schema_solver_enum_matches_chain_solver_names() {
    // The schema's `solver_names` enum lists every CaptchaSolver
    // impl name a rule can reference. It must stay in sync with
    // `CaptchaSolverChain::default_chain().solver_names()` plus the
    // OcrCaptchaSolver / MultiStepCaptchaSolver / VlmCaptchaSolver
    // that Config::build_chain appends.
    let chain = captchaforge::solver::CaptchaSolverChain::default_chain();
    let chain_names: std::collections::HashSet<&str> =
        chain.solver_names().into_iter().collect();

    let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("schemas")
        .join("community-rules.schema.json");
    let body = std::fs::read_to_string(&path).expect("read schema");
    let v: serde_json::Value = serde_json::from_str(&body).expect("schema parses");
    let schema_enum: Vec<&str> = v["definitions"]["provider"]["properties"]["solver_names"]
        ["items"]["enum"]
        .as_array()
        .expect("solver_names enum is an array")
        .iter()
        .map(|s| s.as_str().expect("enum entry is string"))
        .collect();

    for name in chain_names {
        assert!(
            schema_enum.contains(&name),
            "solver {:?} from default_chain is missing from the schema's solver_names enum — \
             external contributors who reference it in TOML will fail validation",
            name
        );
    }
}