From 2377d594411697fa828eade2d9822631eb3383dd Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Tue, 24 Mar 2026 04:04:47 +0000 Subject: [PATCH] test: add coverage for graph queries and parser error paths (8 new tests) New tests in src/graph/queries.rs (6 tests): - test_find_action_invokers_returns_reasoning_action - test_find_variable_readers_detects_read_edge - test_find_variable_writers_empty_when_no_set_stmt - test_get_topic_reasoning_actions_returns_all_actions - test_get_topic_action_defs_returns_all_defs - test_topic_execution_order_returns_none_for_cyclic_graph New tests in tests/integration_test.rs (2 tests): - test_parse_unterminated_string_literal_returns_error - test_parse_agent_name_without_quotes_returns_error Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/graph/queries.rs | 126 ++++++++++++++++++++++++++++++++++++++ tests/integration_test.rs | 29 +++++++++ 2 files changed, 155 insertions(+) diff --git a/src/graph/queries.rs b/src/graph/queries.rs index 7508e45..397eb46 100644 --- a/src/graph/queries.rs +++ b/src/graph/queries.rs @@ -347,4 +347,130 @@ topic main: // At least one edge should exist (the Routes edge from start_agent → main) assert!(graph.edge_count() > 0, "Expected at least one edge in the graph"); } + + /// Source with one topic containing an action definition and a reasoning action + /// that invokes it, plus a variable that the reasoning action writes to. + fn action_and_variable_source() -> &'static str { + r#"config: + agent_name: "Test" + +variables: + result: mutable string = "" + description: "Action result" + +topic main: + description: "Main" + + actions: + do_thing: + description: "Do the thing" + inputs: + query: string + description: "Query" + outputs: + answer: string + description: "Answer" + target: "flow://DoThing" + + reasoning: + instructions: "Help the user" + actions: + call_do_thing: @actions.do_thing + description: "Call do_thing" + with: + query: @variables.result +"# + } + + #[test] + fn test_find_action_invokers_returns_reasoning_action() { + // The reasoning action `call_do_thing` invokes the action def `do_thing`. + // find_action_invokers(do_thing) should return the reasoning action node. + let graph = parse_and_build(action_and_variable_source()); + let action_def_idx = graph + .get_action_def("main", "do_thing") + .expect("do_thing action def not found"); + + let invokers = graph.find_action_invokers(action_def_idx); + assert_eq!(invokers.len(), 1, "Expected exactly one invoker of do_thing"); + } + + #[test] + fn test_find_variable_readers_detects_read_edge() { + // The reasoning action reads @variables.result as an input binding. + // find_variable_readers(result) should return at least one node. + let graph = parse_and_build(action_and_variable_source()); + let var_idx = graph.get_variable("result").expect("variable 'result' not found"); + + let readers = graph.find_variable_readers(var_idx); + assert!(!readers.is_empty(), "Expected at least one reader of variable 'result'"); + } + + #[test] + fn test_find_variable_writers_empty_when_no_set_stmt() { + // The source above never uses `set @variables.result = ...`, so there should + // be no writer edges pointing at the variable node. + let graph = parse_and_build(action_and_variable_source()); + let var_idx = graph.get_variable("result").expect("variable 'result' not found"); + + let writers = graph.find_variable_writers(var_idx); + assert!(writers.is_empty(), "Expected no writers — variable is only read, never set"); + } + + #[test] + fn test_get_topic_reasoning_actions_returns_all_actions() { + // get_topic_reasoning_actions("main") must return the `call_do_thing` node. + let graph = parse_and_build(action_and_variable_source()); + + let actions = graph.get_topic_reasoning_actions("main"); + assert_eq!(actions.len(), 1, "Expected 1 reasoning action in topic 'main'"); + } + + #[test] + fn test_get_topic_action_defs_returns_all_defs() { + // get_topic_action_defs("main") must return the `do_thing` node. + let graph = parse_and_build(action_and_variable_source()); + + let defs = graph.get_topic_action_defs("main"); + assert_eq!(defs.len(), 1, "Expected 1 action def in topic 'main'"); + } + + #[test] + fn test_topic_execution_order_returns_none_for_cyclic_graph() { + // A → B and B → A form a cycle. topic_execution_order() must return None + // because toposort fails on cyclic graphs. + let source = r#"config: + agent_name: "Test" + +start_agent selector: + description: "Route" + reasoning: + instructions: "Select" + actions: + go_a: @utils.transition to @topic.topic_a + description: "Go to A" + +topic topic_a: + description: "Topic A" + reasoning: + instructions: "In A" + actions: + go_b: @utils.transition to @topic.topic_b + description: "Go to B" + +topic topic_b: + description: "Topic B" + reasoning: + instructions: "In B" + actions: + back_to_a: @utils.transition to @topic.topic_a + description: "Back to A" +"#; + let graph = parse_and_build(source); + let order = graph.topic_execution_order(); + assert!( + order.is_none(), + "Expected None from topic_execution_order for a cyclic graph" + ); + } } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index f5db09b..03f1097 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -271,4 +271,33 @@ topic main: .collect(); assert!(tokens.iter().any(|t| matches!(t, lexer::Token::Config))); } + + // ------------------------------------------------------------------------- + // Parser error-path tests + // ------------------------------------------------------------------------- + + #[test] + fn test_parse_unterminated_string_literal_returns_error() { + // An unterminated string literal causes a lexer error before the parser + // even runs. parse_source must return Err in this case. + let source = "config:\n agent_name: \"unterminated"; + let result = parse_source(source); + assert!( + result.is_err(), + "Unterminated string literal should not parse successfully" + ); + } + + #[test] + fn test_parse_agent_name_without_quotes_returns_error() { + // agent_name requires a quoted string literal. A bare identifier is a + // token type mismatch that the config parser cannot recover from cleanly, + // so parse_source must report at least one error. + let source = "config:\n agent_name: BareIdentifier\n"; + let result = parse_source(source); + assert!( + result.is_err(), + "agent_name with an unquoted bare identifier should not parse successfully" + ); + } }