opencrabs 0.5.1

The autonomous, self-improving AI agent. Single Rust binary. Every channel. Recommended: the 40MB prebuilt binary for macOS, Linux and Windows: https://github.com/adolfousier/opencrabs/releases
//! Pack round-trip tests for the curated theme catalog (#1461): every
//! embedded pack TOML passes the existing validator unchanged, names are
//! unique and collision-free against the hand-built presets, provenance
//! headers are stamped, and the pack serves through the same
//! `built_ins()` / `by_name()` surfaces the picker and `/theme <name>`
//! already use (no loader or picker changes were needed).

use crate::tui::render::presets::{built_ins, by_name};
use crate::tui::render::user_themes::{build_theme, parse_hex};
use crate::tui::theme_catalog::theme_pack::sources;

/// The hard invariant from #1461: every embedded pack file passes the
/// existing validator UNCHANGED. Shadow stems because the real stems now
/// collide with the pack itself (it IS built-ins); the 43 colors and the
/// 2.5:1 contrast floor — what this test is about — validate identically.
#[test]
fn every_embedded_pack_theme_passes_the_validator_unchanged() {
    assert!(!sources().is_empty(), "pack is empty");
    for (name, toml) in sources() {
        let shadow = format!("packcheck-{name}");
        build_theme(&shadow, toml)
            .unwrap_or_else(|e| panic!("pack theme {name} rejected by validator: {e}"));
    }
}

/// Curation contract: ~30 themes, unique names, per-theme provenance.
#[test]
fn pack_is_curated_unique_and_provenance_stamped() {
    let names: Vec<&str> = sources().iter().map(|(n, _)| *n).collect();
    assert!(
        names.len() >= 30,
        "expected ~30 curated themes, got {}",
        names.len()
    );
    let mut sorted = names.clone();
    sorted.sort_unstable();
    sorted.dedup();
    assert_eq!(sorted.len(), names.len(), "duplicate pack names");
    for (name, toml) in sources() {
        assert!(
            toml.starts_with("# source:"),
            "{name}: missing '# source:' provenance header"
        );
        assert!(
            toml.contains("generated by tui::theme_catalog::converter"),
            "{name}: missing generator stamp"
        );
    }
}

/// Shipping wiring: the pack serves through the same surfaces as the
/// hand-built presets, crab-dark stays first, and no name collides.
#[test]
fn pack_serves_through_built_ins_and_by_name() {
    let all = built_ins();
    assert_eq!(all[0].name, "crab-dark", "default theme stays first");
    assert!(
        all.len() >= 8 + sources().len(),
        "built_ins should hold 8 hand-built + {} pack themes, got {}",
        sources().len(),
        all.len()
    );
    let mut names: Vec<&str> = all.iter().map(|t| t.name).collect();
    let total = names.len();
    names.sort_unstable();
    names.dedup();
    assert_eq!(
        names.len(),
        total,
        "name collision between hand-built presets and pack"
    );
    // Spot checks: pack lookup + exact converted color, case-insensitive
    // as /theme expects, and an alacritty complement.
    let tokyonight = by_name("tokyonight").expect("pack theme via by_name");
    assert_eq!(
        tokyonight.colors.ink,
        parse_hex("#1a1b26").unwrap(),
        "tokyonight ink = darkStep1"
    );
    assert!(
        by_name("TokyoNight").is_some(),
        "lookup is case-insensitive"
    );
    assert!(
        by_name("gruvbox-light").is_some(),
        "alacritty complement shipped"
    );
}