From 1a48ef8844c2d4f90569b2c75719bd2a91a98dfc Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 1 Aug 2026 17:36:51 -0500 Subject: [PATCH 1/2] fix(cli): accept --flag=value and reordered options in pane read/wait-output --- src/cli.rs | 59 +++++++ src/cli/pane.rs | 372 +++++++++++++++++++++++++++++++++++++-------- tests/cli/panes.rs | 31 ++++ 3 files changed, 397 insertions(+), 65 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 4d3c67c079..a0e3a93916 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -913,6 +913,25 @@ pub(super) fn parse_u64_flag(flag: &str, value: &str) -> std::io::Result { .map_err(|_| std::io::Error::other(format!("invalid value for {flag}: {value}"))) } +/// Expand `--flag=value` tokens into separate `--flag` and `value` tokens so +/// the hand-rolled subcommand parsers accept the same `--flag=value` form the +/// clap-generated help and completions imply. Only `value_options` are split: +/// boolean and unknown options keep their attached value so they still reach +/// the parser's unknown-option branch. +pub(super) fn expand_equals_args(args: &[String], value_options: &[&str]) -> Vec { + let mut expanded = Vec::with_capacity(args.len()); + for arg in args { + match arg.split_once('=') { + Some((flag, value)) if value_options.contains(&flag) => { + expanded.push(flag.to_string()); + expanded.push(value.to_string()); + } + _ => expanded.push(arg.clone()), + } + } + expanded +} + fn parse_session_json_only(args: &[String], usage: &str) -> Result { match args { [] => Ok(false), @@ -1121,4 +1140,44 @@ mod tests { ); assert!(!super::server_not_running::was_reported(&mapped)); } + + #[test] + fn expand_equals_args_splits_long_option_values() { + let args = vec![ + "w1:p1".to_string(), + "--source=recent".to_string(), + "--lines=5".to_string(), + "--raw".to_string(), + ]; + assert_eq!( + super::expand_equals_args(&args, &["--source", "--lines", "--format"]), + vec!["w1:p1", "--source", "recent", "--lines", "5", "--raw"] + ); + } + + #[test] + fn expand_equals_args_leaves_other_tokens_alone() { + let args = vec![ + "--match=a=b".to_string(), + "name=value".to_string(), + "--flag".to_string(), + ]; + assert_eq!( + super::expand_equals_args(&args, &["--match"]), + vec!["--match", "a=b", "name=value", "--flag"] + ); + } + + #[test] + fn expand_equals_args_keeps_values_on_boolean_and_unknown_options() { + let args = vec![ + "--raw=value".to_string(), + "--ansi=value".to_string(), + "--bogus=value".to_string(), + ]; + assert_eq!( + super::expand_equals_args(&args, &["--source", "--lines", "--format"]), + vec!["--raw=value", "--ansi=value", "--bogus=value"] + ); + } } diff --git a/src/cli/pane.rs b/src/cli/pane.rs index 7c12e0f956..791feb0921 100644 --- a/src/cli/pane.rs +++ b/src/cli/pane.rs @@ -441,42 +441,55 @@ fn pane_rename(args: &[String]) -> std::io::Result { } fn pane_read(args: &[String]) -> std::io::Result { - let Some(raw_pane_id) = args.first() else { - eprintln!("usage: herdr pane read [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]"); - return Ok(2); + let params = match parse_pane_read_args(args) { + Ok(params) => params, + Err(message) => { + eprintln!("{message}"); + return Ok(2); + } }; - let pane_id = super::normalize_pane_id(raw_pane_id); + let response = super::send_request(&Request { + id: "cli:pane:read".into(), + method: Method::PaneRead(params), + })?; + + super::print_read_response(&response) +} + +fn parse_pane_read_args(args: &[String]) -> Result { + const USAGE: &str = "usage: herdr pane read [--source visible|recent|recent-unwrapped|detection] [--lines N] [--format text|ansi] [--ansi] [--raw]"; + + let args = super::expand_equals_args(args, &["--source", "--lines", "--format"]); + let mut pane_id = None; let mut source = ReadSource::Recent; let mut lines = None; let mut format = ReadFormat::Text; let mut strip_ansi = true; - let mut index = 1; + let mut index = 0; while index < args.len() { match args[index].as_str() { "--source" => { let Some(value) = args.get(index + 1) else { - eprintln!("missing value for --source"); - return Ok(2); + return Err("missing value for --source".into()); }; - source = super::parse_read_source(value)?; + source = super::parse_read_source(value).map_err(|err| err.to_string())?; index += 2; } "--lines" => { let Some(value) = args.get(index + 1) else { - eprintln!("missing value for --lines"); - return Ok(2); + return Err("missing value for --lines".into()); }; - lines = Some(super::parse_u32_flag("--lines", value)?); + lines = + Some(super::parse_u32_flag("--lines", value).map_err(|err| err.to_string())?); index += 2; } "--format" => { let Some(value) = args.get(index + 1) else { - eprintln!("missing value for --format"); - return Ok(2); + return Err("missing value for --format".into()); }; - format = super::parse_read_format(value)?; + format = super::parse_read_format(value).map_err(|err| err.to_string())?; index += 2; } "--ansi" => { @@ -488,26 +501,31 @@ fn pane_read(args: &[String]) -> std::io::Result { strip_ansi = false; index += 1; } - other => { - eprintln!("unknown option: {other}"); - return Ok(2); + option if option.starts_with('-') => { + return Err(format!("unknown option: {option}")); + } + positional => { + if pane_id.is_some() { + return Err(format!("unexpected argument: {positional}")); + } + pane_id = Some(super::normalize_pane_id(positional)); + index += 1; } } } - let response = super::send_request(&Request { - id: "cli:pane:read".into(), - method: Method::PaneRead(PaneReadParams { - pane_id, - source, - lines, - format, - strip_ansi, - intent: crate::api::schema::ReadIntent::Interactive, - }), - })?; + let Some(pane_id) = pane_id else { + return Err(USAGE.into()); + }; - super::print_read_response(&response) + Ok(PaneReadParams { + pane_id, + source, + lines, + format, + strip_ansi, + intent: crate::api::schema::ReadIntent::Interactive, + }) } fn pane_split(args: &[String]) -> std::io::Result { @@ -936,28 +954,42 @@ fn pane_run(args: &[String]) -> std::io::Result { } fn pane_wait_output(args: &[String]) -> std::io::Result { - let Some(raw_pane_id) = args.first() else { - eprintln!("usage: herdr pane wait-output (--match TEXT | --regex PATTERN) [--source visible|recent|recent-unwrapped] [--lines N] [--timeout MS] [--raw]"); - return Ok(2); + let params = match parse_pane_wait_output_args(args) { + Ok(params) => params, + Err(message) => { + eprintln!("{message}"); + return Ok(2); + } }; - let pane_id = super::normalize_pane_id(raw_pane_id); + + super::print_response(&super::send_request(&Request { + id: "cli:pane:wait-output".into(), + method: Method::PaneWaitForOutput(params), + })?) +} + +fn parse_pane_wait_output_args(args: &[String]) -> Result { + const USAGE: &str = "usage: herdr pane wait-output (--match TEXT | --regex PATTERN) [--source visible|recent|recent-unwrapped] [--lines N] [--timeout MS] [--raw]"; + + let args = super::expand_equals_args( + args, + &["--match", "--regex", "--source", "--lines", "--timeout"], + ); + let mut pane_id = None; let mut source = ReadSource::Recent; let mut lines = None; let mut timeout_ms = None; let mut strip_ansi = true; let mut matcher = None; - let mut index = 1; + let mut index = 0; while index < args.len() { match args[index].as_str() { - "--match" | "--regex" => { - let option = args[index].as_str(); + option @ ("--match" | "--regex") => { let Some(value) = args.get(index + 1) else { - eprintln!("missing value for {option}"); - return Ok(2); + return Err(format!("missing value for {option}")); }; if matcher.is_some() { - eprintln!("--match and --regex are mutually exclusive"); - return Ok(2); + return Err("--match and --regex are mutually exclusive".into()); } matcher = Some(if option == "--regex" { OutputMatch::Regex { @@ -972,53 +1004,57 @@ fn pane_wait_output(args: &[String]) -> std::io::Result { } "--source" => { let Some(value) = args.get(index + 1) else { - eprintln!("missing value for --source"); - return Ok(2); + return Err("missing value for --source".into()); }; - source = super::parse_read_source(value)?; + source = super::parse_read_source(value).map_err(|err| err.to_string())?; index += 2; } "--lines" => { let Some(value) = args.get(index + 1) else { - eprintln!("missing value for --lines"); - return Ok(2); + return Err("missing value for --lines".into()); }; - lines = Some(super::parse_u32_flag("--lines", value)?); + lines = + Some(super::parse_u32_flag("--lines", value).map_err(|err| err.to_string())?); index += 2; } "--timeout" => { let Some(value) = args.get(index + 1) else { - eprintln!("missing value for --timeout"); - return Ok(2); + return Err("missing value for --timeout".into()); }; - timeout_ms = Some(super::parse_u64_flag("--timeout", value)?); + timeout_ms = + Some(super::parse_u64_flag("--timeout", value).map_err(|err| err.to_string())?); index += 2; } "--raw" => { strip_ansi = false; index += 1; } - other => { - eprintln!("unknown option: {other}"); - return Ok(2); + option if option.starts_with('-') => { + return Err(format!("unknown option: {option}")); + } + positional => { + if pane_id.is_some() { + return Err(format!("unexpected argument: {positional}")); + } + pane_id = Some(super::normalize_pane_id(positional)); + index += 1; } } } + let Some(pane_id) = pane_id else { + return Err(USAGE.into()); + }; let Some(matcher) = matcher else { - eprintln!("missing required --match or --regex"); - return Ok(2); + return Err("missing required --match or --regex".into()); }; - super::print_response(&super::send_request(&Request { - id: "cli:pane:wait-output".into(), - method: Method::PaneWaitForOutput(PaneWaitForOutputParams { - pane_id, - source, - lines, - r#match: matcher, - timeout_ms, - strip_ansi, - }), - })?) + Ok(PaneWaitForOutputParams { + pane_id, + source, + lines, + r#match: matcher, + timeout_ms, + strip_ansi, + }) } fn pane_report_agent(args: &[String]) -> std::io::Result { @@ -1765,4 +1801,210 @@ mod tests { assert_eq!(params.direction, PaneDirection::Left); assert_eq!(params.amount, Some(0.125)); } + + #[test] + fn parse_pane_read_args_defaults_with_bare_pane_id() { + let params = parse_pane_read_args(&args(&["issue-1"])).unwrap(); + + assert_eq!(params.pane_id, "issue-1"); + assert_eq!(params.source, ReadSource::Recent); + assert_eq!(params.lines, None); + assert_eq!(params.format, ReadFormat::Text); + assert!(params.strip_ansi); + } + + #[test] + fn parse_pane_read_args_accepts_space_separated_options() { + let params = parse_pane_read_args(&args(&[ + "issue-1", "--source", "visible", "--lines", "5", "--ansi", + ])) + .unwrap(); + + assert_eq!(params.pane_id, "issue-1"); + assert_eq!(params.source, ReadSource::Visible); + assert_eq!(params.lines, Some(5)); + assert_eq!(params.format, ReadFormat::Ansi); + } + + #[test] + fn parse_pane_read_args_accepts_equals_options() { + let params = + parse_pane_read_args(&args(&["issue-1", "--source=recent", "--lines=5"])).unwrap(); + + assert_eq!(params.pane_id, "issue-1"); + assert_eq!(params.source, ReadSource::Recent); + assert_eq!(params.lines, Some(5)); + } + + #[test] + fn parse_pane_read_args_accepts_options_before_pane_id() { + let params = + parse_pane_read_args(&args(&["--source", "visible", "--lines", "5", "issue-1"])) + .unwrap(); + + assert_eq!(params.pane_id, "issue-1"); + assert_eq!(params.source, ReadSource::Visible); + assert_eq!(params.lines, Some(5)); + } + + #[test] + fn parse_pane_read_args_requires_pane_id() { + let err = parse_pane_read_args(&args(&[])).unwrap_err(); + assert!(err.contains("usage: herdr pane read")); + + let err = parse_pane_read_args(&args(&["--source", "recent"])).unwrap_err(); + assert!(err.contains("usage: herdr pane read")); + } + + #[test] + fn parse_pane_read_args_rejects_unknown_option() { + let err = parse_pane_read_args(&args(&["issue-1", "--bogus"])).unwrap_err(); + assert_eq!(err, "unknown option: --bogus"); + + let err = parse_pane_read_args(&args(&["issue-1", "--bogus=1"])).unwrap_err(); + assert_eq!(err, "unknown option: --bogus=1"); + } + + #[test] + fn parse_pane_read_args_rejects_values_on_boolean_options() { + let err = parse_pane_read_args(&args(&["--raw=issue-1"])).unwrap_err(); + assert_eq!(err, "unknown option: --raw=issue-1"); + + let err = parse_pane_read_args(&args(&["--ansi=issue-1"])).unwrap_err(); + assert_eq!(err, "unknown option: --ansi=issue-1"); + } + + #[test] + fn parse_pane_read_args_rejects_invalid_option_values() { + let err = parse_pane_read_args(&args(&["issue-1", "--source", "bogus"])).unwrap_err(); + assert_eq!(err, "invalid read source: bogus"); + + let err = parse_pane_read_args(&args(&["issue-1", "--format", "bogus"])).unwrap_err(); + assert_eq!(err, "invalid read format: bogus"); + + let err = parse_pane_read_args(&args(&["issue-1", "--lines", "nope"])).unwrap_err(); + assert_eq!(err, "invalid value for --lines: nope"); + + let err = parse_pane_read_args(&args(&["issue-1", "--source=bogus"])).unwrap_err(); + assert_eq!(err, "invalid read source: bogus"); + } + + #[test] + fn parse_pane_read_args_rejects_extra_positional() { + let err = parse_pane_read_args(&args(&["issue-1", "issue-2"])).unwrap_err(); + assert_eq!(err, "unexpected argument: issue-2"); + } + + #[test] + fn parse_pane_read_args_rejects_missing_option_value() { + let err = parse_pane_read_args(&args(&["issue-1", "--lines"])).unwrap_err(); + assert_eq!(err, "missing value for --lines"); + } + + #[test] + fn parse_pane_wait_output_args_accepts_space_separated_options() { + let params = parse_pane_wait_output_args(&args(&[ + "issue-1", + "--match", + "ready", + "--timeout", + "5000", + ])) + .unwrap(); + + assert_eq!(params.pane_id, "issue-1"); + assert_eq!( + params.r#match, + OutputMatch::Substring { + value: "ready".into() + } + ); + assert_eq!(params.timeout_ms, Some(5000)); + assert_eq!(params.source, ReadSource::Recent); + assert!(params.strip_ansi); + } + + #[test] + fn parse_pane_wait_output_args_accepts_equals_options() { + let params = + parse_pane_wait_output_args(&args(&["issue-1", "--match=ready", "--timeout=5000"])) + .unwrap(); + + assert_eq!(params.pane_id, "issue-1"); + assert_eq!( + params.r#match, + OutputMatch::Substring { + value: "ready".into() + } + ); + assert_eq!(params.timeout_ms, Some(5000)); + } + + #[test] + fn parse_pane_wait_output_args_accepts_options_before_pane_id() { + let params = parse_pane_wait_output_args(&args(&[ + "--regex", + "rea+y", + "--timeout", + "100", + "issue-1", + ])) + .unwrap(); + + assert_eq!(params.pane_id, "issue-1"); + assert_eq!( + params.r#match, + OutputMatch::Regex { + value: "rea+y".into() + } + ); + assert_eq!(params.timeout_ms, Some(100)); + } + + #[test] + fn parse_pane_wait_output_args_rejects_positional_marker() { + let err = parse_pane_wait_output_args(&args(&["issue-1", "ready", "--timeout", "500"])) + .unwrap_err(); + assert_eq!(err, "unexpected argument: ready"); + } + + #[test] + fn parse_pane_wait_output_args_requires_matcher() { + let err = parse_pane_wait_output_args(&args(&["issue-1"])).unwrap_err(); + assert_eq!(err, "missing required --match or --regex"); + } + + #[test] + fn parse_pane_wait_output_args_rejects_conflicting_matchers() { + let err = parse_pane_wait_output_args(&args(&["issue-1", "--match", "a", "--regex", "b"])) + .unwrap_err(); + assert_eq!(err, "--match and --regex are mutually exclusive"); + } + + #[test] + fn parse_pane_wait_output_args_rejects_unknown_option() { + let err = parse_pane_wait_output_args(&args(&["issue-1", "--match", "a", "--bogus=1"])) + .unwrap_err(); + assert_eq!(err, "unknown option: --bogus=1"); + } + + #[test] + fn parse_pane_wait_output_args_rejects_values_on_boolean_options() { + let err = + parse_pane_wait_output_args(&args(&["--raw=issue-1", "--match", "ready"])).unwrap_err(); + assert_eq!(err, "unknown option: --raw=issue-1"); + } + + #[test] + fn parse_pane_wait_output_args_rejects_invalid_option_values() { + let err = + parse_pane_wait_output_args(&args(&["issue-1", "--match", "a", "--timeout", "nope"])) + .unwrap_err(); + assert_eq!(err, "invalid value for --timeout: nope"); + + let err = + parse_pane_wait_output_args(&args(&["issue-1", "--match", "a", "--source=bogus"])) + .unwrap_err(); + assert_eq!(err, "invalid read source: bogus"); + } } diff --git a/tests/cli/panes.rs b/tests/cli/panes.rs index 32110b365d..4e6bfb39dd 100644 --- a/tests/cli/panes.rs +++ b/tests/cli/panes.rs @@ -566,3 +566,34 @@ fn pane_shell_gets_herdr_socket_and_pane_env() { cleanup_spawned_herdr(herdr, base); } + +#[test] +fn pane_read_and_wait_output_reject_invalid_values_with_usage_errors() { + // Invalid option values fail as CLI usage errors before any server + // contact: exit 2 with the plain parser message, not the old + // `Error: Custom { ... }` io::Error wrapper from main. + let socket_path = Path::new("/tmp/herdr-cli-invalid-values-no-server.sock"); + + let read = run_cli(socket_path, &["pane", "read", "w1:p1", "--source", "bogus"]); + assert_eq!(read.status.code(), Some(2)); + let read_stderr = String::from_utf8_lossy(&read.stderr); + assert_eq!(read_stderr.trim(), "invalid read source: bogus"); + assert!(!read_stderr.contains("Error: Custom")); + + let wait = run_cli( + socket_path, + &[ + "pane", + "wait-output", + "w1:p1", + "--match", + "ready", + "--timeout", + "nope", + ], + ); + assert_eq!(wait.status.code(), Some(2)); + let wait_stderr = String::from_utf8_lossy(&wait.stderr); + assert_eq!(wait_stderr.trim(), "invalid value for --timeout: nope"); + assert!(!wait_stderr.contains("Error: Custom")); +} From 937e4f7bc161dcf88965be74c4be4423b7fdeeed Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Thu, 6 Aug 2026 03:03:04 +0300 Subject: [PATCH 2/2] test(cli): trim redundant pane parser coverage --- src/cli.rs | 43 +++++---------- src/cli/pane.rs | 134 +++------------------------------------------ tests/cli/panes.rs | 25 ++------- 3 files changed, 25 insertions(+), 177 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index a0e3a93916..6acbfcbe5e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1142,42 +1142,27 @@ mod tests { } #[test] - fn expand_equals_args_splits_long_option_values() { - let args = vec![ - "w1:p1".to_string(), - "--source=recent".to_string(), - "--lines=5".to_string(), - "--raw".to_string(), - ]; - assert_eq!( - super::expand_equals_args(&args, &["--source", "--lines", "--format"]), - vec!["w1:p1", "--source", "recent", "--lines", "5", "--raw"] - ); - } - - #[test] - fn expand_equals_args_leaves_other_tokens_alone() { + fn expand_equals_args_splits_value_options_only() { + // Known value options split; values may contain `=`. Boolean and + // unknown options keep the attached form so parsers still reject them. let args = vec![ "--match=a=b".to_string(), "name=value".to_string(), - "--flag".to_string(), - ]; - assert_eq!( - super::expand_equals_args(&args, &["--match"]), - vec!["--match", "a=b", "name=value", "--flag"] - ); - } - - #[test] - fn expand_equals_args_keeps_values_on_boolean_and_unknown_options() { - let args = vec![ "--raw=value".to_string(), - "--ansi=value".to_string(), "--bogus=value".to_string(), + "--timeout=5000".to_string(), ]; assert_eq!( - super::expand_equals_args(&args, &["--source", "--lines", "--format"]), - vec!["--raw=value", "--ansi=value", "--bogus=value"] + super::expand_equals_args(&args, &["--match", "--timeout"]), + vec![ + "--match", + "a=b", + "name=value", + "--raw=value", + "--bogus=value", + "--timeout", + "5000", + ] ); } } diff --git a/src/cli/pane.rs b/src/cli/pane.rs index 95b1fcc36b..5979fd9964 100644 --- a/src/cli/pane.rs +++ b/src/cli/pane.rs @@ -1861,80 +1861,15 @@ mod tests { } #[test] - fn parse_pane_read_args_accepts_equals_options() { + fn parse_pane_read_args_accepts_reordered_equals_options() { let params = - parse_pane_read_args(&args(&["issue-1", "--source=recent", "--lines=5"])).unwrap(); - - assert_eq!(params.pane_id, "issue-1"); - assert_eq!(params.source, ReadSource::Recent); - assert_eq!(params.lines, Some(5)); - } - - #[test] - fn parse_pane_read_args_accepts_options_before_pane_id() { - let params = - parse_pane_read_args(&args(&["--source", "visible", "--lines", "5", "issue-1"])) - .unwrap(); + parse_pane_read_args(&args(&["--source=visible", "--lines=5", "issue-1"])).unwrap(); assert_eq!(params.pane_id, "issue-1"); assert_eq!(params.source, ReadSource::Visible); assert_eq!(params.lines, Some(5)); } - #[test] - fn parse_pane_read_args_requires_pane_id() { - let err = parse_pane_read_args(&args(&[])).unwrap_err(); - assert!(err.contains("usage: herdr pane read")); - - let err = parse_pane_read_args(&args(&["--source", "recent"])).unwrap_err(); - assert!(err.contains("usage: herdr pane read")); - } - - #[test] - fn parse_pane_read_args_rejects_unknown_option() { - let err = parse_pane_read_args(&args(&["issue-1", "--bogus"])).unwrap_err(); - assert_eq!(err, "unknown option: --bogus"); - - let err = parse_pane_read_args(&args(&["issue-1", "--bogus=1"])).unwrap_err(); - assert_eq!(err, "unknown option: --bogus=1"); - } - - #[test] - fn parse_pane_read_args_rejects_values_on_boolean_options() { - let err = parse_pane_read_args(&args(&["--raw=issue-1"])).unwrap_err(); - assert_eq!(err, "unknown option: --raw=issue-1"); - - let err = parse_pane_read_args(&args(&["--ansi=issue-1"])).unwrap_err(); - assert_eq!(err, "unknown option: --ansi=issue-1"); - } - - #[test] - fn parse_pane_read_args_rejects_invalid_option_values() { - let err = parse_pane_read_args(&args(&["issue-1", "--source", "bogus"])).unwrap_err(); - assert_eq!(err, "invalid read source: bogus"); - - let err = parse_pane_read_args(&args(&["issue-1", "--format", "bogus"])).unwrap_err(); - assert_eq!(err, "invalid read format: bogus"); - - let err = parse_pane_read_args(&args(&["issue-1", "--lines", "nope"])).unwrap_err(); - assert_eq!(err, "invalid value for --lines: nope"); - - let err = parse_pane_read_args(&args(&["issue-1", "--source=bogus"])).unwrap_err(); - assert_eq!(err, "invalid read source: bogus"); - } - - #[test] - fn parse_pane_read_args_rejects_extra_positional() { - let err = parse_pane_read_args(&args(&["issue-1", "issue-2"])).unwrap_err(); - assert_eq!(err, "unexpected argument: issue-2"); - } - - #[test] - fn parse_pane_read_args_rejects_missing_option_value() { - let err = parse_pane_read_args(&args(&["issue-1", "--lines"])).unwrap_err(); - assert_eq!(err, "missing value for --lines"); - } - #[test] fn parse_pane_wait_output_args_accepts_space_separated_options() { let params = parse_pane_wait_output_args(&args(&[ @@ -1959,86 +1894,31 @@ mod tests { } #[test] - fn parse_pane_wait_output_args_accepts_equals_options() { + fn parse_pane_wait_output_args_accepts_reordered_equals_options() { let params = - parse_pane_wait_output_args(&args(&["issue-1", "--match=ready", "--timeout=5000"])) + parse_pane_wait_output_args(&args(&["--match=a=b", "--timeout=100", "issue-1"])) .unwrap(); assert_eq!(params.pane_id, "issue-1"); assert_eq!( params.r#match, OutputMatch::Substring { - value: "ready".into() - } - ); - assert_eq!(params.timeout_ms, Some(5000)); - } - - #[test] - fn parse_pane_wait_output_args_accepts_options_before_pane_id() { - let params = parse_pane_wait_output_args(&args(&[ - "--regex", - "rea+y", - "--timeout", - "100", - "issue-1", - ])) - .unwrap(); - - assert_eq!(params.pane_id, "issue-1"); - assert_eq!( - params.r#match, - OutputMatch::Regex { - value: "rea+y".into() + value: "a=b".into() } ); assert_eq!(params.timeout_ms, Some(100)); } - #[test] - fn parse_pane_wait_output_args_rejects_positional_marker() { - let err = parse_pane_wait_output_args(&args(&["issue-1", "ready", "--timeout", "500"])) - .unwrap_err(); - assert_eq!(err, "unexpected argument: ready"); - } - #[test] fn parse_pane_wait_output_args_requires_matcher() { let err = parse_pane_wait_output_args(&args(&["issue-1"])).unwrap_err(); - assert_eq!(err, "missing required --match or --regex"); + assert!(err.contains("missing required --match or --regex")); } #[test] fn parse_pane_wait_output_args_rejects_conflicting_matchers() { let err = parse_pane_wait_output_args(&args(&["issue-1", "--match", "a", "--regex", "b"])) .unwrap_err(); - assert_eq!(err, "--match and --regex are mutually exclusive"); - } - - #[test] - fn parse_pane_wait_output_args_rejects_unknown_option() { - let err = parse_pane_wait_output_args(&args(&["issue-1", "--match", "a", "--bogus=1"])) - .unwrap_err(); - assert_eq!(err, "unknown option: --bogus=1"); - } - - #[test] - fn parse_pane_wait_output_args_rejects_values_on_boolean_options() { - let err = - parse_pane_wait_output_args(&args(&["--raw=issue-1", "--match", "ready"])).unwrap_err(); - assert_eq!(err, "unknown option: --raw=issue-1"); - } - - #[test] - fn parse_pane_wait_output_args_rejects_invalid_option_values() { - let err = - parse_pane_wait_output_args(&args(&["issue-1", "--match", "a", "--timeout", "nope"])) - .unwrap_err(); - assert_eq!(err, "invalid value for --timeout: nope"); - - let err = - parse_pane_wait_output_args(&args(&["issue-1", "--match", "a", "--source=bogus"])) - .unwrap_err(); - assert_eq!(err, "invalid read source: bogus"); + assert!(err.contains("mutually exclusive")); } } diff --git a/tests/cli/panes.rs b/tests/cli/panes.rs index 4e6bfb39dd..fb6cd3dee1 100644 --- a/tests/cli/panes.rs +++ b/tests/cli/panes.rs @@ -568,7 +568,7 @@ fn pane_shell_gets_herdr_socket_and_pane_env() { } #[test] -fn pane_read_and_wait_output_reject_invalid_values_with_usage_errors() { +fn pane_read_rejects_invalid_value_with_usage_error() { // Invalid option values fail as CLI usage errors before any server // contact: exit 2 with the plain parser message, not the old // `Error: Custom { ... }` io::Error wrapper from main. @@ -576,24 +576,7 @@ fn pane_read_and_wait_output_reject_invalid_values_with_usage_errors() { let read = run_cli(socket_path, &["pane", "read", "w1:p1", "--source", "bogus"]); assert_eq!(read.status.code(), Some(2)); - let read_stderr = String::from_utf8_lossy(&read.stderr); - assert_eq!(read_stderr.trim(), "invalid read source: bogus"); - assert!(!read_stderr.contains("Error: Custom")); - - let wait = run_cli( - socket_path, - &[ - "pane", - "wait-output", - "w1:p1", - "--match", - "ready", - "--timeout", - "nope", - ], - ); - assert_eq!(wait.status.code(), Some(2)); - let wait_stderr = String::from_utf8_lossy(&wait.stderr); - assert_eq!(wait_stderr.trim(), "invalid value for --timeout: nope"); - assert!(!wait_stderr.contains("Error: Custom")); + let stderr = String::from_utf8_lossy(&read.stderr); + assert!(stderr.contains("invalid read source: bogus")); + assert!(!stderr.contains("Error: Custom")); }