use super::*;
const EXTENDED: &str = r#"
graph orchestrator {
start planner
input {
request string
customer_id string
}
output {
answer string
}
checkpoint inherit
interrupt manual
channel messages messages
channel usage aggregate "usage_delta"
channel arrivals barrier 2
node planner {
kind agent
model "default"
command {
goto fanout
update {
status "planned"
}
}
}
node fanout {
kind model
sends [
send worker_a "split_a"
send worker_b "split_b"
]
next worker_a
}
node worker_a {
kind model
next gather
}
node worker_b {
kind model
next gather
}
node gather {
kind join
sources [worker_a, worker_b]
next research
}
node research {
kind subagent
agent "researcher"
input "topic"
timeout 30
retry {
max_attempts 3
backoff "exponential"
}
next sub
}
node sub {
kind subgraph
graph "summarize"
next triage
}
node triage {
kind repl_agent
model "default"
script "triage_script"
next review
}
node review {
kind interrupt
prompt "Approve?"
options ["approve", "reject"]
routes {
approve -> END
reject -> planner
}
}
join [worker_a, worker_b] -> gather
}
"#;
#[test]
fn extended_grammar_parses_and_compiles_blueprint_shape() {
let program = parse_str(EXTENDED).unwrap();
let bp = compile(&program).unwrap().remove(0);
assert_eq!(bp.graph_id, "orchestrator");
assert_eq!(bp.start, "planner");
assert_eq!(bp.checkpoint.as_deref(), Some("inherit"));
assert_eq!(bp.interrupt.as_deref(), Some("manual"));
assert_eq!(bp.input.len(), 2);
assert_eq!(bp.input[0].name, "request");
assert_eq!(bp.input[0].ty, "string");
assert_eq!(bp.output.len(), 1);
assert_eq!(bp.output[0].name, "answer");
assert_eq!(bp.channels.len(), 3);
let usage = bp.channels.iter().find(|c| c.name == "usage").unwrap();
assert_eq!(usage.reducer, "aggregate");
assert_eq!(usage.args, vec![Literal::Str("usage_delta".into())]);
let arrivals = bp.channels.iter().find(|c| c.name == "arrivals").unwrap();
assert_eq!(arrivals.reducer, "barrier");
assert_eq!(arrivals.args, vec![Literal::Num(2.0)]);
let planner = bp.nodes.iter().find(|n| n.name == "planner").unwrap();
let cmd = planner.command.as_ref().unwrap();
assert_eq!(cmd.goto.as_deref(), Some("fanout"));
assert_eq!(
cmd.update,
vec![("status".into(), Literal::Str("planned".into()))]
);
assert_eq!(planner.routing, Routing::Next("fanout".into()));
let fanout = bp.nodes.iter().find(|n| n.name == "fanout").unwrap();
assert_eq!(fanout.sends.len(), 2);
assert_eq!(fanout.sends[0].target, "worker_a");
assert_eq!(fanout.sends[0].input.as_deref(), Some("split_a"));
let gather = bp.nodes.iter().find(|n| n.name == "gather").unwrap();
assert_eq!(gather.kind, "join");
assert_eq!(gather.join_sources, vec!["worker_a", "worker_b"]);
let research = bp.nodes.iter().find(|n| n.name == "research").unwrap();
assert_eq!(research.kind, "subagent");
assert_eq!(research.agent.as_deref(), Some("researcher"));
assert_eq!(research.input.as_deref(), Some("topic"));
assert_eq!(research.timeout.as_deref(), Some("30"));
assert_eq!(
research.retry,
vec![
("max_attempts".into(), Literal::Num(3.0)),
("backoff".into(), Literal::Str("exponential".into())),
]
);
let sub = bp.nodes.iter().find(|n| n.name == "sub").unwrap();
assert_eq!(sub.kind, "subgraph");
assert_eq!(sub.subgraph.as_deref(), Some("summarize"));
let triage = bp.nodes.iter().find(|n| n.name == "triage").unwrap();
assert_eq!(triage.kind, "repl_agent");
assert_eq!(triage.script.as_deref(), Some("triage_script"));
let review = bp.nodes.iter().find(|n| n.name == "review").unwrap();
assert_eq!(review.kind, "interrupt");
assert_eq!(review.options, vec!["approve", "reject"]);
assert!(matches!(review.routing, Routing::Conditional(_)));
assert_eq!(bp.joins.len(), 1);
assert_eq!(bp.joins[0].target, "gather");
assert_eq!(bp.joins[0].sources, vec!["worker_a", "worker_b"]);
}
#[test]
fn extended_blueprint_round_trips_through_serde() {
let bp = compile(&parse_str(EXTENDED).unwrap()).unwrap().remove(0);
let json = serde_json::to_string(&bp).unwrap();
let back: crate::language::types::Blueprint = serde_json::from_str(&json).unwrap();
assert_eq!(bp, back);
}
#[test]
fn command_goto_unknown_target_is_a_compile_error() {
let src = "graph g { start a node a { command { goto ghost } } }";
let err = compile(&parse_str(src).unwrap()).unwrap_err();
assert!(err.to_string().contains("command goto target"), "{err}");
}
#[test]
fn send_unknown_target_is_a_compile_error() {
let src = "graph g { start a node a { sends [ send ghost ] } }";
let err = compile(&parse_str(src).unwrap()).unwrap_err();
assert!(err.to_string().contains("send target"), "{err}");
}
#[test]
fn join_unknown_source_is_a_compile_error() {
let src = "graph g { start a node a { } join [ghost] -> a }";
let err = compile(&parse_str(src).unwrap()).unwrap_err();
assert!(err.to_string().contains("join source"), "{err}");
}
#[test]
fn extended_kinds_bind_against_a_resolver() {
let bp = compile(&parse_str(EXTENDED).unwrap()).unwrap().remove(0);
let resolver = CapabilityResolver::from_lists(["default".to_string()], std::iter::empty())
.allow_subgraph("summarize")
.allow_agent("researcher")
.allow_script("triage_script")
.allow_reducer("messages")
.allow_reducer("aggregate")
.allow_reducer("barrier")
.with_node_kinds(
crate::language::capability_resolver::DEFAULT_NODE_KINDS
.iter()
.map(|k| k.to_string()),
);
resolver.bind_blueprint(&bp).unwrap();
}
#[test]
fn bind_blueprint_rejects_unregistered_subagent_and_script() {
let node_kinds = || {
crate::language::capability_resolver::DEFAULT_NODE_KINDS
.iter()
.map(|k| k.to_string())
};
let bp = compile(&parse_str(EXTENDED).unwrap()).unwrap().remove(0);
let missing_agent = CapabilityResolver::from_lists(["default".to_string()], std::iter::empty())
.allow_subgraph("summarize")
.allow_script("triage_script")
.allow_reducer("messages")
.allow_reducer("aggregate")
.allow_reducer("barrier")
.with_node_kinds(node_kinds());
let err = missing_agent.bind_blueprint(&bp).unwrap_err();
assert!(matches!(err, crate::error::TinyAgentsError::Capability(_)));
assert!(err.to_string().contains("unknown agent"), "{err}");
assert!(err.to_string().contains("researcher"), "{err}");
let missing_script =
CapabilityResolver::from_lists(["default".to_string()], std::iter::empty())
.allow_subgraph("summarize")
.allow_agent("researcher")
.allow_reducer("messages")
.allow_reducer("aggregate")
.allow_reducer("barrier")
.with_node_kinds(node_kinds());
let err = missing_script.bind_blueprint(&bp).unwrap_err();
assert!(err.to_string().contains("unknown script"), "{err}");
assert!(err.to_string().contains("triage_script"), "{err}");
}