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
//! Lock the captchaforge Prometheus alerting rules file structure
//! so a refactor that drops an alert OR changes a metric name in
//! captchaforge::telemetry::MetricsTelemetry without updating the
//! matching alert breaks here.

use std::fs;
use std::path::PathBuf;

fn rules_path() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("deploy")
        .join("prometheus")
        .join("captchaforge.rules.yml")
}

#[test]
fn rules_file_exists_and_is_non_empty() {
    let body = fs::read_to_string(rules_path()).expect("read rules file");
    assert!(!body.is_empty());
    assert!(body.starts_with('#') || body.starts_with("groups:"));
}

#[test]
fn ships_required_alerts() {
    let body = fs::read_to_string(rules_path()).expect("read rules file");
    const REQUIRED_ALERTS: &[&str] = &[
        "CaptchaforgeVendorSuccessRateLow",
        "CaptchaforgeVendorSuccessRateCritical",
        "CaptchaforgeSolveP95Slow",
        "CaptchaforgeHardBlockRateHigh",
        "CaptchaforgeSilentFailRateHigh",
        "CaptchaforgeServeDown",
    ];
    for name in REQUIRED_ALERTS {
        assert!(
            body.contains(&format!("alert: {name}")),
            "alert {name} missing from rules.yml"
        );
    }
}

#[test]
fn references_the_metric_names_captchaforge_actually_emits() {
    // MetricsTelemetry emits these names; if it ever renames a
    // metric, the alert file becomes dead. Lock both sides here.
    let body = fs::read_to_string(rules_path()).expect("read rules file");
    assert!(
        body.contains("captchaforge_solve_total"),
        "alerts must reference captchaforge_solve_total"
    );
    assert!(
        body.contains("captchaforge_solve_duration_ms_bucket"),
        "alerts must reference captchaforge_solve_duration_ms_bucket"
    );
    assert!(body.contains("outcome=\"success\""));
    assert!(body.contains("outcome=\"hard_block\""));
    assert!(body.contains("outcome=\"silent_fail\""));
}

#[test]
fn every_alert_has_severity_label() {
    let body = fs::read_to_string(rules_path()).expect("read rules file");
    let alert_lines: Vec<&str> = body
        .lines()
        .filter(|l| l.trim().starts_with("- alert:"))
        .collect();
    let severity_lines: Vec<&str> = body
        .lines()
        .filter(|l| l.trim().starts_with("severity:"))
        .collect();
    assert!(!alert_lines.is_empty(), "rules file has zero alerts");
    assert!(
        severity_lines.len() >= alert_lines.len(),
        "every alert must declare a severity label: got {} alerts, {} severities",
        alert_lines.len(),
        severity_lines.len()
    );
}