diff options
| author | akbash <akbash@herdr.dev> | 2026-07-19 23:07:10 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-19 23:07:10 +0300 |
| commit | f76f895efe5b9f19aa9db909354c6c0f2195630b (patch) | |
| tree | a10a8af928f400e096f18eccf1cd13f57e41dd16 /src/app/api/plugins | |
| parent | 08409d167deba8950495d9737fb54b0cc6634f4c (diff) | |
fix: preserve plugin command arguments (#1613)
Diffstat (limited to 'src/app/api/plugins')
| -rw-r--r-- | src/app/api/plugins/manifest.rs | 13 | ||||
| -rw-r--r-- | src/app/api/plugins/mod.rs | 95 |
2 files changed, 103 insertions, 5 deletions
diff --git a/src/app/api/plugins/manifest.rs b/src/app/api/plugins/manifest.rs index c2f17704..645748e0 100644 --- a/src/app/api/plugins/manifest.rs +++ b/src/app/api/plugins/manifest.rs @@ -163,7 +163,14 @@ pub(crate) fn load_plugin_manifest( .into_iter() .map(normalize_manifest_event) .collect::<Result<Vec<_>, _>>()?; - events.sort_by(|a, b| a.on.cmp(&b.on).then_with(|| a.command.cmp(&b.command))); + events.sort_by(|a, b| { + a.on.cmp(&b.on).then_with(|| { + a.command + .iter() + .map(|arg| arg.trim()) + .cmp(b.command.iter().map(|arg| arg.trim())) + }) + }); let mut panes = raw .panes .into_iter() @@ -534,10 +541,6 @@ fn platform_name(p: PluginPlatform) -> &'static str { } fn normalize_command(command: Vec<String>) -> Result<Vec<String>, (&'static str, String)> { - let command = command - .into_iter() - .map(|arg| arg.trim().to_string()) - .collect::<Vec<_>>(); if command.is_empty() || command.iter().any(|arg| arg.is_empty()) { return Err(( "invalid_plugin_command", diff --git a/src/app/api/plugins/mod.rs b/src/app/api/plugins/mod.rs index 164d566e..83a6065e 100644 --- a/src/app/api/plugins/mod.rs +++ b/src/app/api/plugins/mod.rs @@ -938,6 +938,101 @@ platforms = ["linux", "macos", "windows"] } #[test] + fn plugin_manifest_preserves_whitespace_only_command_arguments() { + let root = unique_temp_path("plugin-whitespace-argv"); + write_manifest_content( + &root, + r#" +id = "example.whitespace-argv" +name = "Whitespace argv" +version = "0.1.0" +min_herdr_version = "0.7.0" +platforms = ["linux", "macos"] + +[[panes]] +id = "cut" +title = "Cut on tab" +command = ["awk", "-F", "\t", " {print $1} "] +"#, + ); + + let plugin = load_plugin_manifest(&root.display().to_string(), true) + .expect("literal whitespace argv elements should be valid"); + assert_eq!(plugin.panes[0].command, ["awk", "-F", "\t", " {print $1} "]); + + let _ = std::fs::remove_dir_all(root); + } + + #[test] + fn plugin_manifest_rejects_empty_command_elements() { + for (name, command) in [("array", "[]"), ("element", r#"["echo", ""]"#)] { + let root = unique_temp_path(&format!("plugin-empty-command-{name}")); + write_manifest_content( + &root, + &format!( + r#" +id = "example.empty-command-{name}" +name = "Empty command {name}" +version = "0.1.0" +min_herdr_version = "0.7.0" +platforms = ["linux", "macos"] + +[[panes]] +id = "empty" +title = "Empty command" +command = {command} +"# + ), + ); + + let result = load_plugin_manifest(&root.display().to_string(), true); + assert!(matches!(result, Err(("invalid_plugin_command", _)))); + + let _ = std::fs::remove_dir_all(root); + } + } + + #[test] + fn plugin_manifest_preserves_legacy_event_order_with_exact_argv() { + let root = unique_temp_path("plugin-event-whitespace-order"); + write_manifest_content( + &root, + r#" +id = "example.event-whitespace-order" +name = "Event whitespace order" +version = "0.1.0" +min_herdr_version = "0.7.0" +platforms = ["linux", "macos"] + +[[events]] +on = "workspace.created" +command = ["echo", "!"] + +[[events]] +on = "workspace.created" +command = ["echo", " a"] + +[[events]] +on = "workspace.created" +command = ["echo", "a ", "first"] + +[[events]] +on = "workspace.created" +command = ["echo", " a", "first "] +"#, + ); + + let plugin = load_plugin_manifest(&root.display().to_string(), true) + .expect("event commands with whitespace should load"); + assert_eq!(plugin.events[0].command, ["echo", "!"]); + assert_eq!(plugin.events[1].command, ["echo", " a"]); + assert_eq!(plugin.events[2].command, ["echo", "a ", "first"]); + assert_eq!(plugin.events[3].command, ["echo", " a", "first "]); + + let _ = std::fs::remove_dir_all(root); + } + + #[test] fn plugin_link_rejects_invalid_github_source_path() { let mut app = test_app(); let root = unique_temp_path("plugin-invalid-source"); |
