fix(protocol): reject duplicate command/args keys in wait-for block

A second `command` node silently overwrote the first via a scalar
Option<PathBuf>, 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<Vec<String>> and
error immediately on a repeated command or args key.
This commit is contained in:
2026-08-07 15:41:48 +02:00
parent 7631eefc67
commit 4c34e336a2
+65 -8
View File
@@ -267,7 +267,7 @@ fn parse_wait_for(doc: &KdlDocument, path: &Path) -> Result<Option<WaitForConfig
let mut conditions: Vec<WaitCondition> = Vec::new();
let mut command: Option<PathBuf> = None;
let mut args: Vec<String> = Vec::new();
let mut args: Option<Vec<String>> = 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<Option<WaitForConfig
conditions.push(WaitCondition::Tcp(addr));
}
"command" => {
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<Option<WaitForConfig
}
if let Some(command) = command {
conditions.push(WaitCondition::Command { command, args });
} else if !args.is_empty() {
conditions.push(WaitCondition::Command {
command,
args: args.unwrap_or_default(),
});
} else if args.is_some() {
return Err(invalid("`args` requires `command`".into()));
}
@@ -670,4 +683,48 @@ stop {
}
));
}
#[test]
fn duplicate_command_key_fails() {
let text =
"command \"/bin/x\"\nport 1\nwait-for { command \"/bin/a\"\ncommand \"/bin/b\" }";
let err = parse_server_config("foo", text, p()).unwrap_err();
assert!(matches!(
err,
ConfigError::InvalidValue {
field: "wait-for",
..
}
));
}
#[test]
fn duplicate_args_key_fails() {
let text =
"command \"/bin/x\"\nport 1\nwait-for { command \"docker\"\nargs \"a\"\nargs \"b\" }";
let err = parse_server_config("foo", text, p()).unwrap_err();
assert!(matches!(
err,
ConfigError::InvalidValue {
field: "wait-for",
..
}
));
}
#[test]
fn expand_tilde_alone_is_left_unchanged() {
let home = PathBuf::from("/home/someone");
assert_eq!(expand_tilde("~", Some(&home)), PathBuf::from("~"));
}
#[test]
fn expand_tilde_of_empty_string_is_left_unchanged() {
let home = PathBuf::from("/home/someone");
assert_eq!(expand_tilde("", Some(&home)), PathBuf::from(""));
}
}