roboticus-core 0.11.2

Shared types, config parsing, personality system, and error types for the Roboticus agent runtime
Documentation
    use super::*;
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn config_toml_roundtrip_preserves_values(port in 1024u16..=65535u16) {
            let toml_str = format!(r#"
[agent]
name = "TestBot"
id = "test"
workspace = "/tmp/test"
log_level = "debug"

[server]
bind = "localhost"
port = {port}

[database]
path = "/tmp/test.db"

[models]
primary = "ollama/qwen3:8b"
"#);
            let config = RoboticusConfig::from_str(&toml_str).unwrap();
            assert_eq!(config.server.port, port);
            assert_eq!(config.server.bind, "localhost");
        }
    }

    fn minimal_toml() -> &'static str {
        r#"
[agent]
name = "TestBot"
id = "test"

[server]
port = 9999

[database]
path = "/tmp/test.db"

[models]
primary = "ollama/qwen3:8b"
"#
    }

    #[test]
    fn parse_minimal_config() {
        let cfg = RoboticusConfig::from_str(minimal_toml()).unwrap();
        assert_eq!(cfg.agent.name, "TestBot");
        assert_eq!(cfg.agent.id, "test");
        assert_eq!(cfg.server.port, 9999);
        assert_eq!(cfg.models.primary, "ollama/qwen3:8b");
    }

    #[test]
    fn defaults_applied() {
        let cfg = RoboticusConfig::from_str(minimal_toml()).unwrap();
        assert_eq!(cfg.memory.working_budget_pct, 30.0);
        assert_eq!(cfg.memory.episodic_budget_pct, 25.0);
        assert_eq!(cfg.memory.semantic_budget_pct, 20.0);
        assert_eq!(cfg.memory.procedural_budget_pct, 15.0);
        assert_eq!(cfg.memory.relationship_budget_pct, 10.0);
        assert_eq!(cfg.cache.semantic_threshold, 0.95);
        assert_eq!(cfg.cache.max_entries, 10000);
        assert_eq!(cfg.treasury.per_payment_cap, 100.0);
        assert!(cfg.skills.sandbox_env);
        assert_eq!(cfg.skills.script_timeout_seconds, 30);
        #[cfg(windows)]
        assert_eq!(
            cfg.skills.allowed_interpreters,
            vec!["bash", "python", "python3", "node"]
        );
        #[cfg(not(windows))]
        assert_eq!(
            cfg.skills.allowed_interpreters,
            vec!["bash", "python3", "node"]
        );
        assert_eq!(cfg.a2a.max_message_size, 65536);
        assert_eq!(cfg.a2a.rate_limit_per_peer, 10);
        assert!(cfg.a2a.enabled);
        assert_eq!(cfg.agent.autonomy_max_react_turns, 10);
        assert_eq!(cfg.agent.autonomy_max_turn_duration_seconds, 90);
    }

    #[test]
    fn autonomy_budget_validation_fail() {
        let toml = r#"
[agent]
name = "TestBot"
id = "test"
autonomy_max_react_turns = 0

[server]
port = 9999

[database]
path = "/tmp/test.db"

[models]
primary = "ollama/qwen3:8b"
"#;
        let err = RoboticusConfig::from_str(toml).unwrap_err();
        assert!(err.to_string().contains("autonomy_max_react_turns"));

        let toml2 = r#"
[agent]
name = "TestBot"
id = "test"
autonomy_max_turn_duration_seconds = 0

[server]
port = 9999

[database]
path = "/tmp/test.db"

[models]
primary = "ollama/qwen3:8b"
"#;
        let err2 = RoboticusConfig::from_str(toml2).unwrap_err();
        assert!(
            err2.to_string()
                .contains("autonomy_max_turn_duration_seconds")
        );
    }

    #[test]
    fn memory_budget_validation_fail() {
        let toml = r#"
[agent]
name = "TestBot"
id = "test"

[server]
port = 9999

[database]
path = "/tmp/test.db"

[models]
primary = "ollama/qwen3:8b"

[memory]
working_budget_pct = 50.0
episodic_budget_pct = 25.0
semantic_budget_pct = 20.0
procedural_budget_pct = 15.0
relationship_budget_pct = 10.0
"#;
        let err = RoboticusConfig::from_str(toml).unwrap_err();
        assert!(err.to_string().contains("sum to 100"));
    }

    #[test]
    fn treasury_validation_fail() {
        let toml = r#"
[agent]
name = "TestBot"
id = "test"

[server]
port = 9999

[database]
path = "/tmp/test.db"

[models]
primary = "ollama/qwen3:8b"

[treasury]
per_payment_cap = -1.0
"#;
        let err = RoboticusConfig::from_str(toml).unwrap_err();
        assert!(err.to_string().contains("per_payment_cap"));
    }

    #[test]
    fn revenue_swap_validation_requires_default_chain_to_exist() {
        let toml = r#"
[agent]
name = "TestBot"
id = "test"

[server]
port = 9999

[database]
path = "/tmp/test.db"

[models]
primary = "ollama/qwen3:8b"

[treasury]

[treasury.revenue_swap]
enabled = true
target_symbol = "PUSD"
default_chain = "ARBITRUM"

[[treasury.revenue_swap.chains]]
chain = "ETH"
target_contract_address = "0xfaf0cee6b20e2aaa4b80748a6af4cd89609a3d78"
"#;
        let err = RoboticusConfig::from_str(toml).unwrap_err();
        assert!(err.to_string().contains("default_chain"));
    }

    #[test]
    fn revenue_swap_validation_rejects_duplicate_chain_entries() {
        let toml = r#"
[agent]
name = "TestBot"
id = "test"

[server]
port = 9999

[database]
path = "/tmp/test.db"

[models]
primary = "ollama/qwen3:8b"

[treasury]

[treasury.revenue_swap]
enabled = true
target_symbol = "PUSD"
default_chain = "ETH"

[[treasury.revenue_swap.chains]]
chain = "ETH"
target_contract_address = "0xfaf0cee6b20e2aaa4b80748a6af4cd89609a3d78"

[[treasury.revenue_swap.chains]]
chain = "eth"
target_contract_address = "0x1111111111111111111111111111111111111111"
"#;
        let err = RoboticusConfig::from_str(toml).unwrap_err();
        assert!(err.to_string().contains("duplicate chain"));
    }

    #[test]
    fn full_config_roundtrip() {
        let toml = r#"
[agent]
name = "Duncan Idaho"
id = "duncan"
workspace = "/tmp/workspace"
log_level = "debug"

[server]
port = 18789
bind = "0.0.0.0"

[database]
path = "/tmp/state.db"

[models]
primary = "openai/gpt-5.3-codex"
fallbacks = ["google/gemini-3-flash", "ollama/qwen3:14b"]

[models.routing]
mode = "metascore"
confidence_threshold = 0.85
local_first = true

[providers.anthropic]
url = "https://api.anthropic.com"
tier = "T3"

[providers.ollama]
url = "http://localhost:11434"
tier = "T1"

[circuit_breaker]
threshold = 5
window_seconds = 120

[memory]
working_budget_pct = 30.0
episodic_budget_pct = 25.0
semantic_budget_pct = 20.0
procedural_budget_pct = 15.0
relationship_budget_pct = 10.0

[cache]
enabled = true
exact_match_ttl_seconds = 7200
semantic_threshold = 0.92
max_entries = 5000

[treasury]
per_payment_cap = 50.0
hourly_transfer_limit = 200.0
daily_transfer_limit = 1000.0
minimum_reserve = 10.0
daily_inference_budget = 25.0

[yield]
enabled = false
protocol = "aave"
chain = "base"
min_deposit = 100.0
withdrawal_threshold = 50.0

[wallet]
path = "/tmp/wallet.json"
chain_id = 8453
rpc_url = "https://mainnet.base.org"

[a2a]
enabled = true
max_message_size = 32768
rate_limit_per_peer = 5
session_timeout_seconds = 1800
require_on_chain_identity = true

[skills]
skills_dir = "/tmp/skills"
script_timeout_seconds = 15
script_max_output_bytes = 524288
allowed_interpreters = ["bash", "python3"]
sandbox_env = true
hot_reload = true
"#;
        let cfg = RoboticusConfig::from_str(toml).unwrap();
        assert_eq!(cfg.agent.name, "Duncan Idaho");
        assert_eq!(cfg.models.routing.confidence_threshold, 0.85);
        assert!(
            cfg.providers.len() >= 2,
            "user providers plus bundled defaults"
        );
        assert!(cfg.providers.contains_key("anthropic"));
        assert!(cfg.providers.contains_key("ollama"));
        assert_eq!(cfg.providers["anthropic"].url, "https://api.anthropic.com");
        assert_eq!(cfg.providers["anthropic"].tier, "T3");
        assert_eq!(cfg.circuit_breaker.threshold, 5);
        assert_eq!(cfg.cache.semantic_threshold, 0.92);
        assert_eq!(cfg.treasury.per_payment_cap, 50.0);
        assert!(!cfg.r#yield.enabled);
        assert_eq!(cfg.a2a.max_message_size, 32768);
        assert_eq!(cfg.skills.script_timeout_seconds, 15);
        assert_eq!(cfg.skills.allowed_interpreters, vec!["bash", "python3"]);
    }

    #[test]
    fn config_from_missing_file() {
        let result = RoboticusConfig::from_file(Path::new("/nonexistent/config.toml"));
        assert!(result.is_err());
    }