From 4c34e336a2c0ad84d80822e15ab757021cf2791e Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Fri, 7 Aug 2026 15:41:38 +0200 Subject: [PATCH] fix(protocol): reject duplicate command/args keys in wait-for block A second `command` node silently overwrote the first via a scalar Option, and a second `args` node silently overwrote the first via a plain Vec, so duplicate keys last-won instead of tripping the exactly-one-condition rule. Track args as Option> and error immediately on a repeated command or args key. --- crates/xy-protocol/src/kdl_parse.rs | 73 +++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/crates/xy-protocol/src/kdl_parse.rs b/crates/xy-protocol/src/kdl_parse.rs index 8f7ca96..82f3dae 100644 --- a/crates/xy-protocol/src/kdl_parse.rs +++ b/crates/xy-protocol/src/kdl_parse.rs @@ -267,7 +267,7 @@ fn parse_wait_for(doc: &KdlDocument, path: &Path) -> Result = Vec::new(); let mut command: Option = None; - let mut args: Vec = Vec::new(); + let mut args: Option> = None; let mut timeout = default_wait_timeout(); let mut interval = default_wait_interval(); @@ -284,14 +284,24 @@ fn parse_wait_for(doc: &KdlDocument, path: &Path) -> Result { + if command.is_some() { + return Err(invalid("duplicate key `command`".into())); + } + command = Some(PathBuf::from(string_arg(child, "wait-for", path)?)); } "args" => { - args = child - .entries() - .iter() - .filter_map(|e| e.value().as_string().map(str::to_string)) - .collect(); + if args.is_some() { + return Err(invalid("duplicate key `args`".into())); + } + + args = Some( + child + .entries() + .iter() + .filter_map(|e| e.value().as_string().map(str::to_string)) + .collect(), + ); } "timeout" => { timeout = parse_duration_arg(child, "wait-for", path)?; @@ -306,8 +316,11 @@ fn parse_wait_for(doc: &KdlDocument, path: &Path) -> Result