diff --git a/src/discover/registry.rs b/src/discover/registry.rs index bb7b11f2a..780f2b6db 100644 --- a/src/discover/registry.rs +++ b/src/discover/registry.rs @@ -1011,6 +1011,67 @@ mod tests { ); } + // --- #1654: ssh classify + rewrite roundtrip --- + + #[test] + fn test_classify_ssh_with_remote_command() { + // Hook path must classify ssh-with-args as Supported so `rtk rewrite` + // returns the prefixed form instead of bailing with exit 1. + match classify_command("ssh user@host uptime") { + Classification::Supported { + rtk_equivalent, + category, + .. + } => { + assert_eq!(rtk_equivalent, "rtk ssh"); + assert_eq!(category, "Network"); + } + other => panic!("expected Supported, got {:?}", other), + } + } + + #[test] + fn test_rewrite_ssh_shapes() { + // Spot-check the four shapes from #1654's reproduction script. + let cases = [ + ("ssh root@host uptime", "rtk ssh root@host uptime"), + ("ssh user@example.test ls", "rtk ssh user@example.test ls"), + ( + "ssh -o ConnectTimeout=5 host echo OK", + "rtk ssh -o ConnectTimeout=5 host echo OK", + ), + ]; + for (input, expected) in cases { + assert_eq!( + rewrite_command_no_prefixes(input, &[]), + Some(expected.into()), + "rewrite for {:?} should be Some({:?})", + input, + expected + ); + } + } + + #[test] + fn test_rewrite_ssh_with_host_only() { + // `ssh host` (no remote command, just login) is the fourth shape + // from #1654's reproduction script. It still needs to flow through + // the filter — even login banners benefit from `strip_lines_matching` + // in `src/filters/ssh.toml`. + assert_eq!( + rewrite_command_no_prefixes("ssh host", &[]), + Some("rtk ssh host".into()) + ); + } + + #[test] + fn test_rewrite_bare_ssh_not_rewritten() { + // `ssh` with no arguments at all is left alone (the regex requires + // `\s+` after `ssh`); without an argv it would just print usage to + // stderr and there's nothing to filter. + assert_eq!(rewrite_command_no_prefixes("ssh", &[]), None); + } + #[test] fn test_classify_sudo_stripped() { assert_eq!( diff --git a/src/discover/rules.rs b/src/discover/rules.rs index df7c72d03..0cde350db 100644 --- a/src/discover/rules.rs +++ b/src/discover/rules.rs @@ -425,6 +425,22 @@ pub const RULES: &[RtkRule] = &[ subcmd_savings: &[], subcmd_status: &[], }, + // #1654: ssh has a TOML filter (`src/filters/ssh.toml`) that passes + // `rtk verify --filter ssh` but the hook rewrite path never reached it + // because the registry didn't list a matching rule. Adding the rule lets + // `ssh user@host cmd` flow through `rtk rewrite` → `rtk ssh user@host cmd` + // → main.rs's TOML-filter dispatch. Interactive `ssh host` (no remote + // command, banners only) is intentionally not rewritten — `\s+` requires + // at least one argument, mirroring wget/curl above. + RtkRule { + pattern: r"^ssh\s+", + rtk_cmd: "rtk ssh", + rewrite_prefixes: &["ssh"], + category: "Network", + savings_pct: 65.0, + subcmd_savings: &[], + subcmd_status: &[], + }, RtkRule { pattern: r"^(python3?\s+-m\s+)?mypy(\s|$)", rtk_cmd: "rtk mypy",