foundation_ai 0.0.1

AI foundation crate for the eweplatform
#![allow(unused_imports)]
use std::collections::HashMap;

use foundation_ai::agentic::testing::*;
use foundation_ai::types::{
    ArgType, CostStatus, ModelInteraction, ModelOutput, Messages, StopReason, TextContent,
    ToolShed, UsageCosting, UsageReport, UserModelContent,
};
use foundation_ai::agentic::tool_impl::ToolImpl;

#[test]
fn mock_provider_returns_scripted_reply() {
    let mut mock = MockModelProvider::new();
    mock.on(|_| true, vec![mock_text("hello")]);

    let mi = ModelInteraction {
        system_prompt: None,
        soul: None,
        tools_shed: ToolShed::default(),
        messages: vec![mock_user("hi")],
        chat_template: None,
        tool_choice: None,
    };

    let result = mock.resolve(&mi).expect("should match");
    assert_eq!(result.len(), 1);
    match &result[0] {
        Messages::Assistant { content, .. } => match content {
            ModelOutput::Text(t) => assert_eq!(t.content, "hello"),
            other => panic!("expected text, got {other:?}"),
        },
        other => panic!("expected assistant, got {other:?}"),
    }
}

#[test]
fn mock_provider_nth_call_matching() {
    let mut mock = MockModelProvider::new();
    mock.on_nth_call(0, vec![mock_text("first")])
        .on_nth_call(1, vec![mock_text("second")])
        .on_any(vec![mock_text("fallback")]);

    let mi = ModelInteraction {
        system_prompt: None,
        soul: None,
        tools_shed: ToolShed::default(),
        messages: vec![mock_user("test")],
        chat_template: None,
        tool_choice: None,
    };

    let r0 = mock.resolve(&mi).unwrap();
    assert!(
        matches!(&r0[0], Messages::Assistant { content: ModelOutput::Text(t), .. } if t.content == "first")
    );

    let r1 = mock.resolve(&mi).unwrap();
    assert!(
        matches!(&r1[0], Messages::Assistant { content: ModelOutput::Text(t), .. } if t.content == "second")
    );

    let r2 = mock.resolve(&mi).unwrap();
    assert!(
        matches!(&r2[0], Messages::Assistant { content: ModelOutput::Text(t), .. } if t.content == "fallback")
    );
}

#[test]
fn mock_provider_failure_injection() {
    let mut mock = MockModelProvider::new();
    mock.fail_with(|_| true, "injected error");

    let mi = ModelInteraction {
        system_prompt: None,
        soul: None,
        tools_shed: ToolShed::default(),
        messages: vec![mock_user("hi")],
        chat_template: None,
        tool_choice: None,
    };

    let err = mock.resolve(&mi).unwrap_err();
    assert!(err.to_string().contains("injected error"));
}

#[test]
fn mock_provider_no_match_errors() {
    let mock = MockModelProvider::new();

    let mi = ModelInteraction {
        system_prompt: None,
        soul: None,
        tools_shed: ToolShed::default(),
        messages: vec![],
        chat_template: None,
        tool_choice: None,
    };

    let err = mock.resolve(&mi).unwrap_err();
    assert!(err.to_string().contains("no matching script"));
}

#[test]
fn mock_provider_call_count() {
    let mut mock = MockModelProvider::new();
    mock.on_any(vec![mock_text("ok")]);

    let mi = ModelInteraction {
        system_prompt: None,
        soul: None,
        tools_shed: ToolShed::default(),
        messages: vec![],
        chat_template: None,
        tool_choice: None,
    };

    assert_eq!(mock.call_count(), 0);
    let _ = mock.resolve(&mi);
    assert_eq!(mock.call_count(), 1);
    let _ = mock.resolve(&mi);
    assert_eq!(mock.call_count(), 2);
}

#[test]
fn last_user_contains_matcher() {
    let mi = ModelInteraction {
        system_prompt: None,
        soul: None,
        tools_shed: ToolShed::default(),
        messages: vec![mock_user("fix the bug")],
        chat_template: None,
        tool_choice: None,
    };

    assert!(last_user_contains("fix")(&mi));
    assert!(!last_user_contains("deploy")(&mi));
}

#[test]
fn system_prompt_contains_matcher() {
    let mi = ModelInteraction {
        system_prompt: Some("You are a coding assistant.".into()),
        soul: None,
        tools_shed: ToolShed::default(),
        messages: vec![],
        chat_template: None,
        tool_choice: None,
    };

    assert!(system_prompt_contains("coding")(&mi));
    assert!(!system_prompt_contains("chef")(&mi));
}

