From 4f244618ca261509a0f55f71aa72568f3740da26 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Mon, 18 May 2026 10:08:36 -0600 Subject: [PATCH] test: added additional test coverage to graph components --- src/config/agent.rs | 2 -- src/function/mod.rs | 17 +++++++++ src/graph/llm.rs | 27 +++++++++++++++ src/graph/types.rs | 21 ++++++++++++ src/graph/validator.rs | 78 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 143 insertions(+), 2 deletions(-) diff --git a/src/config/agent.rs b/src/config/agent.rs index 2a831e8..18f362a 100644 --- a/src/config/agent.rs +++ b/src/config/agent.rs @@ -1049,7 +1049,6 @@ description: A graph agent model: anthropic:claude-sonnet-4-6 temperature: 0.3 top_p: 0.8 -agent_session: temp global_tools: - fetch_pdf.sh mcp_servers: @@ -1074,7 +1073,6 @@ nodes: ); assert_eq!(config.temperature, Some(0.3)); assert_eq!(config.top_p, Some(0.8)); - assert_eq!(config.agent_session.as_deref(), Some("temp")); assert_eq!(config.global_tools, vec!["fetch_pdf.sh"]); assert_eq!(config.mcp_servers, vec!["pubmed-search"]); assert_eq!(config.conversation_starters, vec!["Start here"]); diff --git a/src/function/mod.rs b/src/function/mod.rs index 1512d83..867f6df 100644 --- a/src/function/mod.rs +++ b/src/function/mod.rs @@ -1429,6 +1429,23 @@ mod tests { assert!(tc.thought_signature.is_none()); } + #[test] + fn direct_invoker_maps_each_language() { + assert_eq!( + Language::Bash.direct_invoker(), + Some(("bash", &[] as &[&str])) + ); + assert_eq!( + Language::Python.direct_invoker(), + Some(("python3", &[] as &[&str])) + ); + assert_eq!( + Language::TypeScript.direct_invoker(), + Some(("npx", &["tsx"] as &[&str])) + ); + assert_eq!(Language::Unsupported.direct_invoker(), None); + } + #[test] fn toolcall_with_thought_signature() { let tc = ToolCall::new("t".into(), json!({}), None) diff --git a/src/graph/llm.rs b/src/graph/llm.rs index a8abf23..5f75cef 100644 --- a/src/graph/llm.rs +++ b/src/graph/llm.rs @@ -468,6 +468,33 @@ mod tests { assert!(state.state().get(OUTPUT_KEY).is_none()); } + #[test] + fn next_for_llm_node_success_routes_to_next() { + assert_eq!( + next_for_llm_node(Some("nx"), false, Some("fb")).unwrap(), + "nx" + ); + } + + #[test] + fn next_for_llm_node_failure_with_fallback_routes_to_fallback() { + assert_eq!( + next_for_llm_node(Some("nx"), true, Some("fb")).unwrap(), + "fb" + ); + } + + #[test] + fn next_for_llm_node_failure_without_fallback_routes_to_next() { + assert_eq!(next_for_llm_node(Some("nx"), true, None).unwrap(), "nx"); + } + + #[test] + fn next_for_llm_node_errors_without_next_or_fallback() { + assert!(next_for_llm_node(None, false, None).is_err()); + assert!(next_for_llm_node(None, true, None).is_err()); + } + fn node_with_schema(updates: Option>, schema: Value) -> LlmNode { let mut n = node_with(updates); n.output_schema = Some(schema); diff --git a/src/graph/types.rs b/src/graph/types.rs index d471a1b..864f862 100644 --- a/src/graph/types.rs +++ b/src/graph/types.rs @@ -863,6 +863,27 @@ nodes: assert!(graph.conversation_starters.is_empty()); } + #[test] + fn node_ids_lists_nodes_in_order() { + let yaml = r#" +name: g +start: first +nodes: + first: + id: first + type: agent + agent: helper + prompt: hi + next: last + last: + id: last + type: end + output: done +"#; + let graph: Graph = serde_yaml::from_str(yaml).unwrap(); + assert_eq!(graph.node_ids(), vec!["first", "last"]); + } + #[test] fn has_agent_node_detects_agent_nodes() { let with_agent = r#" diff --git a/src/graph/validator.rs b/src/graph/validator.rs index 147b125..387b4e2 100644 --- a/src/graph/validator.rs +++ b/src/graph/validator.rs @@ -464,6 +464,84 @@ mod tests { } } + fn input_node(id: &str, on_timeout: Option<&str>, next: Option<&str>) -> Node { + Node { + id: id.into(), + description: String::new(), + node_type: NodeType::Input(InputNode { + question: "?".into(), + default: None, + validation: None, + state_updates: None, + timeout: None, + on_timeout: on_timeout.map(String::from), + }), + next: next.map(String::from), + } + } + + fn llm_node(id: &str, fallback: Option<&str>, next: Option<&str>) -> Node { + Node { + id: id.into(), + description: String::new(), + node_type: NodeType::Llm(LlmNode { + instructions: None, + prompt: "p".into(), + tools: None, + model: None, + temperature: None, + top_p: None, + fallback: fallback.map(String::from), + max_attempts: 1, + max_iterations: 10, + state_updates: None, + output_schema: None, + timeout: None, + }), + next: next.map(String::from), + } + } + + #[test] + fn flags_missing_approval_on_timeout_target() { + let mut approval = approval_node("a", &["yes"], &[("yes", "end")], "end"); + if let NodeType::Approval(ref mut n) = approval.node_type { + n.on_timeout = Some("ghost".into()); + } + let graph = graph_with(vec![("a", approval), ("end", end_node("end"))], "a"); + let result = validator().validate(&graph); + assert!(!result.is_valid()); + assert!(result.errors.iter().any(|e| e.message.contains("ghost"))); + } + + #[test] + fn flags_missing_input_on_timeout_target() { + let graph = graph_with( + vec![ + ("i", input_node("i", Some("ghost"), Some("end"))), + ("end", end_node("end")), + ], + "i", + ); + let result = validator().validate(&graph); + assert!(!result.is_valid()); + assert!(result.errors.iter().any(|e| e.message.contains("ghost"))); + } + + #[test] + fn flags_missing_llm_fallback_target() { + let graph = graph_with( + vec![ + ("l", llm_node("l", Some("ghost"), Some("end"))), + ("end", end_node("end")), + ], + "l", + ); + let result = validator().validate(&graph); + assert!(!result.is_valid()); + assert!(result.errors.iter().any(|e| e.message.contains("ghost"))); + } + #[test] fn rag_node_without_documents_errors() { let graph = graph_with(