use std::collections::HashMap;
use foundation_ai::agentic::tool_impl::{ToolCallManager, ToolCallRequest, ToolCallStage};
use foundation_ai::types::{ExecutionHint, SessionId};
fn call(id: &str, deps: &[&str], hint: ExecutionHint) -> ToolCallRequest {
ToolCallRequest {
id: id.into(),
name: format!("tool_{id}"),
arguments: HashMap::new(),
depends_on: deps.iter().map(|s| (*s).to_string()).collect(),
execution_hint: hint,
}
}
fn stage_ids(stage: &ToolCallStage) -> Vec<String> {
let calls = match stage {
ToolCallStage::Parallel { calls, .. } | ToolCallStage::Sequential { calls, .. } => calls,
};
let mut ids: Vec<String> = calls.iter().map(|c| c.id.clone()).collect();
ids.sort();
ids
}
fn mgr() -> ToolCallManager {
ToolCallManager::new(SessionId::new())
}
#[test]
fn empty_calls_produce_no_stages() {
let wf = mgr().build_workflow(&[]).expect("empty is valid");
assert!(wf.stages.is_empty());
}
#[test]
fn independent_calls_land_in_one_stage() {
let calls = vec![
call("a", &[], ExecutionHint::Unspecified),
call("b", &[], ExecutionHint::Unspecified),
];
let wf = mgr().build_workflow(&calls).expect("valid");
assert_eq!(wf.stages.len(), 1, "no dependencies => a single stage");
assert_eq!(stage_ids(&wf.stages[0]), vec!["a", "b"]);
}
#[test]
fn dependency_creates_ordered_stages() {
let calls = vec![
call("a", &[], ExecutionHint::Unspecified),
call("b", &["a"], ExecutionHint::Unspecified),
call("c", &["b"], ExecutionHint::Unspecified),
];
let wf = mgr().build_workflow(&calls).expect("valid");
assert_eq!(wf.stages.len(), 3, "a -> b -> c is three depth stages");
assert_eq!(stage_ids(&wf.stages[0]), vec!["a"]);
assert_eq!(stage_ids(&wf.stages[1]), vec!["b"]);
assert_eq!(stage_ids(&wf.stages[2]), vec!["c"]);
}
#[test]
fn diamond_dependency_groups_by_depth() {
let calls = vec![
call("a", &[], ExecutionHint::Unspecified),
call("b", &["a"], ExecutionHint::Unspecified),
call("c", &["a"], ExecutionHint::Unspecified),
call("d", &["b", "c"], ExecutionHint::Unspecified),
];
let wf = mgr().build_workflow(&calls).expect("valid");
assert_eq!(wf.stages.len(), 3);
assert_eq!(stage_ids(&wf.stages[0]), vec!["a"]);
assert_eq!(stage_ids(&wf.stages[1]), vec!["b", "c"], "b and c run together");
assert_eq!(stage_ids(&wf.stages[2]), vec!["d"]);
}
#[test]
fn cyclic_dependency_is_rejected() {
let calls = vec![
call("a", &["b"], ExecutionHint::Unspecified),
call("b", &["a"], ExecutionHint::Unspecified),
];
let err = mgr()
.build_workflow(&calls)
.expect_err("a cycle must be rejected");
assert!(
format!("{err:?}").to_lowercase().contains("cyclic"),
"error should name the cyclic dependency: {err:?}"
);
}
#[test]
fn unknown_dependency_is_rejected() {
let calls = vec![call("a", &["ghost"], ExecutionHint::Unspecified)];
let err = mgr()
.build_workflow(&calls)
.expect_err("a dep on a non-existent call must be rejected");
assert!(
format!("{err:?}").to_lowercase().contains("unknown"),
"error should name the unknown dependency: {err:?}"
);
}
#[test]
fn sequential_hint_makes_its_stage_sequential() {
let calls = vec![
call("a", &[], ExecutionHint::Sequential),
call("b", &[], ExecutionHint::Unspecified),
];
let wf = mgr().build_workflow(&calls).expect("valid");
assert_eq!(wf.stages.len(), 1);
assert!(
matches!(wf.stages[0], ToolCallStage::Sequential { .. }),
"a Sequential hint in the stage forces the whole stage sequential"
);
}