From 7a83cb11ed66d0e471a9f8b183c30a72e4b592ab Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 5 Mar 2026 00:05:11 -0500 Subject: [PATCH] Error on unknown $variable placeholders in DOT prompts Reuse expand_vars() from run_config to scan for $identifier patterns in codergen prompt expansion. Unknown variables like $gaol now produce an ArcError::Validation at runtime instead of silently passing through. Co-Authored-By: Claude Opus 4.6 --- crates/arc-workflows/src/handler/codergen.rs | 41 +++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/crates/arc-workflows/src/handler/codergen.rs b/crates/arc-workflows/src/handler/codergen.rs index e8b8d697a..e0e4bc805 100644 --- a/crates/arc-workflows/src/handler/codergen.rs +++ b/crates/arc-workflows/src/handler/codergen.rs @@ -63,9 +63,16 @@ impl CodergenHandler { } } -/// Expand `$goal` in text using the graph goal. -fn expand_variables(text: &str, graph: &Graph) -> String { - text.replace("$goal", graph.goal()) +/// Expand `$variable` placeholders in text using graph attributes. +/// +/// Known variables are built from graph attributes (e.g. `$goal`). Any +/// `$identifier` not in the map produces an error, catching typos like +/// `$gaol` at runtime. +fn expand_variables(text: &str, graph: &Graph) -> Result { + let mut vars = std::collections::HashMap::new(); + vars.insert("goal".to_string(), graph.goal().to_string()); + + crate::cli::run_config::expand_vars(text, &vars).map_err(|e| ArcError::Validation(e.to_string())) } /// Status fields that indicate a JSON object contains routing directives. @@ -203,7 +210,7 @@ impl Handler for CodergenHandler { .prompt() .filter(|p| !p.is_empty()) .unwrap_or_else(|| node.label()); - let expanded = expand_variables(raw_prompt, graph); + let expanded = expand_variables(raw_prompt, graph)?; let preamble = context.get_string("current.preamble", ""); let prompt = if preamble.is_empty() { expanded @@ -455,10 +462,34 @@ mod tests { "goal".to_string(), AttrValue::String("Fix bugs".to_string()), ); - let result = expand_variables("Goal is: $goal, do it", &graph); + let result = expand_variables("Goal is: $goal, do it", &graph).unwrap(); assert_eq!(result, "Goal is: Fix bugs, do it"); } + #[test] + fn expand_variables_errors_on_unknown_variable() { + let graph = Graph::new("test"); + let err = expand_variables("Do $foo now", &graph).unwrap_err(); + assert!( + err.to_string().contains("Undefined variable: $foo"), + "unexpected error: {err}" + ); + } + + #[test] + fn expand_variables_allows_bare_dollar() { + let graph = Graph::new("test"); + let result = expand_variables("costs $5", &graph).unwrap(); + assert_eq!(result, "costs $5"); + } + + #[test] + fn expand_variables_allows_dollar_alone() { + let graph = Graph::new("test"); + let result = expand_variables("just a $ sign", &graph).unwrap(); + assert_eq!(result, "just a $ sign"); + } + #[test] fn truncate_short_string() { assert_eq!(truncate("hello", 200), "hello");