diff --git a/src/graph/queries.rs b/src/graph/queries.rs index 7508e45..ee25cc4 100644 --- a/src/graph/queries.rs +++ b/src/graph/queries.rs @@ -347,4 +347,92 @@ 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 a topic that defines an action def and a reasoning action that invokes + /// it, reads a variable, and writes another variable. + fn source_with_invocation() -> &'static str { + r#"config: + agent_name: "Test" + +variables: + param_val: mutable string = "" + result: mutable string = "" + +topic main: + description: "Main topic" + + actions: + do_something: + description: "An action" + inputs: + param: string + description: "Input parameter" + outputs: + data: string + description: "Output data" + target: "flow://DoSomething" + + reasoning: + instructions: "Handle" + actions: + invoke_something: @actions.do_something + description: "Invoke do_something" + with param=@variables.param_val + set @variables.result = @outputs.data +"# + } + + #[test] + fn test_find_action_invokers() { + // A reasoning action referencing @actions.do_something via an Invokes edge should + // appear in find_action_invokers(do_something_idx). + let graph = parse_and_build(source_with_invocation()); + let action_def_idx = graph + .get_action_def("main", "do_something") + .expect("do_something action def not found"); + let invokers = graph.find_action_invokers(action_def_idx); + assert_eq!(invokers.len(), 1, "Expected exactly 1 invoker for do_something"); + } + + #[test] + fn test_find_variable_readers() { + // A reasoning action with `with param=@variables.param_val` creates a Reads edge. + // find_variable_readers should return that action. + let graph = parse_and_build(source_with_invocation()); + let var_idx = graph + .get_variable("param_val") + .expect("param_val variable not found"); + let readers = graph.find_variable_readers(var_idx); + assert!(!readers.is_empty(), "Expected at least one reader for param_val"); + } + + #[test] + fn test_find_variable_writers() { + // A reasoning action with `set @variables.result = @outputs.data` creates a Writes + // edge. find_variable_writers should return that action. + let graph = parse_and_build(source_with_invocation()); + let var_idx = graph + .get_variable("result") + .expect("result variable not found"); + let writers = graph.find_variable_writers(var_idx); + assert!(!writers.is_empty(), "Expected at least one writer for result"); + } + + #[test] + fn test_get_topic_reasoning_actions() { + // get_topic_reasoning_actions should return the reasoning actions declared in a + // topic's reasoning block (one in this case: invoke_something). + let graph = parse_and_build(source_with_invocation()); + 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() { + // get_topic_action_defs should return the action definitions declared in a topic's + // actions block (one in this case: do_something). + let graph = parse_and_build(source_with_invocation()); + let defs = graph.get_topic_action_defs("main"); + assert_eq!(defs.len(), 1, "Expected 1 action def in topic main"); + } } diff --git a/tests/test_serializer_roundtrip.rs b/tests/test_serializer_roundtrip.rs index cc1c831..e3978db 100644 --- a/tests/test_serializer_roundtrip.rs +++ b/tests/test_serializer_roundtrip.rs @@ -216,3 +216,95 @@ topic main: assert!(topic.before_reasoning.is_some(), "before_reasoning lost after roundtrip"); assert!(topic.after_reasoning.is_some(), "after_reasoning lost after roundtrip"); } + +#[test] +fn test_roundtrip_system_block_with_messages() { + // Covers the top-level `system:` block containing both `instructions:` and + // `messages:` (welcome + error). The serializer writes these fields but no + // roundtrip test existed — this ensures they survive parse → serialize → parse. + let original = r#"config: + agent_name: "SupportAgent" + +system: + instructions: "You are a helpful support agent." + + messages: + welcome: "Hello! How can I help you today?" + error: "I had trouble processing your request. Please try again." + +topic main: + description: "Main" +"#; + + let ast = parse(original).expect("Failed to parse original"); + let serialized = serialize(&ast); + + assert!(serialized.contains("system:"), "Missing system block in serialized output"); + assert!(serialized.contains("messages:"), "Missing messages block in serialized output"); + + let reparsed = parse(&serialized).expect("Failed to reparse serialized"); + assert!(reparsed.system.is_some(), "system block lost after roundtrip"); + let system = &reparsed.system.as_ref().unwrap().node; + assert!(system.instructions.is_some(), "instructions lost after roundtrip"); + let messages = system.messages.as_ref().expect("messages block lost after roundtrip"); + assert!(messages.node.welcome.is_some(), "welcome message lost after roundtrip"); + assert!(messages.node.error.is_some(), "error message lost after roundtrip"); +} + +#[test] +fn test_roundtrip_action_with_various_param_types() { + // Covers action defs with non-string parameter types: `id`, `boolean`, `number`. + // These types are serialized differently from `string` but no roundtrip test + // existed for them. + let original = r#"config: + agent_name: "TypedAgent" + +topic main: + description: "Main" + actions: + lookup_record: + description: "Look up a Salesforce record" + inputs: + record_id: id + description: "Salesforce record ID" + is_required: True + include_details: boolean + description: "Whether to include full details" + is_required: False + max_results: number + description: "Maximum number of results to return" + outputs: + found: boolean + description: "Whether the record was found" + score: number + description: "Confidence score" + target: "flow://LookupRecord" + reasoning: + instructions: "Help the user look up records" +"#; + + let ast = parse(original).expect("Failed to parse original"); + let serialized = serialize(&ast); + + assert!(serialized.contains("lookup_record:"), "Missing action def in serialized output"); + assert!(serialized.contains(": id"), "Missing id param type"); + assert!(serialized.contains(": boolean"), "Missing boolean param type"); + assert!(serialized.contains(": number"), "Missing number param type"); + + let reparsed = parse(&serialized).expect("Failed to reparse serialized"); + assert_eq!(reparsed.topics.len(), 1); + let topic = &reparsed.topics[0].node; + let actions = topic.actions.as_ref().expect("Missing actions block after roundtrip"); + assert_eq!(actions.node.actions.len(), 1, "Expected 1 action def"); + let action = &actions.node.actions[0].node; + assert_eq!( + action.inputs.as_ref().expect("inputs missing").node.len(), + 3, + "Expected 3 inputs" + ); + assert_eq!( + action.outputs.as_ref().expect("outputs missing").node.len(), + 2, + "Expected 2 outputs" + ); +}