#[test]
fn mock_text_builds_assistant_message() {
    let msg = mock_text("hello world");
    match msg {
        Messages::Assistant {
            content: ModelOutput::Text(t),
            stop_reason,
            ..
        } => {
            assert_eq!(t.content, "hello world");
            assert_eq!(stop_reason, StopReason::Stop);
        }
        other => panic!("expected Assistant text, got {other:?}"),
    }
}

#[test]
fn mock_text_usage_carries_report() {
    let usage = UsageReport {
        input: 100.0,
        output: 50.0,
        cache_read: 0.0,
        cache_write: 0.0,
        total_tokens: 150.0,
        cost: UsageCosting::zero(CostStatus::Actual),
    };
    let msg = mock_text_usage("hi", usage.clone());
    match msg {
        Messages::Assistant { usage: u, .. } => {
            assert_eq!(u.input, 100.0);
            assert_eq!(u.total_tokens, 150.0);
        }
        other => panic!("expected Assistant, got {other:?}"),
    }
}

#[test]
fn mock_tool_call_builds_message() {
    let mut args = HashMap::new();
    args.insert("path".into(), foundation_ai::types::ArgType::Text("/tmp".into()));
    let msg = mock_tool_call("read_file", args);
    match msg {
        Messages::Assistant {
            content:
                ModelOutput::ToolCall {
                    name, arguments, ..
                },
            stop_reason,
            ..
        } => {
            assert_eq!(name, "read_file");
            assert!(arguments.is_some());
            assert_eq!(stop_reason, StopReason::ToolUse);
        }
        other => panic!("expected tool call, got {other:?}"),
    }
}

#[test]
fn mock_tool_returning_succeeds() {
    let tool = MockTool::returning("echo", "echoed");
    let result = futures_lite::future::block_on(tool.execute(HashMap::new())).unwrap();
    match &result.content {
        UserModelContent::Text(t) => assert_eq!(t.content, "echoed"),
        other => panic!("expected text, got {other:?}"),
    }
}

#[test]
fn mock_tool_failing_errors() {
    let tool = MockTool::failing("broken", "always fails");
    let err = futures_lite::future::block_on(tool.execute(HashMap::new())).unwrap_err();
    assert!(err.to_string().contains("always fails"));
}

#[test]
fn mock_tool_fails_then_succeeds() {
    use foundation_ai::agentic::tool_impl::{ToolCallResult, ToolError};

    let tool = MockTool::new(
        "flaky",
        ToolBehavior::FailsThenSucceeds {
            failures: 2,
            error: ToolError::Execution {
                tool: "flaky".into(),
                reason: "transient".into(),
            },
            result: ToolCallResult {
                content: UserModelContent::Text(TextContent {
                    content: "ok".into(),
                    signature: None,
                }),
                error_detail: None,
            },
        },
    );

    use futures_lite::future::block_on;

    assert!(block_on(tool.execute(HashMap::new())).is_err());
    assert!(block_on(tool.execute(HashMap::new())).is_err());
    assert!(block_on(tool.execute(HashMap::new())).is_ok());
    assert!(block_on(tool.execute(HashMap::new())).is_ok());
}

#[test]
fn mock_provider_into_router() {
    use foundation_ai::types::ModelId;

    let mut mock = MockModelProvider::new();
    mock.on_any(vec![mock_text("routed")]);
    let router = mock.into_router();

    assert!(router.is_single());
    let provider = router
        .resolve(&ModelId::Name("anything".into(), None))
        .unwrap();
    assert_eq!(provider.name(), "mock");
}

#[test]
fn mock_stream_iterator_yields_messages() {
    use foundation_core::valtron::Stream;

    let messages = vec![mock_text("a"), mock_text("b")];
    let mut iter = MockStreamIterator::new(messages);

    assert!(matches!(iter.next(), Some(Stream::Init)));
    assert!(matches!(iter.next(), Some(Stream::Next(_))));
    assert!(matches!(iter.next(), Some(Stream::Next(_))));
    assert!(iter.next().is_none());
}

#[test]
fn routable_provider_serves_all() {
    use foundation_ai::types::ModelId;
    use foundation_ai::types::RoutableProvider;

    let mock = MockModelProvider::new();
    assert!(mock.serves(&ModelId::Name("gpt-4".into(), None)));
    assert!(mock.serves(&ModelId::Name("claude-3".into(), None)));
}

#[test]
fn tool_definition_has_correct_name() {
    let tool = MockTool::returning("my_tool", "result");
    let def = tool.definition();
    assert_eq!(def.name(), "my_tool");
    // Default category is a RECOGNIZED slot ("shell") so the mock tool actually
    // populates the ToolShed handed to the model; override via with_category.
    assert_eq!(def.category().unwrap(), "shell");
    assert_eq!(
        MockTool::returning("t", "r").with_category("search").definition().category().unwrap(),
        "search"
    );
}