foundation_ai 0.0.1

AI foundation crate for the eweplatform
use foundation_ai::agentic::errors::*;
use foundation_ai::errors::GenerationError;
use foundation_ai::types::base_types::ModelId;

fn gen_err(msg: &str) -> GenerationError {
    GenerationError::Generic(msg.to_string())
}

#[test]
fn agentic_error_round_trips_through_json() {
    let errors = vec![
        AgenticError::Generation(GenerationFailure {
            kind: GenKind::Provider,
            message: "boom".into(),
        }),
        AgenticError::ToolCall {
            tool_name: "read".into(),
            reason: "no file".into(),
        },
        AgenticError::ToolNotAuthorized {
            tool_name: "shell".into(),
            user: UserId("u1".into()),
        },
        AgenticError::Budget { limit: 1000 },
        AgenticError::LoopDetected(LoopDetection {
            kind: "tool_call".into(),
            occurrences: 3,
        }),
        AgenticError::Auth(AuthError {
            reason: "expired".into(),
        }),
        AgenticError::Routing("no provider for model x".into()),
        AgenticError::Unexpected("???".into()),
    ];
    for e in errors {
        let json = serde_json::to_string(&e).unwrap();
        let back: AgenticError = serde_json::from_str(&json).unwrap();
        assert_eq!(e, back);
    }
}

#[test]
fn from_generation_classifies_rate_limit() {
    let e = gen_err("Rate limit exceeded: retry after 5s");
    let ae = AgenticError::from_generation(&e, None, 8192);
    match ae {
        AgenticError::Generation(g) => assert_eq!(g.kind, GenKind::RateLimit),
        other => panic!("expected Generation, got {other:?}"),
    }
}

#[test]
fn from_generation_classifies_network() {
    let e = gen_err("connection timed out");
    let ae = AgenticError::from_generation(&e, None, 8192);
    match ae {
        AgenticError::Generation(g) => assert_eq!(g.kind, GenKind::Network),
        other => panic!("expected Generation, got {other:?}"),
    }
}

#[test]
fn from_generation_defaults_to_provider() {
    let e = gen_err("the model refused");
    let ae = AgenticError::from_generation(&e, None, 8192);
    match ae {
        AgenticError::Generation(g) => {
            assert_eq!(g.kind, GenKind::Provider);
            assert!(g.message.contains("refused"));
        }
        other => panic!("expected Generation, got {other:?}"),
    }
}

#[test]
fn policy_maps_each_kind_to_the_right_action() {
    let p = ErrorPolicy::new();
    assert_eq!(
        p.classify(AgenticError::Generation(GenerationFailure {
            kind: GenKind::ContextOverflow,
            message: String::new(),
        })),
        AgentAction::RetryWithReducedContext
    );
    assert_eq!(
        p.classify(AgenticError::Generation(GenerationFailure {
            kind: GenKind::RateLimit,
            message: String::new(),
        })),
        AgentAction::SwitchModel
    );
    assert_eq!(
        p.classify(AgenticError::ToolCall {
            tool_name: "x".into(),
            reason: "y".into(),
        }),
        AgentAction::Continue
    );
    assert_eq!(
        p.classify(AgenticError::Budget { limit: 1 }),
        AgentAction::Terminate(AgenticError::Budget { limit: 1 })
    );
    assert!(matches!(
        p.classify(AgenticError::Generation(GenerationFailure {
            kind: GenKind::Provider,
            message: String::new(),
        })),
        AgentAction::Terminate(_)
    ));
}

#[test]
fn circuit_breaker_switches_then_exhausts() {
    let fallbacks = vec![
        ModelId::Name("backup-1".into(), None),
        ModelId::Name("backup-2".into(), None),
    ];
    let mut cb = CircuitBreaker::new(2, fallbacks);

    assert_eq!(cb.on_failure(), None);
    assert_eq!(
        cb.on_failure(),
        Some(ModelId::Name("backup-1".into(), None))
    );
    assert_eq!(cb.on_failure(), None);
    assert_eq!(
        cb.on_failure(),
        Some(ModelId::Name("backup-2".into(), None))
    );
    assert!(cb.is_exhausted());
    assert_eq!(cb.on_failure(), None);
    assert_eq!(cb.on_failure(), None);
}

#[test]
fn circuit_breaker_reset_on_success() {
    let mut cb = CircuitBreaker::new(3, vec![ModelId::Name("b".into(), None)]);
    cb.on_failure();
    cb.on_failure();
    cb.on_success();
    assert_eq!(cb.on_failure(), None);
    assert_eq!(cb.on_failure(), None);
    assert_eq!(cb.on_failure(), Some(ModelId::Name("b".into(), None)));
}