Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions src/graph/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,144 @@ topic topic_b:
);
}

#[test]
fn test_find_variable_readers_via_with_clause() {
// A reasoning action that passes a variable via a `with` clause creates a
// RefEdge::Reads edge from the reasoning action to the variable.
// find_variable_readers() must return that reasoning action.
let source = r#"config:
agent_name: "Test"

variables:
order_id: mutable string = ""
description: "Order identifier"

topic main:
description: "Main"

actions:
get_order:
description: "Retrieve order details"
inputs:
id: string
description: "Order ID"
outputs:
status: string
description: "Order status"
target: "flow://GetOrder"

reasoning:
instructions: "Help"
actions:
fetch_order: @actions.get_order
description: "Fetch the order by ID"
with id = @variables.order_id
"#;
let graph = parse_and_build(source);

let var_idx = graph.get_variable("order_id").expect("order_id variable not found");
let action_idx = graph
.get_reasoning_action("main", "fetch_order")
.expect("fetch_order reasoning action not found");

let readers = graph.find_variable_readers(var_idx);
assert_eq!(readers.len(), 1, "Expected exactly 1 reader for order_id");
assert_eq!(
readers.nodes[0], action_idx,
"Expected fetch_order reasoning action to be the reader"
);
}

#[test]
fn test_find_variable_writers_via_set_clause() {
// A reasoning action that writes to a variable via a `set` clause creates a
// RefEdge::Writes edge from the reasoning action to the variable.
// find_variable_writers() must return that reasoning action.
let source = r#"config:
agent_name: "Test"

variables:
result_status: mutable string = ""
description: "Most-recent status value"

topic main:
description: "Main"

actions:
get_status:
description: "Retrieve status from backend"
outputs:
status: string
description: "Current status"
target: "flow://GetStatus"

reasoning:
instructions: "Help"
actions:
check_status: @actions.get_status
description: "Check status and store result"
set @variables.result_status = @outputs.status
"#;
let graph = parse_and_build(source);

let var_idx = graph.get_variable("result_status").expect("result_status variable not found");
let action_idx = graph
.get_reasoning_action("main", "check_status")
.expect("check_status reasoning action not found");

let writers = graph.find_variable_writers(var_idx);
assert_eq!(writers.len(), 1, "Expected exactly 1 writer for result_status");
assert_eq!(
writers.nodes[0], action_idx,
"Expected check_status reasoning action to be the writer"
);
}

#[test]
fn test_find_action_invokers_returns_reasoning_action() {
// A reasoning action whose target is `@actions.<name>` creates a
// RefEdge::Invokes edge from the reasoning action to the action definition.
// find_action_invokers() must return that reasoning action.
let source = r#"config:
agent_name: "Test"

topic main:
description: "Main"

actions:
lookup_data:
description: "Look up data from backend"
inputs:
id: string
description: "Record identifier"
outputs:
result: string
description: "Lookup result"
target: "flow://LookupData"

reasoning:
instructions: "Help"
actions:
do_lookup: @actions.lookup_data
description: "Perform the data lookup"
"#;
let graph = parse_and_build(source);

let action_def_idx = graph
.get_action_def("main", "lookup_data")
.expect("lookup_data action def not found");
let reasoning_idx = graph
.get_reasoning_action("main", "do_lookup")
.expect("do_lookup reasoning action not found");

let invokers = graph.find_action_invokers(action_def_idx);
assert_eq!(invokers.len(), 1, "Expected exactly 1 invoker for lookup_data");
assert_eq!(
invokers.nodes[0], reasoning_idx,
"Expected do_lookup reasoning action to be the invoker"
);
}

#[test]
fn test_stats_counts_nodes_correctly() {
// Verify that stats() correctly counts topics, action defs, and variables.
Expand Down
32 changes: 32 additions & 0 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,36 @@ topic main:
.collect();
assert!(tokens.iter().any(|t| matches!(t, lexer::Token::Config)));
}

#[test]
fn test_parse_none_default_on_non_boolean_variable_returns_error() {
// `= None` is only a valid default for `boolean` types. The parser's
// validate() callback emits a custom error when it appears on any other type
// (e.g. `integer`). This test verifies that parse_source returns Err and
// that at least one error message mentions None or the boolean type
// restriction — covering a previously untested parser error path.
let source = r#"config:
agent_name: "Test"

variables:
counter: mutable integer = None
description: "Counter — None is not a valid default for integer"

topic main:
description: "Main"
"#;
let result = parse_source(source);
assert!(
result.is_err(),
"Expected parse_source to return Err for '= None' on a non-boolean type, got Ok"
);
let errors = result.unwrap_err();
assert!(!errors.is_empty(), "Expected at least one error message");
// The validate() callback formats a message that includes "None" and "boolean"
assert!(
errors.iter().any(|e| e.contains("None") || e.contains("boolean")),
"Expected error message referencing None or boolean type, got: {:?}",
errors
);
}
}
72 changes: 72 additions & 0 deletions tests/test_serializer_roundtrip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,78 @@ topic main:
assert_eq!(reparsed.language.as_ref().unwrap().node.entries.len(), 1);
}

#[test]
fn test_roundtrip_system_block_with_messages() {
// Covers the `system:` block with both a `messages:` sub-block (welcome + error)
// and a top-level `instructions:` entry. Ensures all three fields survive a
// parse → serialize → parse cycle.
let original = r#"config:
agent_name: "SystemAgent"

system:
messages:
welcome: "Welcome to our service!"
error: "Something went wrong. Please try again."
instructions: "You are a helpful assistant."

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("welcome"), "Missing welcome message key");
assert!(serialized.contains("error"), "Missing error message key");

let reparsed = parse(&serialized).expect("Failed to reparse serialized");
let system = reparsed.system.as_ref().expect("system block lost after roundtrip");
let messages = system.node.messages.as_ref().expect("messages sub-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");
assert!(system.node.instructions.is_some(), "instructions lost after roundtrip");
}

#[test]
fn test_roundtrip_linked_variable() {
// Covers the `linked` variable kind with a `source:` reference — the serializer
// handles it but no roundtrip test existed. Ensures the kind, type, and source
// survive a parse → serialize → parse cycle.
let original = r#"config:
agent_name: "LinkedVarAgent"

variables:
user_email: linked string
description: "User email injected from conversation context"
source: @messagingSession.userEmail

topic main:
description: "Main"
"#;

let ast = parse(original).expect("Failed to parse original");
let serialized = serialize(&ast);

assert!(serialized.contains("linked"), "Missing 'linked' keyword in serialized output");
assert!(serialized.contains("user_email"), "Missing variable name in serialized output");

let reparsed = parse(&serialized).expect("Failed to reparse serialized");
let vars = &reparsed.variables.as_ref().expect("variables block lost after roundtrip").node;
assert_eq!(vars.variables.len(), 1, "Expected exactly 1 variable after roundtrip");
let var = &vars.variables[0].node;
assert_eq!(
var.kind,
busbar_sf_agentscript::ast::VariableKind::Linked,
"Expected Linked variable kind after roundtrip"
);
assert_eq!(
var.ty.node,
busbar_sf_agentscript::Type::String,
"Expected String type after roundtrip"
);
}

#[test]
fn test_roundtrip_before_and_after_reasoning() {
// Covers `before_reasoning:` and `after_reasoning:` directive blocks inside a topic.
Expand Down