From a60e845d8cb754670e7efc0275b2ba94c9afba54 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Sat, 28 Mar 2026 04:08:22 +0000 Subject: [PATCH] test: add coverage for parser error paths and expression edge cases --- src/parser/tests.rs | 94 +++++++++++++++++++++++++ tests/test_serializer_roundtrip.rs | 108 +++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) diff --git a/src/parser/tests.rs b/src/parser/tests.rs index 4b90a57..500627e 100644 --- a/src/parser/tests.rs +++ b/src/parser/tests.rs @@ -1000,3 +1000,97 @@ topic claims_processing: let file = result.unwrap(); assert_eq!(file.topics.len(), 2, "Should have 2 topics"); } + +// ============================================================================ +// Parser Error Path Tests +// ============================================================================ + +#[test] +fn test_parse_error_non_string_agent_name() { + // `agent_name` requires a quoted string literal. Providing a bare identifier + // must cause a parse error — `parse()` must return `Err`, not `Ok`. + let source = r#"config: + agent_name: NotAString +"#; + let result = parse(source); + assert!( + result.is_err(), + "Expected parse error for non-string agent_name, but parse succeeded" + ); +} + +#[test] +fn test_parse_error_number_literal_as_agent_name() { + // agent_name requires a string literal. A numeric literal is a different token + // type (NumberLit), so the parser must report an error. + let source = "config:\n agent_name: 42\n"; + let result = parse(source); + assert!( + result.is_err(), + "Expected parse error for numeric agent_name, but parse succeeded" + ); +} + +// ============================================================================ +// Expression Edge-Case Tests +// ============================================================================ + +#[test] +fn test_parse_unary_negation_in_variable_default() { + // Unary negation (`-`) in a variable default value exercises the `UnaryOp::Neg` + // branch of the expression parser, which has no other isolated test coverage. + let source = r#"config: + agent_name: "NegTest" + +variables: + offset: mutable number = -1 + description: "A negative offset" + +topic main: + description: "Main" +"#; + let result = parse(source); + assert!( + result.is_ok(), + "Expected successful parse with unary negation default: {:?}", + result.err() + ); + let file = result.unwrap(); + let vars = file.variables.expect("Expected variables block"); + assert_eq!(vars.node.variables.len(), 1, "Expected one variable"); + let var = &vars.node.variables[0].node; + assert_eq!(var.name.node, "offset"); + // The default must be present (value = -1, represented as UnaryOp::Neg) + assert!(var.default.is_some(), "Expected a default value for 'offset'"); +} + +#[test] +fn test_parse_ternary_expression_in_set_statement() { + // Ternary expression (`then_val if condition else else_val`) in a + // `before_reasoning:` set statement. The `Expr::Ternary` variant is defined and + // serialized but had no test coverage for parsing. + let source = r#"config: + agent_name: "TernaryTest" + +variables: + flag: mutable boolean = True + label: mutable string = "" + +topic main: + description: "Main" + before_reasoning: + set @variables.label = "active" if @variables.flag else "inactive" + reasoning: + instructions: "Help the user." +"#; + let result = parse(source); + assert!( + result.is_ok(), + "Expected successful parse with ternary expression in set: {:?}", + result.err() + ); + let file = result.unwrap(); + assert_eq!(file.topics.len(), 1); + let topic = &file.topics[0].node; + assert!(topic.before_reasoning.is_some(), "Expected before_reasoning block"); +} diff --git a/tests/test_serializer_roundtrip.rs b/tests/test_serializer_roundtrip.rs index cc1c831..e157cfd 100644 --- a/tests/test_serializer_roundtrip.rs +++ b/tests/test_serializer_roundtrip.rs @@ -216,3 +216,111 @@ 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 `system:` block containing both `messages:` (welcome + error) and + // `instructions:`. No prior roundtrip test exercised the messages sub-block. + let original = r#"config: + agent_name: "MsgAgent" + +system: + instructions: "You are a helpful assistant." + messages: + welcome: "Hello! How can I help you today?" + error: "Something went wrong. 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 sub-block"); + assert!(serialized.contains("welcome:"), "Missing welcome entry"); + assert!(serialized.contains("error:"), "Missing error entry"); + + let reparsed = parse(&serialized).expect("Failed to reparse serialized"); + let sys = reparsed.system.as_ref().expect("system block lost after roundtrip"); + assert!(sys.node.instructions.is_some(), "system instructions lost after roundtrip"); + let msgs = sys.node.messages.as_ref().expect("messages block lost after roundtrip"); + assert!(msgs.node.welcome.is_some(), "welcome message lost after roundtrip"); + assert!(msgs.node.error.is_some(), "error message lost after roundtrip"); +} + +#[test] +fn test_roundtrip_linked_variable() { + // Covers a `linked` variable declaration with a `source:` reference binding. + // Previous roundtrip tests only covered `mutable` variables. + use busbar_sf_agentscript::ast::VariableKind; + + let original = r#"config: + agent_name: "LinkedAgent" + +variables: + user_email: linked string + description: "Email of the logged-in user" + 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"); + + let reparsed = parse(&serialized).expect("Failed to reparse serialized"); + let vars = reparsed.variables.as_ref().expect("variables block lost after roundtrip"); + assert_eq!(vars.node.variables.len(), 1, "Expected one variable after roundtrip"); + let var = &vars.node.variables[0].node; + assert_eq!(var.name.node, "user_email"); + assert!( + matches!(var.kind, VariableKind::Linked), + "Variable kind changed from Linked after roundtrip" + ); + assert!(var.source.is_some(), "source reference lost after roundtrip"); +} + +#[test] +fn test_roundtrip_topic_with_system_override() { + // Covers a `topic` block that contains a `system:` override block. The override + // sets topic-scoped instructions that differ from the global system block. + // No prior roundtrip test exercised the per-topic system override. + let original = r#"config: + agent_name: "SysOverrideAgent" + +topic support: + description: "Support topic" + + system: + instructions: "You are now in support mode. Focus on resolving customer issues quickly." + + reasoning: + instructions: "Help the user resolve their issue." +"#; + + let ast = parse(original).expect("Failed to parse original"); + let serialized = serialize(&ast); + + assert!( + serialized.contains("system:"), + "Missing system override in serialized output" + ); + assert!( + serialized.contains("support mode"), + "Missing system override instruction text" + ); + + let reparsed = parse(&serialized).expect("Failed to reparse serialized"); + assert_eq!(reparsed.topics.len(), 1); + let topic = &reparsed.topics[0].node; + assert!(topic.system.is_some(), "topic system override lost after roundtrip"); + assert!( + topic.system.as_ref().unwrap().node.instructions.is_some(), + "system override instructions lost after roundtrip" + ); +}