1use lazy_static::lazy_static;
4use regex::{Regex, RegexSet};
5
6use super::lexer::{split_on_operators, tokenize, TokenKind};
7use super::rules::{IGNORED_EXACT, IGNORED_PREFIXES, RULES};
8
9#[derive(Debug, PartialEq)]
11pub enum Classification {
12 Supported {
13 rtk_equivalent: &'static str,
14 category: &'static str,
15 estimated_savings_pct: f64,
16 status: super::report::RtkStatus,
17 },
18 Unsupported {
19 base_command: String,
20 },
21 Ignored,
22}
23
24pub fn category_avg_tokens(category: &str, subcmd: &str) -> usize {
26 match category {
27 "Git" => match subcmd {
28 "log" | "diff" | "show" => 200,
29 _ => 40,
30 },
31 "Cargo" => match subcmd {
32 "test" => 500,
33 _ => 150,
34 },
35 "Tests" => 800,
36 "Files" => 100,
37 "Build" => 300,
38 "Infra" => 120,
39 "Network" => 150,
40 "GitHub" => 200,
41 "GitLab" => 200,
42 "PackageManager" => 150,
43 _ => 150,
44 }
45}
46
47lazy_static! {
48 static ref REGEX_SET: RegexSet =
49 RegexSet::new(RULES.iter().map(|r| r.pattern)).expect("invalid regex patterns");
50 static ref COMPILED: Vec<Regex> = RULES
51 .iter()
52 .map(|r| Regex::new(r.pattern).expect("invalid regex"))
53 .collect();
54 static ref ENV_PREFIX: Regex = {
55 let double_quoted = r#""(?:[^"\\]|\\.)*""#;
56 let single_quoted = r#"'(?:[^'\\]|\\.)*'"#;
57 let unquoted = r#"[^\s]*"#;
58 let env_value = format!("(?:{}|{}|{})", double_quoted, single_quoted, unquoted);
59 let env_assign = format!(r#"[A-Z_][A-Z0-9_]*={}"#, env_value);
60 Regex::new(&format!(r#"^(?:sudo\s+|env\s+|{}\s+)+"#, env_assign)).unwrap()
61 };
62 static ref GIT_GLOBAL_OPT: Regex =
65 Regex::new(r"^(?:(?:-C\s+\S+|-c\s+\S+|--git-dir(?:=\S+|\s+\S+)|--work-tree(?:=\S+|\s+\S+)|--no-pager|--no-optional-locks|--bare|--literal-pathspecs)\s+)+").unwrap();
66 static ref HEAD_N: Regex = Regex::new(r"^head\s+-(\d+)\s+(\S+)$").unwrap();
71 static ref HEAD_LINES: Regex = Regex::new(r"^head\s+--lines=(\d+)\s+(\S+)$").unwrap();
72 static ref TAIL_N: Regex = Regex::new(r"^tail\s+-(\d+)\s+(\S+)$").unwrap();
73 static ref TAIL_N_SPACE: Regex = Regex::new(r"^tail\s+-n\s+(\d+)\s+(\S+)$").unwrap();
74 static ref TAIL_LINES_EQ: Regex = Regex::new(r"^tail\s+--lines=(\d+)\s+(\S+)$").unwrap();
75 static ref TAIL_LINES_SPACE: Regex = Regex::new(r"^tail\s+--lines\s+(\d+)\s+(\S+)$").unwrap();
76}
77
78const GOLANGCI_GLOBAL_OPT_WITH_VALUE: &[&str] = &[
79 "-c",
80 "--color",
81 "--config",
82 "--cpu-profile-path",
83 "--mem-profile-path",
84 "--trace-path",
85];
86
87#[derive(Debug, Clone, Copy)]
88struct GolangciRunParts<'a> {
89 global_segment: &'a str,
90 run_segment: &'a str,
91}
92
93pub fn classify_command(cmd: &str) -> Classification {
95 let trimmed = cmd.trim();
96 if trimmed.is_empty() {
97 return Classification::Ignored;
98 }
99
100 for exact in IGNORED_EXACT {
102 if trimmed == *exact {
103 return Classification::Ignored;
104 }
105 }
106 for prefix in IGNORED_PREFIXES {
107 if trimmed.starts_with(prefix) {
108 return Classification::Ignored;
109 }
110 }
111
112 let stripped = ENV_PREFIX.replace(trimmed, "");
114 let cmd_clean = stripped.trim();
115 if cmd_clean.is_empty() {
116 return Classification::Ignored;
117 }
118
119 let cmd_normalized = strip_absolute_path(cmd_clean);
121 let cmd_normalized = strip_git_global_opts(&cmd_normalized);
123 let cmd_normalized = strip_golangci_global_opts(&cmd_normalized);
126 let cmd_clean = cmd_normalized.as_str();
127
128 if cmd_clean.starts_with("cat ")
130 || cmd_clean.starts_with("head ")
131 || cmd_clean.starts_with("tail ")
132 {
133 let has_redirect = cmd_clean
134 .split_whitespace()
135 .skip(1)
136 .any(|t| t.starts_with('>') || t == "<" || t.starts_with(">>"));
137 if has_redirect {
138 return Classification::Unsupported {
139 base_command: cmd_clean
140 .split_whitespace()
141 .next()
142 .unwrap_or("cat")
143 .to_string(),
144 };
145 }
146 }
147
148 let matches: Vec<usize> = REGEX_SET.matches(cmd_clean).into_iter().collect();
150 if let Some(&idx) = matches.last() {
151 let rule = &RULES[idx];
152
153 let (savings, status) = if let Some(caps) = COMPILED[idx].captures(cmd_clean) {
155 if let Some(sub) = caps.get(1) {
156 let subcmd = sub.as_str();
157 let status = rule
159 .subcmd_status
160 .iter()
161 .find(|(s, _)| *s == subcmd)
162 .map(|(_, st)| *st)
163 .unwrap_or(super::report::RtkStatus::Existing);
164
165 let savings = rule
167 .subcmd_savings
168 .iter()
169 .find(|(s, _)| *s == subcmd)
170 .map(|(_, pct)| *pct)
171 .unwrap_or(rule.savings_pct);
172
173 (savings, status)
174 } else {
175 (rule.savings_pct, super::report::RtkStatus::Existing)
176 }
177 } else {
178 (rule.savings_pct, super::report::RtkStatus::Existing)
179 };
180
181 Classification::Supported {
182 rtk_equivalent: rule.rtk_cmd,
183 category: rule.category,
184 estimated_savings_pct: savings,
185 status,
186 }
187 } else {
188 let base = extract_base_command(cmd_clean);
190 if base.is_empty() {
191 Classification::Ignored
192 } else {
193 Classification::Unsupported {
194 base_command: base.to_string(),
195 }
196 }
197 }
198}
199
200fn extract_base_command(cmd: &str) -> &str {
202 let parts: Vec<&str> = cmd.splitn(3, char::is_whitespace).collect();
203 match parts.len() {
204 0 => "",
205 1 => parts[0],
206 _ => {
207 let second = parts[1];
208 if !second.starts_with('-') && !second.contains('/') && !second.contains('.') {
210 let end = cmd
212 .find(char::is_whitespace)
213 .and_then(|i| {
214 let rest = &cmd[i..];
215 let trimmed = rest.trim_start();
216 trimmed
217 .find(char::is_whitespace)
218 .map(|j| i + (rest.len() - trimmed.len()) + j)
219 })
220 .unwrap_or(cmd.len());
221 &cmd[..end]
222 } else {
223 parts[0]
224 }
225 }
226 }
227}
228
229pub fn has_heredoc(cmd: &str) -> bool {
231 tokenize(cmd)
232 .iter()
233 .any(|t| t.kind == TokenKind::Redirect && t.value.starts_with("<<"))
234}
235
236pub fn split_command_chain(cmd: &str) -> Vec<&str> {
237 let trimmed = cmd.trim();
238 if trimmed.is_empty() {
239 return vec![];
240 }
241
242 if has_heredoc(trimmed) || trimmed.contains("$((") {
244 return vec![trimmed];
245 }
246
247 split_on_operators(trimmed, true)
248}
249
250fn strip_git_global_opts(cmd: &str) -> String {
254 if !cmd.starts_with("git ") {
256 return cmd.to_string();
257 }
258 let after_git = &cmd[4..]; let stripped = GIT_GLOBAL_OPT.replace(after_git, "");
260 format!("git {}", stripped.trim())
261}
262
263fn strip_golangci_global_opts(cmd: &str) -> String {
267 match parse_golangci_run_parts(cmd) {
268 Some(parts) => format!("golangci-lint {}", parts.run_segment),
269 None => cmd.to_string(),
270 }
271}
272
273fn parse_golangci_run_parts(cmd: &str) -> Option<GolangciRunParts<'_>> {
275 let tokens = split_token_spans(cmd);
276 let first = tokens.first()?;
277 if first.0 != "golangci-lint" && first.0 != "golangci" {
278 return None;
279 }
280
281 let mut i = 1;
282 while i < tokens.len() {
283 let token = tokens[i].0;
284
285 if token == "--" {
286 return None;
287 }
288
289 if !token.starts_with('-') {
290 if token == "run" {
291 let global_segment = if i > 1 {
292 cmd[tokens[1].1..tokens[i].1].trim()
293 } else {
294 ""
295 };
296 let run_segment = cmd[tokens[i].1..].trim();
297 return Some(GolangciRunParts {
298 global_segment,
299 run_segment,
300 });
301 }
302 return None;
303 }
304
305 if let Some(flag) = split_golangci_flag_name(token) {
306 if golangci_flag_takes_separate_value(token, flag) {
307 i += 1;
308 }
309 }
310
311 i += 1;
312 }
313
314 None
315}
316
317fn split_golangci_flag_name(arg: &str) -> Option<&str> {
318 if arg.starts_with("--") {
319 return Some(arg.split_once('=').map(|(flag, _)| flag).unwrap_or(arg));
320 }
321
322 if arg.starts_with('-') {
323 return Some(arg);
324 }
325
326 None
327}
328
329fn golangci_flag_takes_separate_value(arg: &str, flag: &str) -> bool {
330 if !GOLANGCI_GLOBAL_OPT_WITH_VALUE.contains(&flag) {
331 return false;
332 }
333
334 if arg.starts_with("--") && arg.contains('=') {
335 return false;
336 }
337
338 true
339}
340
341fn split_token_spans(cmd: &str) -> Vec<(&str, usize, usize)> {
342 let mut tokens = Vec::new();
343 let mut start = None;
344
345 for (idx, ch) in cmd.char_indices() {
346 if ch.is_whitespace() {
347 if let Some(token_start) = start.take() {
348 tokens.push((&cmd[token_start..idx], token_start, idx));
349 }
350 } else if start.is_none() {
351 start = Some(idx);
352 }
353 }
354
355 if let Some(token_start) = start {
356 tokens.push((&cmd[token_start..], token_start, cmd.len()));
357 }
358
359 tokens
360}
361
362fn strip_absolute_path(cmd: &str) -> String {
365 let first_space = cmd.find(' ');
366 let first_word = match first_space {
367 Some(pos) => &cmd[..pos],
368 None => cmd,
369 };
370 if first_word.contains('/') {
371 let basename = first_word.rsplit('/').next().unwrap_or(first_word);
373 if basename.is_empty() {
374 return cmd.to_string();
375 }
376 match first_space {
377 Some(pos) => format!("{}{}", basename, &cmd[pos..]),
378 None => basename.to_string(),
379 }
380 } else {
381 cmd.to_string()
382 }
383}
384
385pub fn prefix_contains_rtk_disabled(prefix_part: &str) -> bool {
386 prefix_part.contains("RTK_DISABLED=")
387}
388
389pub fn cmd_has_rtk_disabled_prefix(cmd: &str) -> bool {
391 let (prefix_part, _) = strip_disabled_prefix(cmd);
392 prefix_contains_rtk_disabled(prefix_part)
393}
394
395pub fn strip_disabled_prefix(cmd: &str) -> (&str, &str) {
397 let trimmed = cmd.trim();
398 let stripped = ENV_PREFIX.replace(trimmed, "");
399 let prefix_len = trimmed.len() - stripped.len();
402 let prefix_part = &trimmed[..prefix_len];
403 let rest = trimmed[prefix_len..].trim();
404 (prefix_part, rest)
405}
406
407fn strip_trailing_redirects(cmd: &str) -> (&str, &str) {
408 let tokens = tokenize(cmd);
409 if tokens.is_empty() {
410 return (cmd, "");
411 }
412
413 let mut redir_boundary = tokens.len();
414 let mut i = tokens.len();
415 while i > 0 {
416 i -= 1;
417 match tokens[i].kind {
418 TokenKind::Redirect => {
419 redir_boundary = i;
420 }
421 TokenKind::Arg => {
422 if i > 0 && tokens[i - 1].kind == TokenKind::Redirect {
423 redir_boundary = i - 1;
424 i -= 1;
425 } else {
426 break;
427 }
428 }
429 _ => break,
430 }
431 }
432
433 if redir_boundary >= tokens.len() {
434 return (cmd, "");
435 }
436
437 let cut = tokens[redir_boundary].offset;
438 let cmd_part = cmd[..cut].trim_end();
439 let redir_part = &cmd[cmd_part.len()..];
440 (cmd_part, redir_part)
441}
442
443lazy_static! {
444 static ref LINE_CONTINUATION_RE: Regex =
452 Regex::new(r"(?m)[ \t\x0B\x0C]*\\\r?\n[ \t\x0B\x0C]*").unwrap();
453}
454
455fn collapse_line_continuations(s: &str) -> std::borrow::Cow<'_, str> {
459 LINE_CONTINUATION_RE.replace_all(s, " ")
460}
461
462pub fn rewrite_command(
482 cmd: &str,
483 excluded: &[String],
484 transparent_prefixes: &[String],
485) -> Option<String> {
486 rewrite_command_with_proxy(cmd, excluded, transparent_prefixes, "rtk")
487}
488
489pub fn rewrite_command_with_proxy(
496 cmd: &str,
497 excluded: &[String],
498 transparent_prefixes: &[String],
499 proxy_prefix: &str,
500) -> Option<String> {
501 let proxy_prefix = proxy_prefix.trim();
502
503 let normalized = collapse_line_continuations(cmd);
508 let trimmed = normalized.trim();
509 if trimmed.is_empty() {
510 return None;
511 }
512
513 if has_heredoc(trimmed) || trimmed.contains("$((") {
514 return None;
515 }
516
517 let compiled = compile_exclude_patterns(excluded);
518 let normalized_prefixes = normalize_transparent_prefixes(transparent_prefixes);
519
520 let has_compound = trimmed.contains("&&")
524 || trimmed.contains("||")
525 || trimmed.contains(';')
526 || trimmed.contains('|')
527 || trimmed.contains(" & ");
528 if !has_compound
529 && (trimmed.starts_with("rtk ")
530 || trimmed == "rtk"
531 || strip_word_prefix(trimmed, proxy_prefix).is_some())
532 {
533 return Some(trimmed.to_string());
534 }
535
536 rewrite_compound(trimmed, &compiled, &normalized_prefixes, proxy_prefix)
537}
538
539fn rewrite_compound(
541 cmd: &str,
542 excluded: &[ExcludePattern],
543 transparent_prefixes: &[String],
544 proxy_prefix: &str,
545) -> Option<String> {
546 let tokens = tokenize(cmd);
547 let mut result = String::with_capacity(cmd.len() + 32);
548 let mut any_changed = false;
549 let mut seg_start: usize = 0;
550
551 for tok in &tokens {
552 if tok.offset < seg_start {
553 continue;
554 }
555 match tok.kind {
556 TokenKind::Operator => {
557 let seg = cmd[seg_start..tok.offset].trim();
558 let rewritten = rewrite_segment(seg, excluded, transparent_prefixes, proxy_prefix)
559 .unwrap_or_else(|| seg.to_string());
560 if rewritten != seg {
561 any_changed = true;
562 }
563 result.push_str(&rewritten);
564 if tok.value == ";" {
565 result.push(';');
566 let after = tok.offset + tok.value.len();
567 if after < cmd.len() {
568 result.push(' ');
569 }
570 } else {
571 result.push(' ');
572 result.push_str(&tok.value);
573 result.push(' ');
574 }
575 seg_start = tok.offset + tok.value.len();
576 while seg_start < cmd.len() && cmd.as_bytes().get(seg_start) == Some(&b' ') {
577 seg_start += 1;
578 }
579 }
580 TokenKind::Pipe => {
581 let seg = cmd[seg_start..tok.offset].trim();
582 let is_pipe_incompatible = seg.starts_with("find ")
583 || seg == "find"
584 || seg.starts_with("fd ")
585 || seg == "fd";
586 let rewritten = if is_pipe_incompatible {
587 seg.to_string()
588 } else {
589 rewrite_segment(seg, excluded, transparent_prefixes, proxy_prefix)
590 .unwrap_or_else(|| seg.to_string())
591 };
592 if rewritten != seg {
593 any_changed = true;
594 }
595 result.push_str(&rewritten);
596
597 let pipe_group_end = tokens.iter().find(|t| {
598 t.offset > tok.offset
599 && (t.kind == TokenKind::Operator
600 || (t.kind == TokenKind::Shellism && t.value == "&"))
601 });
602
603 match pipe_group_end {
604 Some(next_op) => {
605 result.push(' ');
606 result.push_str(cmd[tok.offset..next_op.offset].trim());
607 seg_start = next_op.offset;
608 }
609 None => {
610 result.push(' ');
611 result.push_str(cmd[tok.offset..].trim_start());
612 return if any_changed { Some(result) } else { None };
613 }
614 }
615 }
616 TokenKind::Shellism if tok.value == "&" => {
617 let seg = cmd[seg_start..tok.offset].trim();
618 let rewritten = rewrite_segment(seg, excluded, transparent_prefixes, proxy_prefix)
619 .unwrap_or_else(|| seg.to_string());
620 if rewritten != seg {
621 any_changed = true;
622 }
623 result.push_str(&rewritten);
624 result.push_str(" & ");
625 seg_start = tok.offset + tok.value.len();
626 while seg_start < cmd.len() && cmd.as_bytes().get(seg_start) == Some(&b' ') {
627 seg_start += 1;
628 }
629 }
630 _ => {}
631 }
632 }
633
634 let seg = cmd[seg_start..].trim();
635 let rewritten = rewrite_segment(seg, excluded, transparent_prefixes, proxy_prefix)
636 .unwrap_or_else(|| seg.to_string());
637 if rewritten != seg {
638 any_changed = true;
639 }
640 result.push_str(&rewritten);
641
642 if any_changed {
643 Some(result)
644 } else {
645 None
646 }
647}
648
649fn rewrite_line_range(cmd: &str, proxy_prefix: &str) -> Option<String> {
650 for re in [&*HEAD_N, &*HEAD_LINES] {
651 if let Some(caps) = re.captures(cmd) {
652 let n = caps.get(1)?.as_str();
653 let file = caps.get(2)?.as_str();
654 return Some(format!("{} read {} --max-lines {}", proxy_prefix, file, n));
655 }
656 }
657 if cmd.starts_with("head -") {
658 return None;
659 }
660 for re in [
661 &*TAIL_N,
662 &*TAIL_N_SPACE,
663 &*TAIL_LINES_EQ,
664 &*TAIL_LINES_SPACE,
665 ] {
666 if let Some(caps) = re.captures(cmd) {
667 let n = caps.get(1)?.as_str();
668 let file = caps.get(2)?.as_str();
669 return Some(format!("{} read {} --tail-lines {}", proxy_prefix, file, n));
670 }
671 }
672 None
673}
674
675const SHELL_PREFIX_BUILTINS: &[&str] = &["noglob", "command", "builtin", "exec", "nocorrect"];
678
679const MAX_PREFIX_DEPTH: usize = 10;
680
681enum ExcludePattern {
682 Regex(Regex),
683 Prefix(String),
684}
685
686fn compile_exclude_patterns(patterns: &[String]) -> Vec<ExcludePattern> {
687 patterns
688 .iter()
689 .filter_map(|pattern| {
690 let trimmed = pattern.trim();
691 if trimmed.is_empty() || trimmed == "^" {
692 eprintln!(
693 "rtk: warning: ignoring trivial exclude_commands pattern '{}'",
694 pattern
695 );
696 return None;
697 }
698 let anchored = if trimmed.starts_with('^') {
699 trimmed.to_string()
700 } else {
701 format!(r"^{}($|\s)", regex::escape(trimmed))
702 };
703 Some(match Regex::new(&anchored) {
704 Ok(re) => ExcludePattern::Regex(re),
705 Err(e) => {
706 eprintln!(
707 "rtk: warning: invalid exclude_commands pattern '{}': {}",
708 pattern, e
709 );
710 ExcludePattern::Prefix(trimmed.to_string())
711 }
712 })
713 })
714 .collect()
715}
716
717fn normalize_transparent_prefixes(prefixes: &[String]) -> Vec<String> {
718 let mut normalized: Vec<String> = prefixes
719 .iter()
720 .map(|prefix| prefix.trim())
721 .filter(|prefix| !prefix.is_empty())
722 .map(str::to_string)
723 .collect();
724
725 normalized.sort_by(|a, b| b.len().cmp(&a.len()).then_with(|| a.cmp(b)));
727 normalized.dedup();
728 normalized
729}
730
731fn rewrite_segment(
732 seg: &str,
733 excluded: &[ExcludePattern],
734 transparent_prefixes: &[String],
735 proxy_prefix: &str,
736) -> Option<String> {
737 rewrite_segment_inner(seg, excluded, transparent_prefixes, proxy_prefix, 0)
738}
739
740fn is_excluded(cmd: &str, excluded: &[ExcludePattern]) -> bool {
741 excluded.iter().any(|pat| match pat {
742 ExcludePattern::Regex(re) => re.is_match(cmd),
743 ExcludePattern::Prefix(prefix) => cmd.starts_with(prefix.as_str()),
744 })
745}
746
747fn rewrite_segment_inner(
748 seg: &str,
749 excluded: &[ExcludePattern],
750 transparent_prefixes: &[String],
751 proxy_prefix: &str,
752 depth: usize,
753) -> Option<String> {
754 let trimmed = seg.trim();
755 if trimmed.is_empty() {
756 return None;
757 }
758
759 if depth >= MAX_PREFIX_DEPTH {
760 return None;
761 }
762
763 let (env_prefix, rest_after_env) = strip_disabled_prefix(trimmed);
764 if !env_prefix.is_empty() {
765 if env_prefix.contains("RTK_DISABLED=") {
768 eprintln!(
769 "[rtk] RTK_DISABLED=1 detected — skipping filter for this command. \
770 Remove RTK_DISABLED=1 to restore token savings."
771 );
772 return None;
773 }
774 let rewritten = rewrite_segment_inner(
775 rest_after_env,
776 excluded,
777 transparent_prefixes,
778 proxy_prefix,
779 depth + 1,
780 )?;
781 return Some(format!("{}{}", env_prefix, rewritten));
782 }
783
784 for &prefix in SHELL_PREFIX_BUILTINS {
785 if let Some(rest) = strip_word_prefix(trimmed, prefix) {
786 if rest.is_empty() {
787 return None;
788 }
789 return rewrite_segment_inner(
790 rest,
791 excluded,
792 transparent_prefixes,
793 proxy_prefix,
794 depth + 1,
795 )
796 .map(|rewritten| format!("{} {}", prefix, rewritten));
797 }
798 }
799
800 for prefix in transparent_prefixes {
803 if let Some(rest) = strip_word_prefix(trimmed, prefix) {
804 if rest.is_empty() {
805 return None;
806 }
807 return rewrite_segment_inner(
808 rest,
809 excluded,
810 transparent_prefixes,
811 proxy_prefix,
812 depth + 1,
813 )
814 .map(|rewritten| format!("{} {}", prefix, rewritten));
815 }
816 }
817
818 let (cmd_part, redirect_suffix) = strip_trailing_redirects(trimmed);
821
822 if cmd_part.starts_with("rtk ")
825 || cmd_part == "rtk"
826 || strip_word_prefix(cmd_part, proxy_prefix).is_some()
827 {
828 return Some(trimmed.to_string());
829 }
830
831 if cmd_part.starts_with("head -") || cmd_part.starts_with("tail ") {
832 return rewrite_line_range(cmd_part, proxy_prefix)
833 .map(|r| format!("{}{}", r, redirect_suffix));
834 }
835
836 if let Some(cmd_args) = cmd_part.strip_prefix("cat ") {
840 let args = cmd_args.trim_start();
841 if args.starts_with('-') && !args.starts_with("-n ") && !args.starts_with("-n\t") {
842 return None;
843 }
844 }
845
846 let rtk_equivalent = match classify_command(cmd_part) {
848 Classification::Supported { rtk_equivalent, .. } => {
849 let stripped = ENV_PREFIX.replace(cmd_part, "");
850 let cmd_clean = stripped.trim();
851 if is_excluded(cmd_clean, excluded) {
852 return None;
853 }
854 rtk_equivalent
855 }
856 _ => return None,
857 };
858
859 let rule = RULES.iter().find(|r| r.rtk_cmd == rtk_equivalent)?;
861
862 if let Some(parts) = parse_golangci_run_parts(cmd_part) {
863 let rewritten = if parts.global_segment.is_empty() {
864 format!("{} golangci-lint {}", proxy_prefix, parts.run_segment)
865 } else {
866 format!(
867 "{} golangci-lint {} {}",
868 proxy_prefix, parts.global_segment, parts.run_segment
869 )
870 };
871 return Some(rewritten);
872 }
873
874 if rule.rtk_cmd == "rtk gh" {
877 let args_lower = cmd_part.to_lowercase();
878 if args_lower.contains("--json")
879 || args_lower.contains("--jq")
880 || args_lower.contains("--template")
881 {
882 return None;
883 }
884 }
885
886 for &prefix in rule.rewrite_prefixes {
888 if let Some(rest) = strip_word_prefix(cmd_part, prefix) {
889 let rewritten = if rest.is_empty() {
890 format!(
891 "{}{}",
892 with_proxy(rule.rtk_cmd, proxy_prefix)?,
893 redirect_suffix
894 )
895 } else {
896 format!(
897 "{} {}{}",
898 with_proxy(rule.rtk_cmd, proxy_prefix)?,
899 rest,
900 redirect_suffix
901 )
902 };
903 return Some(rewritten);
904 }
905 }
906
907 None
908}
909
910fn with_proxy(rtk_cmd: &str, proxy_prefix: &str) -> Option<String> {
911 let rest = strip_word_prefix(rtk_cmd, "rtk")?;
912 if rest.is_empty() {
913 Some(proxy_prefix.to_string())
914 } else {
915 Some(format!("{} {}", proxy_prefix, rest))
916 }
917}
918
919fn strip_word_prefix<'a>(cmd: &'a str, prefix: &str) -> Option<&'a str> {
922 if cmd == prefix {
923 Some("")
924 } else if cmd.len() > prefix.len()
925 && cmd.starts_with(prefix)
926 && cmd.as_bytes()[prefix.len()] == b' '
927 {
928 Some(cmd[prefix.len() + 1..].trim_start())
929 } else {
930 None
931 }
932}
933
934#[cfg(test)]
935mod tests {
936 use super::super::report::RtkStatus;
937 use super::*;
938
939 fn rewrite_command_no_prefixes(cmd: &str, excluded: &[String]) -> Option<String> {
940 super::rewrite_command(cmd, excluded, &[])
941 }
942
943 fn rewrite_command_with_host_proxy(cmd: &str) -> Option<String> {
944 super::rewrite_command_with_proxy(cmd, &[], &[], "'/opt/anvil' __rtk")
945 }
946
947 #[test]
948 fn test_classify_git_status() {
949 assert_eq!(
950 classify_command("git status"),
951 Classification::Supported {
952 rtk_equivalent: "rtk git",
953 category: "Git",
954 estimated_savings_pct: 70.0,
955 status: RtkStatus::Existing,
956 }
957 );
958 }
959
960 #[test]
961 fn test_classify_yadm_status() {
962 assert_eq!(
963 classify_command("yadm status"),
964 Classification::Supported {
965 rtk_equivalent: "rtk git",
966 category: "Git",
967 estimated_savings_pct: 70.0,
968 status: RtkStatus::Existing,
969 }
970 );
971 }
972
973 #[test]
974 fn test_classify_yadm_diff() {
975 assert_eq!(
976 classify_command("yadm diff"),
977 Classification::Supported {
978 rtk_equivalent: "rtk git",
979 category: "Git",
980 estimated_savings_pct: 80.0,
981 status: RtkStatus::Existing,
982 }
983 );
984 }
985
986 #[test]
987 fn test_rewrite_yadm_status() {
988 assert_eq!(
989 rewrite_command_no_prefixes("yadm status", &[]),
990 Some("rtk git status".to_string())
991 );
992 }
993
994 #[test]
995 fn test_rewrite_with_custom_proxy() {
996 assert_eq!(
997 rewrite_command_with_host_proxy("git status"),
998 Some("'/opt/anvil' __rtk git status".to_string())
999 );
1000 }
1001
1002 #[test]
1003 fn test_rewrite_compound_with_custom_proxy() {
1004 assert_eq!(
1005 rewrite_command_with_host_proxy("cargo test && git status"),
1006 Some("'/opt/anvil' __rtk cargo test && '/opt/anvil' __rtk git status".to_string())
1007 );
1008 }
1009
1010 #[test]
1011 fn test_with_proxy_requires_rtk_word_boundary() {
1012 assert_eq!(super::with_proxy("rtk git", "P"), Some("P git".to_string()));
1013 assert_eq!(super::with_proxy("rtk", "P"), Some("P".to_string()));
1014 assert_eq!(super::with_proxy("rtkfoo bar", "P"), None);
1016 }
1017
1018 #[test]
1019 fn test_rewrite_with_custom_proxy_is_idempotent() {
1020 let once = rewrite_command_with_host_proxy("git status").expect("first rewrite");
1021 assert_eq!(rewrite_command_with_host_proxy(&once), Some(once.clone()));
1024 }
1025
1026 #[test]
1027 fn test_rewrite_compound_with_custom_proxy_is_idempotent() {
1028 let once =
1029 rewrite_command_with_host_proxy("cargo test && git status").expect("first rewrite");
1030 assert_eq!(rewrite_command_with_host_proxy(&once), None);
1033 }
1034
1035 #[test]
1036 fn test_classify_git_diff_cached() {
1037 assert_eq!(
1038 classify_command("git diff --cached"),
1039 Classification::Supported {
1040 rtk_equivalent: "rtk git",
1041 category: "Git",
1042 estimated_savings_pct: 80.0,
1043 status: RtkStatus::Existing,
1044 }
1045 );
1046 }
1047
1048 #[test]
1049 fn test_classify_cargo_test_filter() {
1050 assert_eq!(
1051 classify_command("cargo test filter::"),
1052 Classification::Supported {
1053 rtk_equivalent: "rtk cargo",
1054 category: "Cargo",
1055 estimated_savings_pct: 90.0,
1056 status: RtkStatus::Existing,
1057 }
1058 );
1059 }
1060
1061 #[test]
1062 fn test_classify_npx_tsc() {
1063 assert_eq!(
1064 classify_command("npx tsc --noEmit"),
1065 Classification::Supported {
1066 rtk_equivalent: "rtk tsc",
1067 category: "Build",
1068 estimated_savings_pct: 83.0,
1069 status: RtkStatus::Existing,
1070 }
1071 );
1072 }
1073
1074 #[test]
1075 fn test_classify_cat_file() {
1076 assert_eq!(
1077 classify_command("cat src/main.rs"),
1078 Classification::Supported {
1079 rtk_equivalent: "rtk read",
1080 category: "Files",
1081 estimated_savings_pct: 60.0,
1082 status: RtkStatus::Existing,
1083 }
1084 );
1085 }
1086
1087 #[test]
1088 fn test_classify_cat_redirect_not_supported() {
1089 let write_commands = [
1091 "cat > /tmp/output.txt",
1092 "cat >> /tmp/output.txt",
1093 "cat file.txt > output.txt",
1094 "cat -n file.txt >> log.txt",
1095 "head -10 README.md > output.txt",
1096 "tail -f app.log > /dev/null",
1097 ];
1098 for cmd in &write_commands {
1099 if let Classification::Supported { .. } = classify_command(cmd) {
1100 panic!("{} should NOT be classified as Supported", cmd)
1101 }
1102 }
1104 }
1105
1106 #[test]
1107 fn test_classify_cd_ignored() {
1108 assert_eq!(classify_command("cd /tmp"), Classification::Ignored);
1109 }
1110
1111 #[test]
1112 fn test_classify_rtk_already() {
1113 assert_eq!(classify_command("rtk git status"), Classification::Ignored);
1114 }
1115
1116 #[test]
1117 fn test_classify_echo_ignored() {
1118 assert_eq!(
1119 classify_command("echo hello world"),
1120 Classification::Ignored
1121 );
1122 }
1123
1124 #[test]
1125 fn test_classify_htop_unsupported() {
1126 match classify_command("htop -d 10") {
1127 Classification::Unsupported { base_command } => {
1128 assert_eq!(base_command, "htop");
1129 }
1130 other => panic!("expected Unsupported, got {:?}", other),
1131 }
1132 }
1133
1134 #[test]
1135 fn test_classify_env_prefix_stripped() {
1136 assert_eq!(
1137 classify_command("GIT_SSH_COMMAND=ssh git push"),
1138 Classification::Supported {
1139 rtk_equivalent: "rtk git",
1140 category: "Git",
1141 estimated_savings_pct: 70.0,
1142 status: RtkStatus::Existing,
1143 }
1144 );
1145 }
1146
1147 #[test]
1148 fn test_classify_sudo_stripped() {
1149 assert_eq!(
1150 classify_command("sudo docker ps"),
1151 Classification::Supported {
1152 rtk_equivalent: "rtk docker",
1153 category: "Infra",
1154 estimated_savings_pct: 85.0,
1155 status: RtkStatus::Existing,
1156 }
1157 );
1158 }
1159
1160 #[test]
1161 fn test_classify_cargo_check() {
1162 assert_eq!(
1163 classify_command("cargo check"),
1164 Classification::Supported {
1165 rtk_equivalent: "rtk cargo",
1166 category: "Cargo",
1167 estimated_savings_pct: 80.0,
1168 status: RtkStatus::Existing,
1169 }
1170 );
1171 }
1172
1173 #[test]
1174 fn test_classify_cargo_check_all_targets() {
1175 assert_eq!(
1176 classify_command("cargo check --all-targets"),
1177 Classification::Supported {
1178 rtk_equivalent: "rtk cargo",
1179 category: "Cargo",
1180 estimated_savings_pct: 80.0,
1181 status: RtkStatus::Existing,
1182 }
1183 );
1184 }
1185
1186 #[test]
1187 fn test_classify_cargo_fmt_passthrough() {
1188 assert_eq!(
1189 classify_command("cargo fmt"),
1190 Classification::Supported {
1191 rtk_equivalent: "rtk cargo",
1192 category: "Cargo",
1193 estimated_savings_pct: 80.0,
1194 status: RtkStatus::Passthrough,
1195 }
1196 );
1197 }
1198
1199 #[test]
1200 fn test_classify_cargo_clippy_savings() {
1201 assert_eq!(
1202 classify_command("cargo clippy --all-targets"),
1203 Classification::Supported {
1204 rtk_equivalent: "rtk cargo",
1205 category: "Cargo",
1206 estimated_savings_pct: 80.0,
1207 status: RtkStatus::Existing,
1208 }
1209 );
1210 }
1211
1212 #[test]
1213 fn test_registry_covers_all_cargo_subcommands() {
1214 for subcmd in ["build", "test", "clippy", "check", "fmt"] {
1217 let cmd = format!("cargo {subcmd}");
1218 match classify_command(&cmd) {
1219 Classification::Supported { .. } => {}
1220 other => panic!("cargo {subcmd} should be Supported, got {other:?}"),
1221 }
1222 }
1223 }
1224
1225 #[test]
1226 fn test_registry_covers_all_git_subcommands() {
1227 for subcmd in [
1229 "status", "log", "diff", "show", "add", "commit", "push", "pull", "branch", "fetch",
1230 "stash", "worktree",
1231 ] {
1232 let cmd = format!("git {subcmd}");
1233 match classify_command(&cmd) {
1234 Classification::Supported { .. } => {}
1235 other => panic!("git {subcmd} should be Supported, got {other:?}"),
1236 }
1237 }
1238 }
1239
1240 #[test]
1241 fn test_classify_find_not_blocked_by_fi() {
1242 assert_eq!(
1245 classify_command("find . -name foo"),
1246 Classification::Supported {
1247 rtk_equivalent: "rtk find",
1248 category: "Files",
1249 estimated_savings_pct: 70.0,
1250 status: RtkStatus::Existing,
1251 }
1252 );
1253 }
1254
1255 #[test]
1256 fn test_fi_still_ignored_exact() {
1257 assert_eq!(classify_command("fi"), Classification::Ignored);
1259 }
1260
1261 #[test]
1262 fn test_done_still_ignored_exact() {
1263 assert_eq!(classify_command("done"), Classification::Ignored);
1265 }
1266
1267 #[test]
1268 fn test_split_chain_and() {
1269 assert_eq!(split_command_chain("a && b"), vec!["a", "b"]);
1270 }
1271
1272 #[test]
1273 fn test_split_chain_semicolon() {
1274 assert_eq!(split_command_chain("a ; b"), vec!["a", "b"]);
1275 }
1276
1277 #[test]
1278 fn test_split_pipe_first_only() {
1279 assert_eq!(split_command_chain("a | b"), vec!["a"]);
1280 }
1281
1282 #[test]
1283 fn test_split_single() {
1284 assert_eq!(split_command_chain("git status"), vec!["git status"]);
1285 }
1286
1287 #[test]
1288 fn test_split_quoted_and() {
1289 assert_eq!(
1290 split_command_chain(r#"echo "a && b""#),
1291 vec![r#"echo "a && b""#]
1292 );
1293 }
1294
1295 #[test]
1296 fn test_split_heredoc_no_split() {
1297 let cmd = "cat <<'EOF'\nhello && world\nEOF";
1298 assert_eq!(split_command_chain(cmd), vec![cmd]);
1299 }
1300
1301 #[test]
1302 fn test_classify_mypy() {
1303 assert_eq!(
1304 classify_command("mypy src/"),
1305 Classification::Supported {
1306 rtk_equivalent: "rtk mypy",
1307 category: "Build",
1308 estimated_savings_pct: 80.0,
1309 status: RtkStatus::Existing,
1310 }
1311 );
1312 }
1313
1314 #[test]
1315 fn test_classify_python_m_mypy() {
1316 assert_eq!(
1317 classify_command("python3 -m mypy --strict"),
1318 Classification::Supported {
1319 rtk_equivalent: "rtk mypy",
1320 category: "Build",
1321 estimated_savings_pct: 80.0,
1322 status: RtkStatus::Existing,
1323 }
1324 );
1325 }
1326
1327 #[test]
1330 fn test_rewrite_git_status() {
1331 assert_eq!(
1332 rewrite_command_no_prefixes("git status", &[]),
1333 Some("rtk git status".into())
1334 );
1335 }
1336
1337 #[test]
1338 fn test_rewrite_git_log() {
1339 assert_eq!(
1340 rewrite_command_no_prefixes("git log -10", &[]),
1341 Some("rtk git log -10".into())
1342 );
1343 }
1344
1345 #[test]
1348 fn test_rewrite_git_dash_c_status() {
1349 assert_eq!(
1350 rewrite_command_no_prefixes("git -C /path/to/repo status", &[]),
1351 Some("rtk git -C /path/to/repo status".into())
1352 );
1353 }
1354
1355 #[test]
1356 fn test_rewrite_git_dash_c_log() {
1357 assert_eq!(
1358 rewrite_command_no_prefixes("git -C /tmp/myrepo log --oneline -5", &[]),
1359 Some("rtk git -C /tmp/myrepo log --oneline -5".into())
1360 );
1361 }
1362
1363 #[test]
1364 fn test_rewrite_git_dash_c_diff() {
1365 assert_eq!(
1366 rewrite_command_no_prefixes("git -C /home/user/project diff --name-only", &[]),
1367 Some("rtk git -C /home/user/project diff --name-only".into())
1368 );
1369 }
1370
1371 #[test]
1372 fn test_classify_git_dash_c() {
1373 let result = classify_command("git -C /tmp status");
1374 assert!(
1375 matches!(
1376 result,
1377 Classification::Supported {
1378 rtk_equivalent: "rtk git",
1379 ..
1380 }
1381 ),
1382 "git -C should be classified as supported, got: {:?}",
1383 result
1384 );
1385 }
1386
1387 #[test]
1388 fn test_rewrite_cargo_test() {
1389 assert_eq!(
1390 rewrite_command_no_prefixes("cargo test", &[]),
1391 Some("rtk cargo test".into())
1392 );
1393 }
1394
1395 #[test]
1396 fn test_rewrite_compound_and() {
1397 assert_eq!(
1398 rewrite_command_no_prefixes("git add . && cargo test", &[]),
1399 Some("rtk git add . && rtk cargo test".into())
1400 );
1401 }
1402
1403 #[test]
1404 fn test_rewrite_compound_three_segments() {
1405 assert_eq!(
1406 rewrite_command_no_prefixes(
1407 "cargo fmt --all && cargo clippy --all-targets && cargo test",
1408 &[]
1409 ),
1410 Some("rtk cargo fmt --all && rtk cargo clippy --all-targets && rtk cargo test".into())
1411 );
1412 }
1413
1414 #[test]
1415 fn test_rewrite_already_rtk() {
1416 assert_eq!(
1417 rewrite_command_no_prefixes("rtk git status", &[]),
1418 Some("rtk git status".into())
1419 );
1420 }
1421
1422 #[test]
1423 fn test_rewrite_background_single_amp() {
1424 assert_eq!(
1425 rewrite_command_no_prefixes("cargo test & git status", &[]),
1426 Some("rtk cargo test & rtk git status".into())
1427 );
1428 }
1429
1430 #[test]
1431 fn test_rewrite_background_unsupported_right() {
1432 assert_eq!(
1433 rewrite_command_no_prefixes("cargo test & htop", &[]),
1434 Some("rtk cargo test & htop".into())
1435 );
1436 }
1437
1438 #[test]
1439 fn test_rewrite_background_does_not_affect_double_amp() {
1440 assert_eq!(
1442 rewrite_command_no_prefixes("cargo test && git status", &[]),
1443 Some("rtk cargo test && rtk git status".into())
1444 );
1445 }
1446
1447 #[test]
1448 fn test_rewrite_unsupported_returns_none() {
1449 assert_eq!(rewrite_command_no_prefixes("htop", &[]), None);
1450 }
1451
1452 #[test]
1453 fn test_rewrite_ignored_cd() {
1454 assert_eq!(rewrite_command_no_prefixes("cd /tmp", &[]), None);
1455 }
1456
1457 #[test]
1458 fn test_rewrite_with_env_prefix() {
1459 assert_eq!(
1460 rewrite_command_no_prefixes("GIT_SSH_COMMAND=ssh git push", &[]),
1461 Some("GIT_SSH_COMMAND=ssh rtk git push".into())
1462 );
1463 }
1464
1465 #[test]
1466 fn test_rewrite_tsc() {
1467 let commands = vec![
1468 "npm exec tsc",
1469 "npm rum tsc",
1470 "npm run tsc",
1471 "npm run-script tsc",
1472 "npm urn tsc",
1473 "npm x tsc",
1474 "pnpm dlx tsc",
1475 "pnpm exec tsc",
1476 "pnpm run tsc",
1477 "pnpm run-script tsc",
1478 "npm tsc",
1479 "npx tsc",
1480 "pnpm tsc",
1481 "pnpx tsc",
1482 "tsc",
1483 ];
1484 for command in commands {
1485 assert_eq!(
1486 rewrite_command_no_prefixes(&format!("{command} --noEmit"), &[]),
1487 Some("rtk tsc --noEmit".into()),
1488 "Failed for command: {}",
1489 command
1490 );
1491 }
1492 }
1493
1494 #[test]
1495 fn test_rewrite_cat_file() {
1496 assert_eq!(
1497 rewrite_command_no_prefixes("cat src/main.rs", &[]),
1498 Some("rtk read src/main.rs".into())
1499 );
1500 }
1501
1502 #[test]
1503 fn test_rewrite_cat_with_incompatible_flags_skipped() {
1504 assert_eq!(rewrite_command_no_prefixes("cat -A file.cpp", &[]), None);
1506 assert_eq!(rewrite_command_no_prefixes("cat -v file.txt", &[]), None);
1507 assert_eq!(rewrite_command_no_prefixes("cat -e file.txt", &[]), None);
1508 assert_eq!(rewrite_command_no_prefixes("cat -t file.txt", &[]), None);
1509 assert_eq!(rewrite_command_no_prefixes("cat -s file.txt", &[]), None);
1510 assert_eq!(
1511 rewrite_command_no_prefixes("cat --show-all file.txt", &[]),
1512 None
1513 );
1514 }
1515
1516 #[test]
1517 fn test_rewrite_cat_with_compatible_flags() {
1518 assert_eq!(
1520 rewrite_command_no_prefixes("cat -n file.txt", &[]),
1521 Some("rtk read -n file.txt".into())
1522 );
1523 }
1524
1525 #[test]
1526 fn test_rewrite_rg_pattern() {
1527 assert_eq!(
1528 rewrite_command_no_prefixes("rg \"fn main\"", &[]),
1529 Some("rtk grep \"fn main\"".into())
1530 );
1531 }
1532
1533 #[test]
1534 fn test_rewrite_playwright() {
1535 let commands = vec![
1536 "npm exec playwright",
1537 "npm rum playwright",
1538 "npm run playwright",
1539 "npm run-script playwright",
1540 "npm urn playwright",
1541 "npm x playwright",
1542 "pnpm dlx playwright",
1543 "pnpm exec playwright",
1544 "pnpm run playwright",
1545 "pnpm run-script playwright",
1546 "npm playwright",
1547 "npx playwright",
1548 "pnpm playwright",
1549 "pnpx playwright",
1550 "playwright",
1551 ];
1552 for command in commands {
1553 assert_eq!(
1554 rewrite_command_no_prefixes(&format!("{command} test"), &[]),
1555 Some("rtk playwright test".into()),
1556 "Failed for command: {}",
1557 command
1558 );
1559 }
1560 }
1561
1562 #[test]
1563 fn test_rewrite_next_build() {
1564 let commands = vec![
1565 "npm exec next build",
1566 "npm rum next build",
1567 "npm run next build",
1568 "npm run-script next build",
1569 "npm urn next build",
1570 "npm x next build",
1571 "pnpm dlx next build",
1572 "pnpm exec next build",
1573 "pnpm run next build",
1574 "pnpm run-script next build",
1575 "npm next build",
1576 "npx next build",
1577 "pnpm next build",
1578 "pnpx next build",
1579 "next build",
1580 ];
1581 for command in commands {
1582 assert_eq!(
1583 rewrite_command_no_prefixes(&format!("{command} --turbo"), &[]),
1584 Some("rtk next --turbo".into()),
1585 "Failed for command: {}",
1586 command
1587 );
1588 }
1589 }
1590
1591 #[test]
1592 fn test_rewrite_pipe_first_only() {
1593 assert_eq!(
1595 rewrite_command_no_prefixes("git log -10 | grep feat", &[]),
1596 Some("rtk git log -10 | grep feat".into())
1597 );
1598 }
1599
1600 #[test]
1601 fn test_rewrite_find_pipe_skipped() {
1602 assert_eq!(
1605 rewrite_command_no_prefixes("find . -name '*.rs' | xargs grep 'fn run'", &[]),
1606 None
1607 );
1608 }
1609
1610 #[test]
1611 fn test_rewrite_find_pipe_xargs_wc() {
1612 assert_eq!(
1613 rewrite_command_no_prefixes("find src -type f | wc -l", &[]),
1614 None
1615 );
1616 }
1617
1618 #[test]
1619 fn test_rewrite_find_no_pipe_still_rewritten() {
1620 assert_eq!(
1622 rewrite_command_no_prefixes("find . -name '*.rs'", &[]),
1623 Some("rtk find . -name '*.rs'".into())
1624 );
1625 }
1626
1627 #[test]
1628 fn test_rewrite_heredoc_returns_none() {
1629 assert_eq!(
1630 rewrite_command_no_prefixes("cat <<'EOF'\nfoo\nEOF", &[]),
1631 None
1632 );
1633 }
1634
1635 #[test]
1636 fn test_rewrite_empty_returns_none() {
1637 assert_eq!(rewrite_command_no_prefixes("", &[]), None);
1638 assert_eq!(rewrite_command_no_prefixes(" ", &[]), None);
1639 }
1640
1641 #[test]
1642 fn test_rewrite_mixed_compound_partial() {
1643 assert_eq!(
1645 rewrite_command_no_prefixes("rtk git add . && cargo test", &[]),
1646 Some("rtk git add . && rtk cargo test".into())
1647 );
1648 }
1649
1650 #[test]
1653 fn test_rewrite_rtk_disabled_curl() {
1654 assert_eq!(
1655 rewrite_command_no_prefixes("RTK_DISABLED=1 curl https://example.com", &[]),
1656 None
1657 );
1658 }
1659
1660 #[test]
1661 fn test_rewrite_rtk_disabled_git_status() {
1662 assert_eq!(
1663 rewrite_command_no_prefixes("RTK_DISABLED=1 git status", &[]),
1664 None
1665 );
1666 }
1667
1668 #[test]
1669 fn test_rewrite_rtk_disabled_multi_env() {
1670 assert_eq!(
1671 rewrite_command_no_prefixes("FOO=1 RTK_DISABLED=1 git status", &[]),
1672 None
1673 );
1674 }
1675
1676 #[test]
1677 fn test_rewrite_rtk_disabled_warns_on_stderr() {
1678 assert_eq!(
1679 rewrite_command_no_prefixes("RTK_DISABLED=1 git status", &[]),
1680 None
1681 );
1682 }
1683
1684 #[test]
1685 fn test_rewrite_rtk_disabled_subprocess_warns() {
1686 let rtk_bin = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1687 .join("target")
1688 .join("debug")
1689 .join("rtk");
1690 if !rtk_bin.exists() {
1691 return;
1692 }
1693 let rtk_mtime = std::fs::metadata(&rtk_bin)
1694 .ok()
1695 .and_then(|m| m.modified().ok());
1696 let test_mtime = std::env::current_exe()
1697 .ok()
1698 .and_then(|p| std::fs::metadata(p).ok())
1699 .and_then(|m| m.modified().ok());
1700 if let (Some(rtk_t), Some(test_t)) = (rtk_mtime, test_mtime) {
1701 if rtk_t < test_t {
1702 return;
1703 }
1704 }
1705
1706 let output = std::process::Command::new(&rtk_bin)
1707 .args(["rewrite", "RTK_DISABLED=1 git status"])
1708 .output()
1709 .expect("Failed to run rtk");
1710
1711 assert!(
1712 !output.status.success(),
1713 "Should exit non-zero (no rewrite)"
1714 );
1715 let stderr = String::from_utf8_lossy(&output.stderr);
1716 assert!(
1717 stderr.contains("RTK_DISABLED=1 detected"),
1718 "Should warn on stderr, got: {}",
1719 stderr
1720 );
1721 }
1722
1723 #[test]
1724 fn test_rewrite_non_rtk_disabled_env_still_rewrites() {
1725 assert_eq!(
1726 rewrite_command_no_prefixes("SOME_VAR=1 git status", &[]),
1727 Some("SOME_VAR=1 rtk git status".into())
1728 );
1729 }
1730
1731 #[test]
1732 fn test_rewrite_env_quoted_value_with_spaces() {
1733 assert_eq!(
1734 rewrite_command_no_prefixes(
1735 r#"GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no" git push"#,
1736 &[]
1737 ),
1738 Some(r#"GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no" rtk git push"#.into())
1739 );
1740 }
1741
1742 #[test]
1743 fn test_rewrite_env_single_quoted_value_with_spaces() {
1744 assert_eq!(
1745 rewrite_command_no_prefixes("EDITOR='vim -u NONE' git commit", &[]),
1746 Some("EDITOR='vim -u NONE' rtk git commit".into())
1747 );
1748 }
1749
1750 #[test]
1751 fn test_rewrite_env_quoted_plus_unquoted() {
1752 assert_eq!(
1753 rewrite_command_no_prefixes(r#"FOO="bar baz" BAR=1 git status"#, &[]),
1754 Some(r#"FOO="bar baz" BAR=1 rtk git status"#.into())
1755 );
1756 }
1757
1758 #[test]
1759 fn test_rewrite_env_escaped_quotes_in_value() {
1760 assert_eq!(
1761 rewrite_command_no_prefixes(r#"FOO="he said \"hello\"" git status"#, &[]),
1762 Some(r#"FOO="he said \"hello\"" rtk git status"#.into())
1763 );
1764 }
1765
1766 #[test]
1767 fn test_classify_env_quoted_value_stripped() {
1768 assert_eq!(
1769 classify_command(r#"GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no" git push"#),
1770 Classification::Supported {
1771 rtk_equivalent: "rtk git",
1772 category: "Git",
1773 estimated_savings_pct: 70.0,
1774 status: RtkStatus::Existing,
1775 }
1776 );
1777 }
1778
1779 #[test]
1782 fn test_rewrite_redirect_2_gt_amp_1_with_pipe() {
1783 assert_eq!(
1784 rewrite_command_no_prefixes("cargo test 2>&1 | head", &[]),
1785 Some("rtk cargo test 2>&1 | head".into())
1786 );
1787 }
1788
1789 #[test]
1790 fn test_rewrite_redirect_2_gt_amp_1_trailing() {
1791 assert_eq!(
1792 rewrite_command_no_prefixes("cargo test 2>&1", &[]),
1793 Some("rtk cargo test 2>&1".into())
1794 );
1795 }
1796
1797 #[test]
1798 fn test_rewrite_redirect_plain_2_devnull() {
1799 assert_eq!(
1801 rewrite_command_no_prefixes("git status 2>/dev/null", &[]),
1802 Some("rtk git status 2>/dev/null".into())
1803 );
1804 }
1805
1806 #[test]
1807 fn test_rewrite_redirect_2_gt_amp_1_with_and() {
1808 assert_eq!(
1809 rewrite_command_no_prefixes("cargo test 2>&1 && echo done", &[]),
1810 Some("rtk cargo test 2>&1 && echo done".into())
1811 );
1812 }
1813
1814 #[test]
1815 fn test_rewrite_redirect_amp_gt_devnull() {
1816 assert_eq!(
1817 rewrite_command_no_prefixes("cargo test &>/dev/null", &[]),
1818 Some("rtk cargo test &>/dev/null".into())
1819 );
1820 }
1821
1822 #[test]
1823 fn test_rewrite_redirect_double() {
1824 assert_eq!(
1826 rewrite_command_no_prefixes("git status 2>&1 >/dev/null", &[]),
1827 Some("rtk git status 2>&1 >/dev/null".into())
1828 );
1829 }
1830
1831 #[test]
1832 fn test_rewrite_redirect_fd_close() {
1833 assert_eq!(
1835 rewrite_command_no_prefixes("git status 2>&-", &[]),
1836 Some("rtk git status 2>&-".into())
1837 );
1838 }
1839
1840 #[test]
1841 fn test_rewrite_redirect_quotes_not_stripped() {
1842 let result = rewrite_command_no_prefixes("git commit -m \"it's fixed\" 2>&1", &[]);
1845 assert!(
1846 result.is_some(),
1847 "Should still rewrite even with apostrophe"
1848 );
1849 }
1850
1851 #[test]
1852 fn test_rewrite_background_amp_non_regression() {
1853 assert_eq!(
1855 rewrite_command_no_prefixes("cargo test & git status", &[]),
1856 Some("rtk cargo test & rtk git status".into())
1857 );
1858 }
1859
1860 #[test]
1863 fn test_rewrite_head_numeric_flag() {
1864 assert_eq!(
1866 rewrite_command_no_prefixes("head -20 src/main.rs", &[]),
1867 Some("rtk read src/main.rs --max-lines 20".into())
1868 );
1869 }
1870
1871 #[test]
1872 fn test_rewrite_head_lines_long_flag() {
1873 assert_eq!(
1874 rewrite_command_no_prefixes("head --lines=50 src/lib.rs", &[]),
1875 Some("rtk read src/lib.rs --max-lines 50".into())
1876 );
1877 }
1878
1879 #[test]
1880 fn test_rewrite_head_no_flag_still_rewrites() {
1881 assert_eq!(
1883 rewrite_command_no_prefixes("head src/main.rs", &[]),
1884 Some("rtk read src/main.rs".into())
1885 );
1886 }
1887
1888 #[test]
1889 fn test_rewrite_head_other_flag_skipped() {
1890 assert_eq!(
1892 rewrite_command_no_prefixes("head -c 100 src/main.rs", &[]),
1893 None
1894 );
1895 }
1896
1897 #[test]
1898 fn test_rewrite_tail_numeric_flag() {
1899 assert_eq!(
1900 rewrite_command_no_prefixes("tail -20 src/main.rs", &[]),
1901 Some("rtk read src/main.rs --tail-lines 20".into())
1902 );
1903 }
1904
1905 #[test]
1906 fn test_rewrite_tail_n_space_flag() {
1907 assert_eq!(
1908 rewrite_command_no_prefixes("tail -n 12 src/lib.rs", &[]),
1909 Some("rtk read src/lib.rs --tail-lines 12".into())
1910 );
1911 }
1912
1913 #[test]
1914 fn test_rewrite_tail_lines_long_flag() {
1915 assert_eq!(
1916 rewrite_command_no_prefixes("tail --lines=7 src/lib.rs", &[]),
1917 Some("rtk read src/lib.rs --tail-lines 7".into())
1918 );
1919 }
1920
1921 #[test]
1922 fn test_rewrite_tail_lines_space_flag() {
1923 assert_eq!(
1924 rewrite_command_no_prefixes("tail --lines 7 src/lib.rs", &[]),
1925 Some("rtk read src/lib.rs --tail-lines 7".into())
1926 );
1927 }
1928
1929 #[test]
1930 fn test_rewrite_tail_other_flag_skipped() {
1931 assert_eq!(
1932 rewrite_command_no_prefixes("tail -c 100 src/main.rs", &[]),
1933 None
1934 );
1935 }
1936
1937 #[test]
1938 fn test_rewrite_tail_plain_file_skipped() {
1939 assert_eq!(rewrite_command_no_prefixes("tail src/main.rs", &[]), None);
1940 }
1941
1942 #[test]
1952 fn test_rewrite_head_numeric_flag_multi_file_skipped() {
1953 assert_eq!(
1954 rewrite_command_no_prefixes("head -3 /tmp/a /tmp/b /tmp/c", &[]),
1955 None
1956 );
1957 }
1958
1959 #[test]
1960 fn test_rewrite_head_lines_long_flag_multi_file_skipped() {
1961 assert_eq!(
1962 rewrite_command_no_prefixes("head --lines=50 src/main.rs src/lib.rs", &[]),
1963 None
1964 );
1965 }
1966
1967 #[test]
1968 fn test_rewrite_tail_numeric_flag_multi_file_skipped() {
1969 assert_eq!(
1970 rewrite_command_no_prefixes("tail -20 a.log b.log", &[]),
1971 None
1972 );
1973 }
1974
1975 #[test]
1976 fn test_rewrite_tail_n_space_flag_multi_file_skipped() {
1977 assert_eq!(
1978 rewrite_command_no_prefixes("tail -n 12 a.log b.log c.log", &[]),
1979 None
1980 );
1981 }
1982
1983 #[test]
1984 fn test_rewrite_tail_lines_eq_multi_file_skipped() {
1985 assert_eq!(
1986 rewrite_command_no_prefixes("tail --lines=7 a.log b.log", &[]),
1987 None
1988 );
1989 }
1990
1991 #[test]
1992 fn test_rewrite_tail_lines_space_multi_file_skipped() {
1993 assert_eq!(
1994 rewrite_command_no_prefixes("tail --lines 7 a.log b.log", &[]),
1995 None
1996 );
1997 }
1998
1999 #[test]
2002 fn test_classify_gh_release() {
2003 assert!(matches!(
2004 classify_command("gh release list"),
2005 Classification::Supported {
2006 rtk_equivalent: "rtk gh",
2007 ..
2008 }
2009 ));
2010 }
2011
2012 #[test]
2013 fn test_classify_glab_mr() {
2014 assert!(matches!(
2015 classify_command("glab mr list"),
2016 Classification::Supported {
2017 rtk_equivalent: "rtk glab",
2018 ..
2019 }
2020 ));
2021 }
2022
2023 #[test]
2024 fn test_classify_glab_ci() {
2025 assert!(matches!(
2026 classify_command("glab ci list"),
2027 Classification::Supported {
2028 rtk_equivalent: "rtk glab",
2029 ..
2030 }
2031 ));
2032 }
2033
2034 #[test]
2035 fn test_classify_glab_release() {
2036 assert!(matches!(
2037 classify_command("glab release list"),
2038 Classification::Supported {
2039 rtk_equivalent: "rtk glab",
2040 ..
2041 }
2042 ));
2043 }
2044
2045 #[test]
2046 fn test_rewrite_glab_mr_list() {
2047 assert_eq!(
2048 rewrite_command_no_prefixes("glab mr list", &[]),
2049 Some("rtk glab mr list".into())
2050 );
2051 }
2052
2053 #[test]
2054 fn test_rewrite_glab_ci_status() {
2055 assert_eq!(
2056 rewrite_command_no_prefixes("glab ci status", &[]),
2057 Some("rtk glab ci status".into())
2058 );
2059 }
2060
2061 #[test]
2062 fn test_classify_cargo_install() {
2063 assert!(matches!(
2064 classify_command("cargo install rtk"),
2065 Classification::Supported {
2066 rtk_equivalent: "rtk cargo",
2067 ..
2068 }
2069 ));
2070 }
2071
2072 #[test]
2073 fn test_classify_docker_run() {
2074 assert!(matches!(
2075 classify_command("docker run --rm ubuntu bash"),
2076 Classification::Supported {
2077 rtk_equivalent: "rtk docker",
2078 ..
2079 }
2080 ));
2081 }
2082
2083 #[test]
2084 fn test_classify_docker_exec() {
2085 assert!(matches!(
2086 classify_command("docker exec -it mycontainer bash"),
2087 Classification::Supported {
2088 rtk_equivalent: "rtk docker",
2089 ..
2090 }
2091 ));
2092 }
2093
2094 #[test]
2095 fn test_classify_docker_build() {
2096 assert!(matches!(
2097 classify_command("docker build -t myimage ."),
2098 Classification::Supported {
2099 rtk_equivalent: "rtk docker",
2100 ..
2101 }
2102 ));
2103 }
2104
2105 #[test]
2106 fn test_classify_kubectl_describe() {
2107 assert!(matches!(
2108 classify_command("kubectl describe pod mypod"),
2109 Classification::Supported {
2110 rtk_equivalent: "rtk kubectl",
2111 ..
2112 }
2113 ));
2114 }
2115
2116 #[test]
2117 fn test_classify_kubectl_apply() {
2118 assert!(matches!(
2119 classify_command("kubectl apply -f deploy.yaml"),
2120 Classification::Supported {
2121 rtk_equivalent: "rtk kubectl",
2122 ..
2123 }
2124 ));
2125 }
2126
2127 #[test]
2128 fn test_classify_tree() {
2129 assert!(matches!(
2130 classify_command("tree src/"),
2131 Classification::Supported {
2132 rtk_equivalent: "rtk tree",
2133 ..
2134 }
2135 ));
2136 }
2137
2138 #[test]
2139 fn test_classify_diff() {
2140 assert!(matches!(
2141 classify_command("diff file1.txt file2.txt"),
2142 Classification::Supported {
2143 rtk_equivalent: "rtk diff",
2144 ..
2145 }
2146 ));
2147 }
2148
2149 #[test]
2150 fn test_rewrite_tree() {
2151 assert_eq!(
2152 rewrite_command_no_prefixes("tree src/", &[]),
2153 Some("rtk tree src/".into())
2154 );
2155 }
2156
2157 #[test]
2158 fn test_rewrite_diff() {
2159 assert_eq!(
2160 rewrite_command_no_prefixes("diff file1.txt file2.txt", &[]),
2161 Some("rtk diff file1.txt file2.txt".into())
2162 );
2163 }
2164
2165 #[test]
2166 fn test_rewrite_gh_release() {
2167 assert_eq!(
2168 rewrite_command_no_prefixes("gh release list", &[]),
2169 Some("rtk gh release list".into())
2170 );
2171 }
2172
2173 #[test]
2174 fn test_rewrite_cargo_install() {
2175 assert_eq!(
2176 rewrite_command_no_prefixes("cargo install rtk", &[]),
2177 Some("rtk cargo install rtk".into())
2178 );
2179 }
2180
2181 #[test]
2182 fn test_rewrite_kubectl_describe() {
2183 assert_eq!(
2184 rewrite_command_no_prefixes("kubectl describe pod mypod", &[]),
2185 Some("rtk kubectl describe pod mypod".into())
2186 );
2187 }
2188
2189 #[test]
2190 fn test_rewrite_docker_run() {
2191 assert_eq!(
2192 rewrite_command_no_prefixes("docker run --rm ubuntu bash", &[]),
2193 Some("rtk docker run --rm ubuntu bash".into())
2194 );
2195 }
2196
2197 #[test]
2198 fn test_classify_swift_test() {
2199 assert!(matches!(
2200 classify_command("swift test"),
2201 Classification::Supported {
2202 rtk_equivalent: "rtk swift",
2203 category: "Build",
2204 estimated_savings_pct: 90.0,
2205 status: RtkStatus::Existing,
2206 }
2207 ));
2208 }
2209
2210 #[test]
2211 fn test_rewrite_swift_test() {
2212 assert_eq!(
2213 rewrite_command_no_prefixes("swift test --parallel", &[]),
2214 Some("rtk swift test --parallel".into())
2215 );
2216 }
2217
2218 #[test]
2221 fn test_rewrite_docker_compose_ps() {
2222 assert_eq!(
2223 rewrite_command_no_prefixes("docker compose ps", &[]),
2224 Some("rtk docker compose ps".into())
2225 );
2226 }
2227
2228 #[test]
2229 fn test_rewrite_docker_compose_logs() {
2230 assert_eq!(
2231 rewrite_command_no_prefixes("docker compose logs web", &[]),
2232 Some("rtk docker compose logs web".into())
2233 );
2234 }
2235
2236 #[test]
2237 fn test_rewrite_docker_compose_build() {
2238 assert_eq!(
2239 rewrite_command_no_prefixes("docker compose build", &[]),
2240 Some("rtk docker compose build".into())
2241 );
2242 }
2243
2244 #[test]
2245 fn test_rewrite_docker_compose_up_skipped() {
2246 assert_eq!(
2247 rewrite_command_no_prefixes("docker compose up -d", &[]),
2248 None
2249 );
2250 }
2251
2252 #[test]
2253 fn test_rewrite_docker_compose_down_skipped() {
2254 assert_eq!(
2255 rewrite_command_no_prefixes("docker compose down", &[]),
2256 None
2257 );
2258 }
2259
2260 #[test]
2261 fn test_rewrite_docker_compose_config_skipped() {
2262 assert_eq!(
2263 rewrite_command_no_prefixes("docker compose -f foo.yaml config --services", &[]),
2264 None
2265 );
2266 }
2267
2268 #[test]
2271 fn test_classify_aws() {
2272 assert!(matches!(
2273 classify_command("aws s3 ls"),
2274 Classification::Supported {
2275 rtk_equivalent: "rtk aws",
2276 ..
2277 }
2278 ));
2279 }
2280
2281 #[test]
2282 fn test_classify_aws_ec2() {
2283 assert!(matches!(
2284 classify_command("aws ec2 describe-instances"),
2285 Classification::Supported {
2286 rtk_equivalent: "rtk aws",
2287 ..
2288 }
2289 ));
2290 }
2291
2292 #[test]
2293 fn test_classify_psql() {
2294 assert!(matches!(
2295 classify_command("psql -U postgres"),
2296 Classification::Supported {
2297 rtk_equivalent: "rtk psql",
2298 ..
2299 }
2300 ));
2301 }
2302
2303 #[test]
2304 fn test_classify_psql_url() {
2305 assert!(matches!(
2306 classify_command("psql postgres://localhost/mydb"),
2307 Classification::Supported {
2308 rtk_equivalent: "rtk psql",
2309 ..
2310 }
2311 ));
2312 }
2313
2314 #[test]
2315 fn test_rewrite_aws() {
2316 assert_eq!(
2317 rewrite_command_no_prefixes("aws s3 ls", &[]),
2318 Some("rtk aws s3 ls".into())
2319 );
2320 }
2321
2322 #[test]
2323 fn test_rewrite_aws_ec2() {
2324 assert_eq!(
2325 rewrite_command_no_prefixes("aws ec2 describe-instances --region us-east-1", &[]),
2326 Some("rtk aws ec2 describe-instances --region us-east-1".into())
2327 );
2328 }
2329
2330 #[test]
2331 fn test_rewrite_psql() {
2332 assert_eq!(
2333 rewrite_command_no_prefixes("psql -U postgres -d mydb", &[]),
2334 Some("rtk psql -U postgres -d mydb".into())
2335 );
2336 }
2337
2338 #[test]
2341 fn test_classify_ruff_check() {
2342 assert!(matches!(
2343 classify_command("ruff check ."),
2344 Classification::Supported {
2345 rtk_equivalent: "rtk ruff",
2346 ..
2347 }
2348 ));
2349 }
2350
2351 #[test]
2352 fn test_classify_ruff_format() {
2353 assert!(matches!(
2354 classify_command("ruff format src/"),
2355 Classification::Supported {
2356 rtk_equivalent: "rtk ruff",
2357 ..
2358 }
2359 ));
2360 }
2361
2362 #[test]
2363 fn test_classify_pytest() {
2364 assert!(matches!(
2365 classify_command("pytest tests/"),
2366 Classification::Supported {
2367 rtk_equivalent: "rtk pytest",
2368 ..
2369 }
2370 ));
2371 }
2372
2373 #[test]
2374 fn test_classify_python_m_pytest() {
2375 assert!(matches!(
2376 classify_command("python -m pytest tests/"),
2377 Classification::Supported {
2378 rtk_equivalent: "rtk pytest",
2379 ..
2380 }
2381 ));
2382 }
2383
2384 #[test]
2385 fn test_classify_pip_list() {
2386 assert!(matches!(
2387 classify_command("pip list"),
2388 Classification::Supported {
2389 rtk_equivalent: "rtk pip",
2390 ..
2391 }
2392 ));
2393 }
2394
2395 #[test]
2396 fn test_classify_uv_pip_list() {
2397 assert!(matches!(
2398 classify_command("uv pip list"),
2399 Classification::Supported {
2400 rtk_equivalent: "rtk pip",
2401 ..
2402 }
2403 ));
2404 }
2405
2406 #[test]
2407 fn test_rewrite_ruff_check() {
2408 assert_eq!(
2409 rewrite_command_no_prefixes("ruff check .", &[]),
2410 Some("rtk ruff check .".into())
2411 );
2412 }
2413
2414 #[test]
2415 fn test_rewrite_ruff_format() {
2416 assert_eq!(
2417 rewrite_command_no_prefixes("ruff format src/", &[]),
2418 Some("rtk ruff format src/".into())
2419 );
2420 }
2421
2422 #[test]
2423 fn test_rewrite_pytest() {
2424 assert_eq!(
2425 rewrite_command_no_prefixes("pytest tests/", &[]),
2426 Some("rtk pytest tests/".into())
2427 );
2428 }
2429
2430 #[test]
2431 fn test_rewrite_python_m_pytest() {
2432 assert_eq!(
2433 rewrite_command_no_prefixes("python -m pytest -x tests/", &[]),
2434 Some("rtk pytest -x tests/".into())
2435 );
2436 }
2437
2438 #[test]
2439 fn test_rewrite_pip_list() {
2440 assert_eq!(
2441 rewrite_command_no_prefixes("pip list", &[]),
2442 Some("rtk pip list".into())
2443 );
2444 }
2445
2446 #[test]
2447 fn test_rewrite_pip_outdated() {
2448 assert_eq!(
2449 rewrite_command_no_prefixes("pip outdated", &[]),
2450 Some("rtk pip outdated".into())
2451 );
2452 }
2453
2454 #[test]
2455 fn test_rewrite_uv_pip_list() {
2456 assert_eq!(
2457 rewrite_command_no_prefixes("uv pip list", &[]),
2458 Some("rtk pip list".into())
2459 );
2460 }
2461
2462 #[test]
2465 fn test_classify_go_test() {
2466 assert!(matches!(
2467 classify_command("go test ./..."),
2468 Classification::Supported {
2469 rtk_equivalent: "rtk go",
2470 ..
2471 }
2472 ));
2473 }
2474
2475 #[test]
2476 fn test_classify_go_build() {
2477 assert!(matches!(
2478 classify_command("go build ./..."),
2479 Classification::Supported {
2480 rtk_equivalent: "rtk go",
2481 ..
2482 }
2483 ));
2484 }
2485
2486 #[test]
2487 fn test_classify_go_vet() {
2488 assert!(matches!(
2489 classify_command("go vet ./..."),
2490 Classification::Supported {
2491 rtk_equivalent: "rtk go",
2492 ..
2493 }
2494 ));
2495 }
2496
2497 #[test]
2498 fn test_classify_golangci_lint() {
2499 assert!(matches!(
2500 classify_command("golangci-lint run"),
2501 Classification::Supported {
2502 rtk_equivalent: "rtk golangci-lint run",
2503 ..
2504 }
2505 ));
2506 }
2507
2508 #[test]
2509 fn test_classify_golangci_lint_with_flag_before_run() {
2510 assert!(matches!(
2511 classify_command("golangci-lint -v run ./..."),
2512 Classification::Supported {
2513 rtk_equivalent: "rtk golangci-lint run",
2514 ..
2515 }
2516 ));
2517 }
2518
2519 #[test]
2520 fn test_classify_golangci_lint_with_value_flag_before_run() {
2521 assert!(matches!(
2522 classify_command("golangci-lint --color never run ./..."),
2523 Classification::Supported {
2524 rtk_equivalent: "rtk golangci-lint run",
2525 ..
2526 }
2527 ));
2528 }
2529
2530 #[test]
2531 fn test_classify_golangci_lint_with_inline_value_flag_before_run() {
2532 assert!(matches!(
2533 classify_command("golangci-lint --color=never run ./..."),
2534 Classification::Supported {
2535 rtk_equivalent: "rtk golangci-lint run",
2536 ..
2537 }
2538 ));
2539 }
2540
2541 #[test]
2542 fn test_classify_golangci_lint_with_inline_config_flag_before_run() {
2543 assert!(matches!(
2544 classify_command("golangci-lint --config=foo.yml run ./..."),
2545 Classification::Supported {
2546 rtk_equivalent: "rtk golangci-lint run",
2547 ..
2548 }
2549 ));
2550 }
2551
2552 #[test]
2553 fn test_classify_golangci_lint_bare_is_not_compact_wrapper() {
2554 assert!(!matches!(
2555 classify_command("golangci-lint"),
2556 Classification::Supported {
2557 rtk_equivalent: "rtk golangci-lint run",
2558 ..
2559 }
2560 ));
2561 }
2562
2563 #[test]
2564 fn test_classify_golangci_lint_other_subcommand_is_not_compact_wrapper() {
2565 assert!(!matches!(
2566 classify_command("golangci-lint version"),
2567 Classification::Supported {
2568 rtk_equivalent: "rtk golangci-lint run",
2569 ..
2570 }
2571 ));
2572 }
2573
2574 #[test]
2575 fn test_rewrite_go_test() {
2576 assert_eq!(
2577 rewrite_command_no_prefixes("go test ./...", &[]),
2578 Some("rtk go test ./...".into())
2579 );
2580 }
2581
2582 #[test]
2583 fn test_rewrite_go_build() {
2584 assert_eq!(
2585 rewrite_command_no_prefixes("go build ./...", &[]),
2586 Some("rtk go build ./...".into())
2587 );
2588 }
2589
2590 #[test]
2591 fn test_rewrite_go_vet() {
2592 assert_eq!(
2593 rewrite_command_no_prefixes("go vet ./...", &[]),
2594 Some("rtk go vet ./...".into())
2595 );
2596 }
2597
2598 #[test]
2599 fn test_rewrite_golangci_lint() {
2600 assert_eq!(
2601 rewrite_command_no_prefixes("golangci-lint run ./...", &[]),
2602 Some("rtk golangci-lint run ./...".into())
2603 );
2604 }
2605
2606 #[test]
2607 fn test_rewrite_golangci_lint_with_flag_before_run() {
2608 assert_eq!(
2609 rewrite_command_no_prefixes("golangci-lint -v run ./...", &[]),
2610 Some("rtk golangci-lint -v run ./...".into())
2611 );
2612 }
2613
2614 #[test]
2615 fn test_rewrite_golangci_lint_with_value_flag_before_run() {
2616 assert_eq!(
2617 rewrite_command_no_prefixes("golangci-lint --color never run ./...", &[]),
2618 Some("rtk golangci-lint --color never run ./...".into())
2619 );
2620 }
2621
2622 #[test]
2623 fn test_rewrite_golangci_lint_with_inline_value_flag_before_run() {
2624 assert_eq!(
2625 rewrite_command_no_prefixes("golangci-lint --color=never run ./...", &[]),
2626 Some("rtk golangci-lint --color=never run ./...".into())
2627 );
2628 }
2629
2630 #[test]
2631 fn test_rewrite_golangci_lint_with_inline_config_flag_before_run() {
2632 assert_eq!(
2633 rewrite_command_no_prefixes("golangci-lint --config=foo.yml run ./...", &[]),
2634 Some("rtk golangci-lint --config=foo.yml run ./...".into())
2635 );
2636 }
2637
2638 #[test]
2639 fn test_rewrite_env_prefixed_golangci_lint_with_value_flag_before_run() {
2640 assert_eq!(
2641 rewrite_command_no_prefixes("FOO=1 golangci-lint --color never run ./...", &[]),
2642 Some("FOO=1 rtk golangci-lint --color never run ./...".into())
2643 );
2644 }
2645
2646 #[test]
2647 fn test_rewrite_env_prefixed_golangci_lint_with_inline_value_flag_before_run() {
2648 assert_eq!(
2649 rewrite_command_no_prefixes("FOO=1 golangci-lint --color=never run ./...", &[]),
2650 Some("FOO=1 rtk golangci-lint --color=never run ./...".into())
2651 );
2652 }
2653
2654 #[test]
2655 fn test_rewrite_bare_golangci_lint_skips_compact_wrapper() {
2656 assert_eq!(rewrite_command_no_prefixes("golangci-lint", &[]), None);
2657 }
2658
2659 #[test]
2660 fn test_rewrite_other_golangci_lint_subcommand_skips_compact_wrapper() {
2661 assert_eq!(
2662 rewrite_command_no_prefixes("golangci-lint version", &[]),
2663 None
2664 );
2665 }
2666
2667 #[test]
2670 fn test_classify_lint() {
2671 let commands = vec![
2672 "npm exec biome",
2673 "npm exec eslint",
2674 "npm rum biome",
2675 "npm rum eslint",
2676 "npm rum lint",
2677 "npm run biome",
2678 "npm run eslint",
2679 "npm run lint",
2680 "npm run-script biome",
2681 "npm run-script eslint",
2682 "npm run-script lint",
2683 "npm urn biome",
2684 "npm urn eslint",
2685 "npm urn lint",
2686 "npm x biome",
2687 "npm x eslint",
2688 "pnpm dlx biome",
2689 "pnpm dlx eslint",
2690 "pnpm exec biome",
2691 "pnpm exec eslint",
2692 "pnpm run biome",
2693 "pnpm run eslint",
2694 "pnpm run lint",
2695 "pnpm run-script biome",
2696 "pnpm run-script eslint",
2697 "pnpm run-script lint",
2698 "npm biome",
2699 "npm eslint",
2700 "npm lint",
2701 "npx biome",
2702 "npx eslint",
2703 "npx lint",
2704 "pnpm biome",
2705 "pnpm eslint",
2706 "pnpm lint",
2707 "pnpx biome",
2708 "pnpx eslint",
2709 "pnpx lint",
2710 "biome",
2711 "eslint",
2712 "lint",
2713 ];
2714 for command in commands {
2715 assert!(
2716 matches!(
2717 classify_command(command),
2718 Classification::Supported {
2719 rtk_equivalent: "rtk lint",
2720 ..
2721 }
2722 ),
2723 "Failed for command: {}",
2724 command
2725 );
2726 }
2727 }
2728
2729 #[test]
2730 fn test_rewrite_lint() {
2731 let commands = vec![
2732 "npm exec biome",
2733 "npm exec eslint",
2734 "npm rum biome",
2735 "npm rum eslint",
2736 "npm rum lint",
2737 "npm run biome",
2738 "npm run eslint",
2739 "npm run lint",
2740 "npm run-script biome",
2741 "npm run-script eslint",
2742 "npm run-script lint",
2743 "npm urn biome",
2744 "npm urn eslint",
2745 "npm urn lint",
2746 "npm x biome",
2747 "npm x eslint",
2748 "pnpm dlx biome",
2749 "pnpm dlx eslint",
2750 "pnpm exec biome",
2751 "pnpm exec eslint",
2752 "pnpm run biome",
2753 "pnpm run eslint",
2754 "pnpm run lint",
2755 "pnpm run-script biome",
2756 "pnpm run-script eslint",
2757 "pnpm run-script lint",
2758 "npm biome",
2759 "npm eslint",
2760 "npm lint",
2761 "npx biome",
2762 "npx eslint",
2763 "npx lint",
2764 "pnpm biome",
2765 "pnpm eslint",
2766 "pnpm lint",
2767 "pnpx biome",
2768 "pnpx eslint",
2769 "pnpx lint",
2770 "biome",
2771 "eslint",
2772 "lint",
2773 ];
2774 for command in commands {
2775 assert_eq!(
2776 rewrite_command_no_prefixes(command, &[]),
2777 Some("rtk lint".into()),
2778 "Failed for command: {}",
2779 command
2780 );
2781 }
2782 }
2783
2784 #[test]
2785 fn test_classify_jest() {
2786 let commands = vec![
2787 "jest run",
2788 "jest",
2789 "npm exec jest run",
2790 "npm exec jest",
2791 "npm jest run",
2792 "npm jest",
2793 "npm rum jest run",
2794 "npm rum jest",
2795 "npm run jest run",
2796 "npm run jest",
2797 "npm run-script jest run",
2798 "npm run-script jest",
2799 "npm urn jest run",
2800 "npm urn jest",
2801 "npm x jest run",
2802 "npm x jest",
2803 "npx jest run",
2804 "npx jest",
2805 "pnpm dlx jest run",
2806 "pnpm dlx jest",
2807 "pnpm exec jest run",
2808 "pnpm exec jest",
2809 "pnpm jest run",
2810 "pnpm jest",
2811 "pnpm run jest run",
2812 "pnpm run jest",
2813 "pnpm run-script jest run",
2814 "pnpm run-script jest",
2815 "pnpx jest run",
2816 "pnpx jest",
2817 ];
2818 for command in commands {
2819 assert!(
2820 matches!(
2821 classify_command(command),
2822 Classification::Supported {
2823 rtk_equivalent: "rtk jest",
2824 ..
2825 }
2826 ),
2827 "Failed for command: {}",
2828 command
2829 );
2830 }
2831 }
2832
2833 #[test]
2834 fn test_rewrite_jest() {
2835 let commands = vec![
2836 "jest run",
2837 "jest",
2838 "npm exec jest run",
2839 "npm exec jest",
2840 "npm jest run",
2841 "npm jest",
2842 "npm rum jest run",
2843 "npm rum jest",
2844 "npm run jest run",
2845 "npm run jest",
2846 "npm run-script jest run",
2847 "npm run-script jest",
2848 "npm urn jest run",
2849 "npm urn jest",
2850 "npm x jest run",
2851 "npm x jest",
2852 "npx jest run",
2853 "npx jest",
2854 "pnpm dlx jest run",
2855 "pnpm dlx jest",
2856 "pnpm exec jest run",
2857 "pnpm exec jest",
2858 "pnpm jest run",
2859 "pnpm jest",
2860 "pnpm run jest run",
2861 "pnpm run jest",
2862 "pnpm run-script jest run",
2863 "pnpm run-script jest",
2864 "pnpx jest run",
2865 "pnpx jest",
2866 ];
2867 for command in commands {
2868 assert_eq!(
2869 rewrite_command_no_prefixes(command, &[]),
2870 Some("rtk jest".into()),
2871 "Failed for command: {}",
2872 command
2873 );
2874 }
2875 }
2876
2877 #[test]
2878 fn test_classify_vitest() {
2879 let commands = vec![
2880 "npm exec vitest run",
2881 "npm exec vitest",
2882 "npm rum vitest run",
2883 "npm rum vitest",
2884 "npm run vitest run",
2885 "npm run vitest",
2886 "npm run-script vitest run",
2887 "npm run-script vitest",
2888 "npm urn vitest run",
2889 "npm urn vitest",
2890 "npm vitest run",
2891 "npm vitest",
2892 "npm x vitest run",
2893 "npm x vitest",
2894 "npx vitest run",
2895 "npx vitest",
2896 "pnpm dlx vitest run",
2897 "pnpm dlx vitest",
2898 "pnpm exec vitest run",
2899 "pnpm exec vitest",
2900 "pnpm run vitest run",
2901 "pnpm run vitest",
2902 "pnpm run-script vitest run",
2903 "pnpm run-script vitest",
2904 "pnpm vitest run",
2905 "pnpm vitest",
2906 "pnpx vitest run",
2907 "pnpx vitest",
2908 "vitest run",
2909 "vitest",
2910 ];
2911 for command in commands {
2912 assert!(
2913 matches!(
2914 classify_command(command),
2915 Classification::Supported {
2916 rtk_equivalent: "rtk vitest",
2917 ..
2918 }
2919 ),
2920 "Failed for command: {}",
2921 command
2922 );
2923 }
2924 }
2925
2926 #[test]
2927 fn test_rewrite_vitest() {
2928 let commands = vec![
2929 "npm exec vitest run",
2930 "npm exec vitest",
2931 "npm rum vitest run",
2932 "npm rum vitest",
2933 "npm run vitest run",
2934 "npm run vitest",
2935 "npm run-script vitest run",
2936 "npm run-script vitest",
2937 "npm urn vitest run",
2938 "npm urn vitest",
2939 "npm vitest run",
2940 "npm vitest",
2941 "npm x vitest run",
2942 "npm x vitest",
2943 "npx vitest run",
2944 "npx vitest",
2945 "pnpm dlx vitest run",
2946 "pnpm dlx vitest",
2947 "pnpm exec vitest run",
2948 "pnpm exec vitest",
2949 "pnpm run vitest run",
2950 "pnpm run vitest",
2951 "pnpm run-script vitest run",
2952 "pnpm run-script vitest",
2953 "pnpm vitest run",
2954 "pnpm vitest",
2955 "pnpx vitest run",
2956 "pnpx vitest",
2957 "vitest run",
2958 "vitest",
2959 ];
2960 for command in commands {
2961 assert_eq!(
2962 rewrite_command_no_prefixes(command, &[]),
2963 Some("rtk vitest".into()),
2964 "Failed for command: {}",
2965 command
2966 );
2967 }
2968 }
2969
2970 #[test]
2971 fn test_classify_prisma() {
2972 let commands = vec![
2973 "npm exec prisma",
2974 "npm rum prisma",
2975 "npm run prisma",
2976 "npm run-script prisma",
2977 "npm urn prisma",
2978 "npm x prisma",
2979 "pnpm dlx prisma",
2980 "pnpm exec prisma",
2981 "pnpm run prisma",
2982 "pnpm run-script prisma",
2983 "npm prisma",
2984 "npx prisma",
2985 "pnpm prisma",
2986 "pnpx prisma",
2987 "prisma",
2988 ];
2989 for command in commands {
2990 assert!(
2991 matches!(
2992 classify_command(format!("{command} migrate dev").as_str()),
2993 Classification::Supported {
2994 rtk_equivalent: "rtk prisma",
2995 ..
2996 }
2997 ),
2998 "Failed for command: {}",
2999 command
3000 );
3001 }
3002 }
3003
3004 #[test]
3005 fn test_rewrite_prisma() {
3006 let commands = vec![
3007 "npm exec prisma",
3008 "npm rum prisma",
3009 "npm run prisma",
3010 "npm run-script prisma",
3011 "npm urn prisma",
3012 "npm x prisma",
3013 "pnpm dlx prisma",
3014 "pnpm exec prisma",
3015 "pnpm run prisma",
3016 "pnpm run-script prisma",
3017 "npm prisma",
3018 "npx prisma",
3019 "pnpm prisma",
3020 "pnpx prisma",
3021 "prisma",
3022 ];
3023 for command in commands {
3024 assert_eq!(
3025 rewrite_command_no_prefixes(format!("{command} migrate dev").as_str(), &[]),
3026 Some("rtk prisma migrate dev".into()),
3027 "Failed for command: {}",
3028 command
3029 );
3030 }
3031 }
3032
3033 #[test]
3034 fn test_rewrite_prettier() {
3035 let commands = vec![
3036 "npm exec prettier",
3037 "npm rum prettier",
3038 "npm run prettier",
3039 "npm run-script prettier",
3040 "npm urn prettier",
3041 "npm x prettier",
3042 "pnpm dlx prettier",
3043 "pnpm exec prettier",
3044 "pnpm run prettier",
3045 "pnpm run-script prettier",
3046 "npm prettier",
3047 "npx prettier",
3048 "pnpm prettier",
3049 "pnpx prettier",
3050 "prettier",
3051 ];
3052 for command in commands {
3053 assert_eq!(
3054 rewrite_command_no_prefixes(format!("{command} --check src/").as_str(), &[]),
3055 Some("rtk prettier --check src/".into()),
3056 "Failed for command: {}",
3057 command
3058 );
3059 }
3060 }
3061
3062 #[test]
3063 fn test_rewrite_pnpm_command() {
3064 let commands = vec![
3065 "exec",
3066 "i",
3067 "install",
3068 "list",
3069 "ls",
3070 "outdated",
3071 "run",
3072 "run-script",
3073 ];
3074 for command in commands {
3075 assert_eq!(
3076 rewrite_command_no_prefixes(format!("pnpm {command}").as_str(), &[]),
3077 Some(format!("rtk pnpm {command}")),
3078 "Failed for command: pnpm {}",
3079 command
3080 );
3081 }
3082 }
3083
3084 #[test]
3085 fn test_rewrite_npm_bare_subcommand() {
3086 let commands = vec!["exec", "run", "run-script", "x"];
3087 for command in commands {
3088 assert_eq!(
3089 rewrite_command_no_prefixes(format!("npm {command}").as_str(), &[]),
3090 Some(format!("rtk npm {command}")),
3091 "Failed for bare command: npm {}",
3092 command
3093 );
3094 }
3095 }
3096
3097 #[test]
3098 fn test_rewrite_npm_with_args() {
3099 assert_eq!(
3100 rewrite_command_no_prefixes("npm run test", &[]),
3101 Some("rtk npm run test".to_string()),
3102 );
3103 assert_eq!(
3104 rewrite_command_no_prefixes("npm exec vitest", &[]),
3105 Some("rtk vitest".to_string()),
3106 );
3107 }
3108
3109 #[test]
3110 fn test_rewrite_npx() {
3111 assert_eq!(
3112 rewrite_command_no_prefixes("npx svgo", &[]),
3113 Some("rtk npx svgo".to_string()),
3114 );
3115 }
3116
3117 #[test]
3120 fn test_classify_gradlew() {
3121 assert!(matches!(
3122 classify_command("./gradlew assembleDebug"),
3123 Classification::Supported {
3124 rtk_equivalent: "rtk gradlew",
3125 ..
3126 }
3127 ));
3128 }
3129
3130 #[test]
3131 fn test_classify_gradlew_no_dot_slash() {
3132 assert!(matches!(
3133 classify_command("gradlew build"),
3134 Classification::Supported {
3135 rtk_equivalent: "rtk gradlew",
3136 ..
3137 }
3138 ));
3139 }
3140
3141 #[test]
3142 fn test_classify_gradlew_bat() {
3143 assert!(matches!(
3144 classify_command("gradlew.bat clean"),
3145 Classification::Supported {
3146 rtk_equivalent: "rtk gradlew",
3147 ..
3148 }
3149 ));
3150 }
3151
3152 #[test]
3153 fn test_classify_gradle() {
3154 assert!(matches!(
3155 classify_command("gradle build"),
3156 Classification::Supported {
3157 rtk_equivalent: "rtk gradlew",
3158 ..
3159 }
3160 ));
3161 }
3162
3163 #[test]
3164 fn test_rewrite_gradlew() {
3165 assert_eq!(
3166 rewrite_command_no_prefixes("./gradlew assembleDebug", &[]),
3167 Some("rtk gradlew assembleDebug".into())
3168 );
3169 }
3170
3171 #[test]
3172 fn test_rewrite_gradlew_no_dot_slash() {
3173 assert_eq!(
3174 rewrite_command_no_prefixes("gradlew build", &[]),
3175 Some("rtk gradlew build".into())
3176 );
3177 }
3178
3179 #[test]
3180 fn test_rewrite_gradlew_bat() {
3181 assert_eq!(
3182 rewrite_command_no_prefixes("gradlew.bat clean", &[]),
3183 Some("rtk gradlew clean".into())
3184 );
3185 }
3186
3187 #[test]
3188 fn test_rewrite_gradle() {
3189 assert_eq!(
3190 rewrite_command_no_prefixes("gradle build", &[]),
3191 Some("rtk gradlew build".into())
3192 );
3193 }
3194
3195 #[test]
3196 fn test_rewrite_gradlew_test_savings() {
3197 assert_eq!(
3198 classify_command("./gradlew test"),
3199 Classification::Supported {
3200 rtk_equivalent: "rtk gradlew",
3201 category: "Build",
3202 estimated_savings_pct: 90.0,
3203 status: RtkStatus::Existing,
3204 }
3205 );
3206 }
3207
3208 #[test]
3211 fn test_classify_mvn_test() {
3212 assert!(matches!(
3213 classify_command("mvn test"),
3214 Classification::Supported {
3215 rtk_equivalent: "rtk mvn",
3216 ..
3217 }
3218 ));
3219 }
3220
3221 #[test]
3222 fn test_classify_mvn_integration_test() {
3223 assert!(matches!(
3224 classify_command("mvn integration-test"),
3225 Classification::Supported {
3226 rtk_equivalent: "rtk mvn",
3227 ..
3228 }
3229 ));
3230 }
3231
3232 #[test]
3233 fn test_classify_mvn_flags_before_goal() {
3234 assert!(matches!(
3235 classify_command("mvn -B -DskipTests=false clean install"),
3236 Classification::Supported {
3237 rtk_equivalent: "rtk mvn",
3238 ..
3239 }
3240 ));
3241 }
3242
3243 #[test]
3244 fn test_classify_mvnw_wrapper() {
3245 assert!(matches!(
3246 classify_command("./mvnw verify"),
3247 Classification::Supported {
3248 rtk_equivalent: "rtk mvn",
3249 ..
3250 }
3251 ));
3252 }
3253
3254 #[test]
3255 fn test_classify_mvnw_cmd_wrapper() {
3256 assert!(matches!(
3257 classify_command("mvnw.cmd package"),
3258 Classification::Supported {
3259 rtk_equivalent: "rtk mvn",
3260 ..
3261 }
3262 ));
3263 }
3264
3265 #[test]
3266 fn test_classify_mvn_clean_bypassed() {
3267 assert!(!matches!(
3269 classify_command("mvn clean"),
3270 Classification::Supported {
3271 rtk_equivalent: "rtk mvn",
3272 ..
3273 }
3274 ));
3275 }
3276
3277 #[test]
3278 fn test_classify_mvn_site_bypassed() {
3279 assert!(!matches!(
3280 classify_command("mvn site"),
3281 Classification::Supported {
3282 rtk_equivalent: "rtk mvn",
3283 ..
3284 }
3285 ));
3286 }
3287
3288 #[test]
3289 fn test_classify_mvn_plugin_goal_bypassed() {
3290 assert!(!matches!(
3291 classify_command("mvn dependency:tree"),
3292 Classification::Supported {
3293 rtk_equivalent: "rtk mvn",
3294 ..
3295 }
3296 ));
3297 }
3298
3299 #[test]
3300 fn test_classify_mvn_bare_bypassed() {
3301 assert!(!matches!(
3302 classify_command("mvn"),
3303 Classification::Supported {
3304 rtk_equivalent: "rtk mvn",
3305 ..
3306 }
3307 ));
3308 }
3309
3310 #[test]
3311 fn test_classify_mvn_version_bypassed() {
3312 assert!(!matches!(
3313 classify_command("mvn --version"),
3314 Classification::Supported {
3315 rtk_equivalent: "rtk mvn",
3316 ..
3317 }
3318 ));
3319 }
3320
3321 #[test]
3322 fn test_rewrite_mvn_clean_install() {
3323 assert_eq!(
3324 rewrite_command_no_prefixes("mvn -B clean install", &[]),
3325 Some("rtk mvn -B clean install".into())
3326 );
3327 }
3328
3329 #[test]
3330 fn test_rewrite_mvnw_test() {
3331 assert_eq!(
3332 rewrite_command_no_prefixes("./mvnw test", &[]),
3333 Some("rtk mvn test".into())
3334 );
3335 }
3336
3337 #[test]
3340 fn test_rewrite_compound_or() {
3341 assert_eq!(
3343 rewrite_command_no_prefixes("cargo test || cargo build", &[]),
3344 Some("rtk cargo test || rtk cargo build".into())
3345 );
3346 }
3347
3348 #[test]
3349 fn test_rewrite_compound_semicolon() {
3350 assert_eq!(
3351 rewrite_command_no_prefixes("git status; cargo test", &[]),
3352 Some("rtk git status; rtk cargo test".into())
3353 );
3354 }
3355
3356 #[test]
3357 fn test_rewrite_compound_pipe_raw_filter() {
3358 assert_eq!(
3360 rewrite_command_no_prefixes("cargo test | grep FAILED", &[]),
3361 Some("rtk cargo test | grep FAILED".into())
3362 );
3363 }
3364
3365 #[test]
3366 fn test_rewrite_compound_pipe_git_grep() {
3367 assert_eq!(
3368 rewrite_command_no_prefixes("git log -10 | grep feat", &[]),
3369 Some("rtk git log -10 | grep feat".into())
3370 );
3371 }
3372
3373 #[test]
3374 fn test_rewrite_compound_four_segments() {
3375 assert_eq!(
3376 rewrite_command_no_prefixes(
3377 "cargo fmt --all && cargo clippy && cargo test && git status",
3378 &[]
3379 ),
3380 Some(
3381 "rtk cargo fmt --all && rtk cargo clippy && rtk cargo test && rtk git status"
3382 .into()
3383 )
3384 );
3385 }
3386
3387 #[test]
3388 fn test_rewrite_compound_mixed_supported_unsupported() {
3389 assert_eq!(
3391 rewrite_command_no_prefixes("cargo test && htop", &[]),
3392 Some("rtk cargo test && htop".into())
3393 );
3394 }
3395
3396 #[test]
3397 fn test_rewrite_compound_all_unsupported_returns_none() {
3398 assert_eq!(rewrite_command_no_prefixes("htop && top", &[]), None);
3400 }
3401
3402 #[test]
3405 fn test_rewrite_sudo_docker() {
3406 assert_eq!(
3407 rewrite_command_no_prefixes("sudo docker ps", &[]),
3408 Some("sudo rtk docker ps".into())
3409 );
3410 }
3411
3412 #[test]
3413 fn test_rewrite_env_var_prefix() {
3414 assert_eq!(
3415 rewrite_command_no_prefixes("GIT_SSH_COMMAND=ssh git push origin main", &[]),
3416 Some("GIT_SSH_COMMAND=ssh rtk git push origin main".into())
3417 );
3418 }
3419
3420 #[test]
3423 fn test_rewrite_find_with_flags() {
3424 assert_eq!(
3425 rewrite_command_no_prefixes("find . -name '*.rs' -type f", &[]),
3426 Some("rtk find . -name '*.rs' -type f".into())
3427 );
3428 }
3429
3430 #[test]
3431 fn test_all_rules_are_complete() {
3432 for rule in RULES {
3433 assert!(
3434 !rule.pattern.is_empty(),
3435 "Rule '{}' has empty pattern",
3436 rule.rtk_cmd
3437 );
3438 assert!(!rule.rtk_cmd.is_empty(), "Rule with empty rtk_cmd found");
3439 assert!(
3440 rule.rtk_cmd.starts_with("rtk "),
3441 "rtk_cmd '{}' must start with 'rtk '",
3442 rule.rtk_cmd
3443 );
3444 assert!(
3445 !rule.rewrite_prefixes.is_empty(),
3446 "Rule '{}' has no rewrite_prefixes",
3447 rule.rtk_cmd
3448 );
3449 }
3450 }
3451
3452 #[test]
3455 fn test_rewrite_excludes_curl() {
3456 let excluded = vec!["curl".to_string()];
3457 assert_eq!(
3458 rewrite_command_no_prefixes("curl https://api.example.com/health", &excluded),
3459 None
3460 );
3461 }
3462
3463 #[test]
3464 fn test_rewrite_exclude_does_not_affect_other_commands() {
3465 let excluded = vec!["curl".to_string()];
3466 assert_eq!(
3467 rewrite_command_no_prefixes("git status", &excluded),
3468 Some("rtk git status".into())
3469 );
3470 }
3471
3472 #[test]
3473 fn test_rewrite_empty_excludes_rewrites_curl() {
3474 let excluded: Vec<String> = vec![];
3475 assert!(rewrite_command_no_prefixes("curl https://api.example.com", &excluded).is_some());
3476 }
3477
3478 #[test]
3479 fn test_rewrite_compound_partial_exclude() {
3480 let excluded = vec!["curl".to_string()];
3482 assert_eq!(
3483 rewrite_command_no_prefixes("git status && curl https://api.example.com", &excluded),
3484 Some("rtk git status && curl https://api.example.com".into())
3485 );
3486 }
3487
3488 #[test]
3489 fn test_exclude_env_prefixed_command() {
3490 let excluded = vec!["psql".to_string()];
3491 assert_eq!(
3492 rewrite_command_no_prefixes("PGPASSWORD=postgres psql -h localhost", &excluded),
3493 None
3494 );
3495 }
3496
3497 #[test]
3498 fn test_exclude_subcommand_pattern() {
3499 let excluded = vec!["git push".to_string()];
3500 assert_eq!(
3501 rewrite_command_no_prefixes("git push origin main", &excluded),
3502 None
3503 );
3504 }
3505
3506 #[test]
3507 fn test_exclude_regex_pattern() {
3508 let excluded = vec!["^curl".to_string()];
3509 assert_eq!(
3510 rewrite_command_no_prefixes("curl http://example.com", &excluded),
3511 None
3512 );
3513 }
3514
3515 #[test]
3516 fn test_exclude_invalid_regex_fallback() {
3517 let excluded = vec!["curl[".to_string()];
3518 assert!(rewrite_command_no_prefixes("curl http://example.com", &excluded).is_some());
3519 }
3520
3521 #[test]
3522 fn test_exclude_does_not_substring_match() {
3523 let excluded = vec!["go".to_string()];
3524 assert!(rewrite_command_no_prefixes("golangci-lint run ./...", &excluded).is_some());
3525 }
3526
3527 #[test]
3528 fn test_exclude_does_not_match_hyphenated_command() {
3529 let excluded = vec!["golangci".to_string()];
3530 assert!(rewrite_command_no_prefixes("golangci-lint run ./...", &excluded).is_some());
3531 }
3532
3533 #[test]
3534 fn test_exclude_empty_pattern_ignored() {
3535 let excluded = vec!["".to_string()];
3536 assert!(rewrite_command_no_prefixes("git status", &excluded).is_some());
3537 }
3538
3539 #[test]
3540 fn test_exclude_bare_anchor_ignored() {
3541 let excluded = vec!["^".to_string()];
3542 assert!(rewrite_command_no_prefixes("git status", &excluded).is_some());
3543 }
3544
3545 #[test]
3546 fn test_all_patterns_are_valid_regex() {
3547 use regex::Regex;
3548 for (i, rule) in RULES.iter().enumerate() {
3549 assert!(
3550 Regex::new(rule.pattern).is_ok(),
3551 "RULES[{i}] ({}) has invalid pattern '{}'",
3552 rule.rtk_cmd,
3553 rule.pattern
3554 );
3555 }
3556 }
3557
3558 #[test]
3561 fn test_rewrite_gh_json_skipped() {
3562 assert_eq!(
3563 rewrite_command_no_prefixes("gh pr list --json number,title", &[]),
3564 None
3565 );
3566 }
3567
3568 #[test]
3569 fn test_rewrite_gh_jq_skipped() {
3570 assert_eq!(
3571 rewrite_command_no_prefixes("gh pr list --json number --jq '.[].number'", &[]),
3572 None
3573 );
3574 }
3575
3576 #[test]
3577 fn test_rewrite_gh_template_skipped() {
3578 assert_eq!(
3579 rewrite_command_no_prefixes("gh pr view 42 --template '{{.title}}'", &[]),
3580 None
3581 );
3582 }
3583
3584 #[test]
3585 fn test_rewrite_gh_api_json_skipped() {
3586 assert_eq!(
3587 rewrite_command_no_prefixes("gh api repos/owner/repo --jq '.name'", &[]),
3588 None
3589 );
3590 }
3591
3592 #[test]
3593 fn test_rewrite_gh_without_json_still_works() {
3594 assert_eq!(
3595 rewrite_command_no_prefixes("gh pr list", &[]),
3596 Some("rtk gh pr list".into())
3597 );
3598 }
3599
3600 #[test]
3603 fn test_cmd_has_rtk_disabled_prefix() {
3604 assert!(cmd_has_rtk_disabled_prefix("RTK_DISABLED=1 git status"));
3605 assert!(cmd_has_rtk_disabled_prefix(
3606 "FOO=1 RTK_DISABLED=1 cargo test"
3607 ));
3608 assert!(cmd_has_rtk_disabled_prefix(
3609 "RTK_DISABLED=true git log --oneline"
3610 ));
3611 assert!(!cmd_has_rtk_disabled_prefix("git status"));
3612 assert!(!cmd_has_rtk_disabled_prefix("rtk git status"));
3613 assert!(!cmd_has_rtk_disabled_prefix("SOME_VAR=1 git status"));
3614 }
3615
3616 #[test]
3617 fn test_strip_disabled_prefix() {
3618 assert_eq!(
3619 strip_disabled_prefix("RTK_DISABLED=1 git status"),
3620 ("RTK_DISABLED=1 ", "git status")
3621 );
3622 assert_eq!(
3623 strip_disabled_prefix("FOO=1 RTK_DISABLED=1 cargo test"),
3624 ("FOO=1 RTK_DISABLED=1 ", "cargo test")
3625 );
3626 assert_eq!(strip_disabled_prefix("git status"), ("", "git status"));
3627 }
3628
3629 #[test]
3632 fn test_classify_absolute_path_grep() {
3633 assert_eq!(
3634 classify_command("/usr/bin/grep -rni pattern"),
3635 Classification::Supported {
3636 rtk_equivalent: "rtk grep",
3637 category: "Files",
3638 estimated_savings_pct: 75.0,
3639 status: RtkStatus::Existing,
3640 }
3641 );
3642 }
3643
3644 #[test]
3645 fn test_classify_absolute_path_ls() {
3646 assert_eq!(
3647 classify_command("/bin/ls -la"),
3648 Classification::Supported {
3649 rtk_equivalent: "rtk ls",
3650 category: "Files",
3651 estimated_savings_pct: 65.0,
3652 status: RtkStatus::Existing,
3653 }
3654 );
3655 }
3656
3657 #[test]
3658 fn test_classify_absolute_path_git() {
3659 assert_eq!(
3660 classify_command("/usr/local/bin/git status"),
3661 Classification::Supported {
3662 rtk_equivalent: "rtk git",
3663 category: "Git",
3664 estimated_savings_pct: 70.0,
3665 status: RtkStatus::Existing,
3666 }
3667 );
3668 }
3669
3670 #[test]
3671 fn test_classify_absolute_path_no_args() {
3672 assert_eq!(
3674 classify_command("/usr/bin/find ."),
3675 Classification::Supported {
3676 rtk_equivalent: "rtk find",
3677 category: "Files",
3678 estimated_savings_pct: 70.0,
3679 status: RtkStatus::Existing,
3680 }
3681 );
3682 }
3683
3684 #[test]
3685 fn test_strip_absolute_path_helper() {
3686 assert_eq!(strip_absolute_path("/usr/bin/grep -rn foo"), "grep -rn foo");
3687 assert_eq!(strip_absolute_path("/bin/ls -la"), "ls -la");
3688 assert_eq!(strip_absolute_path("grep -rn foo"), "grep -rn foo");
3689 assert_eq!(strip_absolute_path("/usr/local/bin/git"), "git");
3690 }
3691
3692 #[test]
3695 fn test_classify_git_with_dash_c_path() {
3696 assert_eq!(
3697 classify_command("git -C /tmp status"),
3698 Classification::Supported {
3699 rtk_equivalent: "rtk git",
3700 category: "Git",
3701 estimated_savings_pct: 70.0,
3702 status: RtkStatus::Existing,
3703 }
3704 );
3705 }
3706
3707 #[test]
3708 fn test_classify_git_no_pager_log() {
3709 assert_eq!(
3710 classify_command("git --no-pager log -5"),
3711 Classification::Supported {
3712 rtk_equivalent: "rtk git",
3713 category: "Git",
3714 estimated_savings_pct: 70.0,
3715 status: RtkStatus::Existing,
3716 }
3717 );
3718 }
3719
3720 #[test]
3721 fn test_classify_git_git_dir() {
3722 assert_eq!(
3723 classify_command("git --git-dir /tmp/.git status"),
3724 Classification::Supported {
3725 rtk_equivalent: "rtk git",
3726 category: "Git",
3727 estimated_savings_pct: 70.0,
3728 status: RtkStatus::Existing,
3729 }
3730 );
3731 }
3732
3733 #[test]
3734 fn test_rewrite_git_dash_c() {
3735 assert_eq!(
3736 rewrite_command_no_prefixes("git -C /tmp status", &[]),
3737 Some("rtk git -C /tmp status".to_string())
3738 );
3739 }
3740
3741 #[test]
3742 fn test_rewrite_git_no_pager() {
3743 assert_eq!(
3744 rewrite_command_no_prefixes("git --no-pager log -5", &[]),
3745 Some("rtk git --no-pager log -5".to_string())
3746 );
3747 }
3748
3749 #[test]
3750 fn test_strip_git_global_opts_helper() {
3751 assert_eq!(strip_git_global_opts("git -C /tmp status"), "git status");
3752 assert_eq!(strip_git_global_opts("git --no-pager log"), "git log");
3753 assert_eq!(strip_git_global_opts("git status"), "git status");
3754 assert_eq!(strip_git_global_opts("cargo test"), "cargo test");
3755 }
3756
3757 #[test]
3758 fn test_strip_golangci_global_opts_helper() {
3759 assert_eq!(
3760 strip_golangci_global_opts("golangci-lint -v run ./..."),
3761 "golangci-lint run ./..."
3762 );
3763 assert_eq!(
3764 strip_golangci_global_opts("golangci-lint --color never run ./..."),
3765 "golangci-lint run ./..."
3766 );
3767 assert_eq!(
3768 strip_golangci_global_opts("golangci-lint --color=never run ./..."),
3769 "golangci-lint run ./..."
3770 );
3771 assert_eq!(
3772 strip_golangci_global_opts("golangci-lint --config=foo.yml run ./..."),
3773 "golangci-lint run ./..."
3774 );
3775 assert_eq!(
3776 strip_golangci_global_opts("golangci-lint version"),
3777 "golangci-lint version"
3778 );
3779 assert_eq!(strip_golangci_global_opts("cargo test"), "cargo test");
3780 }
3781
3782 #[test]
3785 fn test_classify_wc_supported() {
3786 assert_eq!(
3789 classify_command("wc -l src/main.rs"),
3790 Classification::Supported {
3791 rtk_equivalent: "rtk wc",
3792 category: "Files",
3793 estimated_savings_pct: 60.0,
3794 status: RtkStatus::Existing,
3795 }
3796 );
3797 }
3798
3799 #[test]
3800 fn test_classify_wc_multi_file() {
3801 assert_eq!(
3802 classify_command("wc src/*.rs"),
3803 Classification::Supported {
3804 rtk_equivalent: "rtk wc",
3805 category: "Files",
3806 estimated_savings_pct: 60.0,
3807 status: RtkStatus::Existing,
3808 }
3809 );
3810 }
3811
3812 #[test]
3813 fn test_rewrite_wc() {
3814 assert_eq!(
3815 rewrite_command_no_prefixes("wc -l src/main.rs", &[]),
3816 Some("rtk wc -l src/main.rs".into())
3817 );
3818 }
3819
3820 #[test]
3821 fn test_rewrite_wc_multi_file() {
3822 assert_eq!(
3823 rewrite_command_no_prefixes("wc src/*.rs", &[]),
3824 Some("rtk wc src/*.rs".into())
3825 );
3826 }
3827
3828 #[test]
3829 fn test_classify_command_substitution_passthrough() {
3830 assert_eq!(
3831 classify_command("git log $(git rev-parse HEAD~1)"),
3832 Classification::Supported {
3833 rtk_equivalent: "rtk git",
3834 category: "Git",
3835 estimated_savings_pct: 70.0,
3836 status: RtkStatus::Existing,
3837 }
3838 );
3839 }
3840
3841 #[test]
3842 fn test_rewrite_command_substitution_passthrough() {
3843 assert_eq!(
3844 rewrite_command_no_prefixes("git log $(git rev-parse HEAD~1)", &[]),
3845 Some("rtk git log $(git rev-parse HEAD~1)".into())
3846 );
3847 }
3848
3849 #[test]
3850 fn test_split_command_substitution_no_split() {
3851 assert_eq!(
3852 split_command_chain("git log $(git rev-parse HEAD~1)"),
3853 vec!["git log $(git rev-parse HEAD~1)"]
3854 );
3855 }
3856
3857 #[test]
3858 fn test_shell_prefix_noglob() {
3859 assert_eq!(
3860 rewrite_command_no_prefixes("noglob git status", &[]),
3861 Some("noglob rtk git status".into())
3862 );
3863 }
3864
3865 #[test]
3866 fn test_shell_prefix_command() {
3867 assert_eq!(
3868 rewrite_command_no_prefixes("command git status", &[]),
3869 Some("command rtk git status".into())
3870 );
3871 }
3872
3873 #[test]
3874 fn test_shell_prefix_builtin_exec_nocorrect() {
3875 assert_eq!(
3876 rewrite_command_no_prefixes("builtin git status", &[]),
3877 Some("builtin rtk git status".into())
3878 );
3879 assert_eq!(
3880 rewrite_command_no_prefixes("exec git status", &[]),
3881 Some("exec rtk git status".into())
3882 );
3883 assert_eq!(
3884 rewrite_command_no_prefixes("nocorrect git status", &[]),
3885 Some("nocorrect rtk git status".into())
3886 );
3887 }
3888
3889 #[test]
3890 fn test_shell_prefix_unknown_inner() {
3891 assert_eq!(
3892 rewrite_command_no_prefixes("noglob unknown_cmd --flag", &[]),
3893 None
3894 );
3895 }
3896
3897 #[test]
3900 fn test_transparent_prefix_strips_and_reprepends() {
3901 let prefixes = vec!["shadowenv exec --".to_string()];
3902 assert_eq!(
3903 super::rewrite_command("shadowenv exec -- git status", &[], &prefixes),
3904 Some("shadowenv exec -- rtk git status".into())
3905 );
3906 }
3907
3908 #[test]
3909 fn test_transparent_prefix_with_test_runner() {
3910 let prefixes = vec!["shadowenv exec --".to_string()];
3911 assert_eq!(
3912 super::rewrite_command("shadowenv exec -- cargo test", &[], &prefixes),
3913 Some("shadowenv exec -- rtk cargo test".into())
3914 );
3915 }
3916
3917 #[test]
3918 fn test_transparent_prefix_unknown_inner_returns_none() {
3919 let prefixes = vec!["shadowenv exec --".to_string()];
3920 assert_eq!(
3921 super::rewrite_command("shadowenv exec -- htop", &[], &prefixes),
3922 None
3923 );
3924 }
3925
3926 #[test]
3927 fn test_transparent_prefix_not_matched_is_passthrough() {
3928 assert_eq!(
3930 super::rewrite_command("shadowenv exec -- git status", &[], &[]),
3931 None
3932 );
3933 }
3934
3935 #[test]
3936 fn test_transparent_prefix_composed_with_builtin() {
3937 let prefixes = vec!["shadowenv exec --".to_string()];
3940 assert_eq!(
3941 super::rewrite_command("noglob shadowenv exec -- git status", &[], &prefixes),
3942 Some("noglob shadowenv exec -- rtk git status".into())
3943 );
3944 }
3945
3946 #[test]
3947 fn test_transparent_prefix_composed_with_env_prefix() {
3948 let prefixes = vec!["bundle exec".to_string()];
3949 assert_eq!(
3950 super::rewrite_command("RAILS_ENV=test bundle exec git status", &[], &prefixes),
3951 Some("RAILS_ENV=test bundle exec rtk git status".into())
3952 );
3953 }
3954
3955 #[test]
3956 fn test_env_prefix_composed_with_builtin() {
3957 assert_eq!(
3958 rewrite_command_no_prefixes("sudo noglob git status", &[]),
3959 Some("sudo noglob rtk git status".into())
3960 );
3961 }
3962
3963 #[test]
3964 fn test_transparent_prefix_multiple_configured() {
3965 let prefixes = vec!["shadowenv exec --".to_string(), "direnv exec .".to_string()];
3966 assert_eq!(
3967 super::rewrite_command("direnv exec . git status", &[], &prefixes),
3968 Some("direnv exec . rtk git status".into())
3969 );
3970 }
3971
3972 #[test]
3973 fn test_transparent_prefixes_normalize_once() {
3974 let prefixes = vec![
3975 " docker exec mycontainer ".to_string(),
3976 "".to_string(),
3977 "docker".to_string(),
3978 "docker exec mycontainer".to_string(),
3979 ];
3980 assert_eq!(
3981 normalize_transparent_prefixes(&prefixes),
3982 vec!["docker exec mycontainer".to_string(), "docker".to_string()]
3983 );
3984 }
3985
3986 #[test]
3987 fn test_transparent_prefix_overlapping_entries_use_longest_match() {
3988 let prefixes = vec!["docker".to_string(), "docker exec app".to_string()];
3989 assert_eq!(
3990 super::rewrite_command("docker exec app git status", &[], &prefixes),
3991 Some("docker exec app rtk git status".into())
3992 );
3993 }
3994
3995 #[test]
3996 fn test_transparent_prefix_whole_word_matching() {
3997 let prefixes = vec!["foo".to_string()];
3999 assert_eq!(
4000 super::rewrite_command("foobar git status", &[], &prefixes),
4001 None
4002 );
4003 }
4004
4005 #[test]
4006 fn test_transparent_prefix_empty_rest_returns_none() {
4007 let prefixes = vec!["shadowenv exec --".to_string()];
4008 assert_eq!(
4009 super::rewrite_command("shadowenv exec --", &[], &prefixes),
4010 None
4011 );
4012 }
4013
4014 #[test]
4015 fn test_transparent_prefix_empty_entry_is_skipped() {
4016 let prefixes = vec!["".to_string(), " ".to_string()];
4018 assert_eq!(
4019 super::rewrite_command("git status", &[], &prefixes),
4020 Some("rtk git status".into())
4021 );
4022 }
4023
4024 #[test]
4025 fn test_transparent_prefix_inside_compound() {
4026 let prefixes = vec!["shadowenv exec --".to_string()];
4028 assert_eq!(
4029 super::rewrite_command(
4030 "shadowenv exec -- git status && shadowenv exec -- cargo test",
4031 &[],
4032 &prefixes
4033 ),
4034 Some("shadowenv exec -- rtk git status && shadowenv exec -- rtk cargo test".into())
4035 );
4036 }
4037
4038 #[test]
4039 fn test_transparent_prefix_respects_excluded() {
4040 let prefixes = vec!["shadowenv exec --".to_string()];
4043 let excluded = vec!["git".to_string()];
4044 assert_eq!(
4045 super::rewrite_command("shadowenv exec -- git status", &excluded, &prefixes),
4046 None
4047 );
4048 }
4049
4050 #[test]
4051 fn test_transparent_prefix_recursion_bounded() {
4052 let prefixes = vec!["wrap".to_string()];
4055 let mut cmd = String::new();
4056 for _ in 0..(MAX_PREFIX_DEPTH + 2) {
4057 cmd.push_str("wrap ");
4058 }
4059 cmd.push_str("git status");
4060 let _ = super::rewrite_command(&cmd, &[], &prefixes);
4063 }
4064
4065 #[test]
4066 fn test_python3_m_pytest() {
4067 assert_eq!(
4068 rewrite_command_no_prefixes("python3 -m pytest tests/", &[]),
4069 Some("rtk pytest tests/".into())
4070 );
4071 }
4072
4073 #[test]
4074 fn test_pip_show() {
4075 assert_eq!(
4076 rewrite_command_no_prefixes("pip show flask", &[]),
4077 Some("rtk pip show flask".into())
4078 );
4079 }
4080
4081 #[test]
4082 fn test_gt_graphite() {
4083 assert_eq!(
4084 rewrite_command_no_prefixes("gt log", &[]),
4085 Some("rtk gt log".into())
4086 );
4087 }
4088
4089 #[test]
4090 fn test_command_no_longer_ignored() {
4091 assert_ne!(
4092 classify_command("command git status"),
4093 Classification::Ignored
4094 );
4095 }
4096
4097 #[test]
4100 fn test_rewrite_pipe_then_and() {
4101 assert_eq!(
4102 rewrite_command_no_prefixes("git log | head -5 && git stash", &[]),
4103 Some("rtk git log | head -5 && rtk git stash".into())
4104 );
4105 }
4106
4107 #[test]
4108 fn test_rewrite_pipe_then_semicolon() {
4109 assert_eq!(
4110 rewrite_command_no_prefixes("cargo test | head; git status", &[]),
4111 Some("rtk cargo test | head; rtk git status".into())
4112 );
4113 }
4114
4115 #[test]
4116 fn test_rewrite_pipe_then_or() {
4117 assert_eq!(
4118 rewrite_command_no_prefixes("cargo test | grep FAIL || git stash", &[]),
4119 Some("rtk cargo test | grep FAIL || rtk git stash".into())
4120 );
4121 }
4122
4123 #[test]
4124 fn test_rewrite_env_pipe_then_and() {
4125 assert_eq!(
4126 rewrite_command_no_prefixes(
4127 "RUST_BACKTRACE=1 cargo test 2>&1 | grep FAILED && git stash",
4128 &[]
4129 ),
4130 Some("RUST_BACKTRACE=1 rtk cargo test 2>&1 | grep FAILED && rtk git stash".into())
4131 );
4132 }
4133
4134 #[test]
4135 fn test_rewrite_and_then_pipe() {
4136 assert_eq!(
4137 rewrite_command_no_prefixes("git status && cargo test | grep FAIL", &[]),
4138 Some("rtk git status && rtk cargo test | grep FAIL".into())
4139 );
4140 }
4141
4142 #[test]
4143 fn test_rewrite_multi_pipe_then_and() {
4144 assert_eq!(
4145 rewrite_command_no_prefixes("git log | head | tail && git status", &[]),
4146 Some("rtk git log | head | tail && rtk git status".into())
4147 );
4148 }
4149
4150 #[test]
4153 fn test_rewrite_leading_backslash_newline() {
4154 assert_eq!(
4157 rewrite_command_no_prefixes("\\\ngit diff HEAD~1", &[]),
4158 Some("rtk git diff HEAD~1".into())
4159 );
4160 }
4161
4162 #[test]
4163 fn test_rewrite_leading_backslash_crlf() {
4164 assert_eq!(
4166 rewrite_command_no_prefixes("\\\r\ngit diff HEAD~1", &[]),
4167 Some("rtk git diff HEAD~1".into())
4168 );
4169 }
4170
4171 #[test]
4172 fn test_rewrite_internal_backslash_newline() {
4173 assert_eq!(
4177 rewrite_command_no_prefixes("git diff \\\nHEAD~1", &[]),
4178 Some("rtk git diff HEAD~1".into())
4179 );
4180 }
4181
4182 #[test]
4183 fn test_rewrite_backslash_newline_with_indent() {
4184 assert_eq!(
4186 rewrite_command_no_prefixes("git \\\n diff HEAD~1", &[]),
4187 Some("rtk git diff HEAD~1".into())
4188 );
4189 }
4190
4191 #[test]
4192 fn test_rewrite_no_line_continuation_unchanged() {
4193 assert_eq!(
4197 rewrite_command_no_prefixes("git diff HEAD~1", &[]),
4198 Some("rtk git diff HEAD~1".into())
4199 );
4200 }
4201
4202 #[test]
4203 fn test_collapse_line_continuations_no_op() {
4204 assert_eq!(
4209 collapse_line_continuations("git diff HEAD~1"),
4210 std::borrow::Cow::<str>::Borrowed("git diff HEAD~1"),
4211 );
4212 }
4213}