hey 0.1.0

Minimal terminal AI coding agent: kernel loop + MCP/Skills self-evolution
Documentation
//! Property-based tests:对纯函数应用 proptest 随机输入验证不变量。
//!
//! 运行:`cargo test --test proptests`(proptest 默认 256 次迭代,约 1-2 秒)。

use proptest::prelude::*;

use hey::agent::{compact_messages, estimate_tokens, is_allowed};
use hey::config::Config;
use hey::llm::ir::{Message, Role};

// ========== is_allowed ==========

proptest! {
    #[test]
    fn is_allowed_never_panics(
        tool in "\\w*",
        args in ".*",
        allow_rule in "(\\w+: \\w*)?",
    ) {
        let mut cfg = Config::default();
        cfg.permission.allow = vec![allow_rule];
        // 任意输入不 panic
        let _ = is_allowed(&cfg, &tool, &args);
    }

    #[test]
    fn is_allowed_empty_allow_always_true(
        tool in "\\w{0,10}",
        args in ".*",
    ) {
        let cfg = Config::default();
        // 空 allow 列表 = 默认全权限
        assert!(is_allowed(&cfg, &tool, &args));
    }
}

// ========== estimate_tokens ==========

proptest! {
    #[test]
    fn estimate_tokens_never_panics(
        texts in proptest::collection::vec(".*", 0..10),
    ) {
        let msgs: Vec<Message> = texts
            .into_iter()
            .map(|t| Message::text(Role::User, t))
            .collect();
        let _ = estimate_tokens(&msgs);
    }

    #[test]
    fn estimate_tokens_empty_zero(
        texts in proptest::collection::vec(".*", 0..1),
    ) {
        let msgs: Vec<Message> = texts
            .into_iter()
            .map(|t| Message::text(Role::User, t))
            .collect();
        if msgs.is_empty() {
            assert_eq!(estimate_tokens(&msgs), 0);
        }
    }

    #[test]
    fn estimate_tokens_positive_for_non_empty(
        texts in proptest::collection::vec(".+", 1..5),
    ) {
        let msgs: Vec<Message> = texts
            .into_iter()
            .map(|t| Message::text(Role::User, t))
            .collect();
        assert!(estimate_tokens(&msgs) > 0);
    }
}

// ========== compact_messages ==========

proptest! {
    #[test]
    fn compact_messages_never_panics(
        texts in proptest::collection::vec(".*", 0..10),
        budget in 0u64..10_000,
    ) {
        let msgs: Vec<Message> = texts
            .into_iter()
            .map(|t| Message::text(Role::User, t))
            .collect();
        let _ = compact_messages(msgs, budget);
    }

    #[test]
    fn compact_messages_output_not_larger_than_input(
        texts in proptest::collection::vec(".{0,20}", 0..10),
        budget in 0u64..10_000,
    ) {
        let msgs: Vec<Message> = texts
            .into_iter()
            .map(|t| Message::text(Role::User, t))
            .collect();
        let input_len = msgs.len();
        let out = compact_messages(msgs, budget);
        assert!(
            out.len() <= input_len,
            "裁剪后不应比输入大: {} > {}",
            out.len(),
            input_len
        );
    }
}

// ========== is_allowed with bash 特判 ==========

proptest! {
    #[test]
    fn is_allowed_bash_json_never_panics(
        cmd in "\\w*",
        _args in ".*",
        allow_pattern in "\\w*",
    ) {
        let mut cfg = Config::default();
        cfg.permission.allow = vec![format!("bash: {allow_pattern}")];
        let json_args = format!(r#"{{"cmd":"{cmd}"}}"#);
        let _ = is_allowed(&cfg, "bash", &json_args);
    }
}