1use memchr::memmem;
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use std::borrow::Cow;
14
15const TIER_INSTANT_REJECT: &str = "instant_reject";
17const TIER_STRUCTURE_ANALYSIS: &str = "structure_analysis";
18const TIER_KEYWORD_FILTER: &str = "keyword_filter";
19const TIER_NEVER_INTERCEPT: &str = "never_intercept";
20const TIER_FULL_CLASSIFICATION: &str = "full_classification";
21
22pub static COMPILATION_KEYWORDS: &[&str] = &[
25 "cargo", "rustc", "gcc", "g++", "clang", "clang++", "make", "cmake", "ninja", "meson", "cc",
26 "c++", "bun", "nextest",
27];
28
29pub static NEVER_INTERCEPT: &[&str] = &[
32 "cargo install",
34 "cargo publish",
35 "cargo login",
36 "cargo fmt",
37 "cargo fix",
38 "cargo clean",
39 "cargo new",
40 "cargo init",
41 "cargo add",
42 "cargo remove",
43 "cargo update",
44 "cargo generate-lockfile",
45 "cargo watch",
46 "cargo --version",
47 "cargo -V",
48 "rustc --version",
50 "rustc -V",
51 "gcc --version",
52 "gcc -v",
53 "clang --version",
54 "clang -v",
55 "make --version",
56 "make -v",
57 "cmake --version",
58 "bun install",
60 "bun add",
61 "bun remove",
62 "bun link",
63 "bun unlink",
64 "bun pm",
65 "bun init",
66 "bun create",
67 "bun upgrade",
68 "bun completions",
69 "bun run",
71 "bun build",
72 "bun --help",
73 "bun -h",
74 "bun --version",
75 "bun -v",
76 "bun dev",
78 "bun repl",
79 "cargo nextest list", "cargo nextest archive", "cargo nextest show", ];
84
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
87pub struct Classification {
88 pub is_compilation: bool,
90 pub confidence: f64,
92 pub kind: Option<CompilationKind>,
94 pub reason: Cow<'static, str>,
96 pub command_prefix: Option<String>,
100 pub extracted_command: Option<String>,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
108#[serde(rename_all = "snake_case")]
109pub enum TierDecision {
110 Pass,
111 Reject,
112}
113
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
116pub struct ClassificationTier {
117 pub tier: u8,
119 pub name: Cow<'static, str>,
121 pub decision: TierDecision,
123 pub reason: Cow<'static, str>,
125}
126
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
129pub struct ClassificationDetails {
130 pub original: String,
132 pub normalized: String,
134 pub tiers: Vec<ClassificationTier>,
136 pub classification: Classification,
138}
139
140impl Classification {
141 pub fn not_compilation(reason: impl Into<Cow<'static, str>>) -> Self {
143 Self {
144 is_compilation: false,
145 confidence: 0.0,
146 kind: None,
147 reason: reason.into(),
148 command_prefix: None,
149 extracted_command: None,
150 }
151 }
152
153 pub fn compilation(
155 kind: CompilationKind,
156 confidence: f64,
157 reason: impl Into<Cow<'static, str>>,
158 ) -> Self {
159 Self {
160 is_compilation: true,
161 confidence,
162 kind: Some(kind),
163 reason: reason.into(),
164 command_prefix: None,
165 extracted_command: None,
166 }
167 }
168
169 pub fn compound_compilation(
173 kind: CompilationKind,
174 confidence: f64,
175 reason: impl Into<Cow<'static, str>>,
176 prefix: String,
177 extracted: String,
178 ) -> Self {
179 Self {
180 is_compilation: true,
181 confidence,
182 kind: Some(kind),
183 reason: reason.into(),
184 command_prefix: Some(prefix),
185 extracted_command: Some(extracted),
186 }
187 }
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
192#[serde(rename_all = "snake_case")]
193pub enum CompilationKind {
194 CargoBuild,
197 CargoTest,
198 CargoCheck,
199 CargoClippy,
200 CargoDoc,
201 CargoNextest,
203 CargoBench,
205 Rustc,
207
208 Gcc,
211 Gpp,
213 Clang,
215 Clangpp,
217
218 Make,
221 CmakeBuild,
223 Ninja,
225 Meson,
227
228 BunTest,
231 BunTypecheck,
233}
234
235impl CompilationKind {
236 pub fn is_test_command(&self) -> bool {
242 matches!(
243 self,
244 CompilationKind::CargoTest
245 | CompilationKind::CargoNextest
246 | CompilationKind::CargoBench
247 | CompilationKind::BunTest
248 )
249 }
250
251 pub fn command_base(&self) -> &'static str {
256 match self {
257 CompilationKind::CargoBuild
259 | CompilationKind::CargoTest
260 | CompilationKind::CargoCheck
261 | CompilationKind::CargoClippy
262 | CompilationKind::CargoDoc
263 | CompilationKind::CargoBench => "cargo",
264 CompilationKind::CargoNextest => "cargo", CompilationKind::Rustc => "rustc",
266 CompilationKind::Gcc => "gcc",
268 CompilationKind::Gpp => "g++",
269 CompilationKind::Clang => "clang",
270 CompilationKind::Clangpp => "clang++",
271 CompilationKind::Make => "make",
273 CompilationKind::CmakeBuild => "cmake",
274 CompilationKind::Ninja => "ninja",
275 CompilationKind::Meson => "meson",
276 CompilationKind::BunTest | CompilationKind::BunTypecheck => "bun",
278 }
279 }
280}
281
282pub fn classify_command(cmd: &str) -> Classification {
290 classify_command_inner(cmd, 0)
291}
292
293#[allow(dead_code)] const MAX_CLASSIFY_DEPTH: u8 = 1;
297
298#[allow(dead_code)] const MAX_SPLIT_INPUT_LEN: usize = 10 * 1024;
302
303fn classify_command_inner(cmd: &str, depth: u8) -> Classification {
304 let cmd = cmd.trim();
305
306 if cmd.is_empty() {
308 return Classification::not_compilation("empty command");
309 }
310
311 if depth == 0
315 && cmd.len() < MAX_SPLIT_INPUT_LEN
316 && let Some(result) = try_classify_compound_command(cmd)
317 {
318 return result;
319 }
320
321 if let Some(reason) = check_structure(cmd) {
323 return Classification::not_compilation(reason);
324 }
325
326 if !contains_compilation_keyword(cmd) {
328 return Classification::not_compilation("no compilation keyword");
329 }
330
331 let normalized_cow = normalize_command(cmd);
333 let normalized = normalized_cow.as_ref();
334
335 for pattern in NEVER_INTERCEPT {
338 if let Some(rest) = normalized.strip_prefix(pattern) {
339 if rest.is_empty() || rest.starts_with(' ') {
342 return Classification::not_compilation("matches never-intercept pattern");
343 }
344 }
345 }
346
347 classify_full(normalized)
349}
350
351fn try_classify_compound_command(cmd: &str) -> Option<Classification> {
364 if !cmd.contains("&&") {
366 return None;
367 }
368
369 if cmd.contains('|') {
372 return None; }
374 if has_file_redirect(cmd) {
375 return None; }
377 if cmd.contains('(') || cmd.contains('`') || cmd.contains("$(") {
378 return None; }
380 if cmd.contains(';') {
381 return None; }
383
384 let segments = split_and_chain(cmd)?;
386 if segments.is_empty() {
387 return None;
388 }
389
390 let last_segment = segments.last()?.trim();
392 if last_segment.is_empty() {
393 return None;
394 }
395
396 let last_classification = classify_command_inner(last_segment, 1);
398
399 if !last_classification.is_compilation {
400 return None;
401 }
402
403 let prefix = if segments.len() == 1 {
405 return None;
407 } else {
408 let last_pos = cmd.rfind(last_segment)?;
411 cmd[..last_pos].to_string()
412 };
413
414 Some(Classification::compound_compilation(
416 last_classification.kind?,
417 last_classification.confidence,
418 "compound command with compilation suffix",
419 prefix,
420 last_segment.to_string(),
421 ))
422}
423
424fn split_and_chain(cmd: &str) -> Option<Vec<&str>> {
427 if !cmd.contains("&&") {
428 return None;
429 }
430
431 let mut segments = Vec::new();
432 let mut current_start = 0;
433 let mut in_single = false;
434 let mut in_double = false;
435 let mut escaped = false;
436 let bytes = cmd.as_bytes();
437 let mut i = 0;
438
439 while i < bytes.len() {
440 let b = bytes[i];
441
442 if escaped {
443 escaped = false;
444 i += 1;
445 continue;
446 }
447
448 if b == b'\\' {
449 escaped = true;
450 i += 1;
451 continue;
452 }
453
454 if b == b'\'' && !in_double {
455 in_single = !in_single;
456 } else if b == b'"' && !in_single {
457 in_double = !in_double;
458 } else if !in_single
459 && !in_double
460 && b == b'&'
461 && i + 1 < bytes.len()
462 && bytes[i + 1] == b'&'
463 {
464 let segment = &cmd[current_start..i];
465 let trimmed = segment.trim();
466 if !trimmed.is_empty() {
467 segments.push(trimmed);
468 }
469 current_start = i + 2;
470 i += 1; }
472
473 i += 1;
474 }
475
476 let final_segment = &cmd[current_start..];
478 let trimmed = final_segment.trim();
479 if !trimmed.is_empty() {
480 segments.push(trimmed);
481 }
482
483 if segments.len() > 1 {
484 Some(segments)
485 } else {
486 None
487 }
488}
489
490#[allow(dead_code)] fn split_multi_command(cmd: &str) -> Option<Vec<&str>> {
496 if !cmd.contains("&&") && !cmd.contains("||") && !cmd.contains(';') {
498 return None;
499 }
500
501 let mut segments = Vec::new();
502 let mut current_start = 0;
503 let mut in_single = false;
504 let mut in_double = false;
505 let mut escaped = false;
506 let chars: Vec<char> = cmd.chars().collect();
507 let mut i = 0;
508
509 while i < chars.len() {
510 let c = chars[i];
511
512 if escaped {
513 escaped = false;
514 i += 1;
515 continue;
516 }
517
518 if c == '\\' {
519 escaped = true;
520 i += 1;
521 continue;
522 }
523
524 if c == '\'' && !in_double {
525 in_single = !in_single;
526 } else if c == '"' && !in_single {
527 in_double = !in_double;
528 } else if !in_single && !in_double {
529 if c == ';' {
531 let segment = &cmd[current_start..byte_index(&chars, i)];
532 let trimmed = segment.trim();
533 if !trimmed.is_empty() {
534 segments.push(trimmed);
535 }
536 current_start = byte_index(&chars, i + 1);
537 } else if c == '&' && i + 1 < chars.len() && chars[i + 1] == '&' {
538 let segment = &cmd[current_start..byte_index(&chars, i)];
539 let trimmed = segment.trim();
540 if !trimmed.is_empty() {
541 segments.push(trimmed);
542 }
543 current_start = byte_index(&chars, i + 2);
544 i += 1; } else if c == '|' && i + 1 < chars.len() && chars[i + 1] == '|' {
546 let segment = &cmd[current_start..byte_index(&chars, i)];
547 let trimmed = segment.trim();
548 if !trimmed.is_empty() {
549 segments.push(trimmed);
550 }
551 current_start = byte_index(&chars, i + 2);
552 i += 1; }
554 }
555
556 i += 1;
557 }
558
559 let final_segment = &cmd[current_start..];
561 let trimmed = final_segment.trim();
562 if !trimmed.is_empty() {
563 segments.push(trimmed);
564 }
565
566 if segments.len() > 1 {
568 Some(segments)
569 } else {
570 None
571 }
572}
573
574#[allow(dead_code)] fn byte_index(chars: &[char], char_idx: usize) -> usize {
577 chars.iter().take(char_idx).map(|c| c.len_utf8()).sum()
578}
579
580#[allow(dead_code)] fn classify_multi_command(segments: &[&str], depth: u8) -> Classification {
586 let mut best_compilation: Option<Classification> = None;
587
588 for &segment in segments {
589 let result = classify_command_inner(segment, depth + 1);
590 if result.is_compilation {
591 let dominated = match &best_compilation {
592 Some(prev) => result.confidence > prev.confidence,
593 None => true,
594 };
595 if dominated {
596 best_compilation = Some(result);
597 }
598 }
599 }
600
601 if let Some(compilation) = best_compilation {
602 return compilation;
603 }
604
605 Classification::not_compilation("no sub-command is compilation")
606}
607
608pub fn classify_command_detailed(cmd: &str) -> ClassificationDetails {
610 let original = cmd.to_string();
611 let cmd = cmd.trim();
612 let mut tiers = Vec::new();
613
614 if cmd.is_empty() {
616 let classification = Classification::not_compilation("empty command");
617 tiers.push(ClassificationTier {
618 tier: 0,
619 name: Cow::Borrowed(TIER_INSTANT_REJECT),
620 decision: TierDecision::Reject,
621 reason: Cow::Borrowed("empty command"),
622 });
623 return ClassificationDetails {
624 original,
625 normalized: cmd.to_string(),
626 tiers,
627 classification,
628 };
629 }
630
631 tiers.push(ClassificationTier {
632 tier: 0,
633 name: Cow::Borrowed(TIER_INSTANT_REJECT),
634 decision: TierDecision::Pass,
635 reason: Cow::Borrowed("command present"),
636 });
637
638 if let Some(reason) = check_structure(cmd) {
640 let classification = Classification::not_compilation(reason);
641 tiers.push(ClassificationTier {
642 tier: 1,
643 name: Cow::Borrowed(TIER_STRUCTURE_ANALYSIS),
644 decision: TierDecision::Reject,
645 reason: Cow::Borrowed(reason),
646 });
647 return ClassificationDetails {
648 original,
649 normalized: cmd.to_string(),
650 tiers,
651 classification,
652 };
653 }
654
655 tiers.push(ClassificationTier {
656 tier: 1,
657 name: Cow::Borrowed(TIER_STRUCTURE_ANALYSIS),
658 decision: TierDecision::Pass,
659 reason: Cow::Borrowed("no pipes/redirects/backgrounding"),
660 });
661
662 if !contains_compilation_keyword(cmd) {
664 let classification = Classification::not_compilation("no compilation keyword");
665 tiers.push(ClassificationTier {
666 tier: 2,
667 name: Cow::Borrowed(TIER_KEYWORD_FILTER),
668 decision: TierDecision::Reject,
669 reason: Cow::Borrowed("no compilation keyword"),
670 });
671 return ClassificationDetails {
672 original,
673 normalized: cmd.to_string(),
674 tiers,
675 classification,
676 };
677 }
678
679 tiers.push(ClassificationTier {
680 tier: 2,
681 name: Cow::Borrowed(TIER_KEYWORD_FILTER),
682 decision: TierDecision::Pass,
683 reason: Cow::Borrowed("keyword present"),
684 });
685
686 let normalized_cow = normalize_command(cmd);
688 let normalized = normalized_cow.as_ref();
689
690 for pattern in NEVER_INTERCEPT {
692 if let Some(rest) = normalized.strip_prefix(pattern)
693 && (rest.is_empty() || rest.starts_with(' '))
694 {
695 let reason: Cow<'static, str> =
696 Cow::Owned(format!("matches never-intercept: {pattern}"));
697 let classification = Classification::not_compilation(reason.clone());
698 tiers.push(ClassificationTier {
699 tier: 3,
700 name: Cow::Borrowed(TIER_NEVER_INTERCEPT),
701 decision: TierDecision::Reject,
702 reason,
703 });
704 return ClassificationDetails {
705 original,
706 normalized: normalized.to_string(),
707 tiers,
708 classification,
709 };
710 }
711 }
712
713 tiers.push(ClassificationTier {
714 tier: 3,
715 name: Cow::Borrowed(TIER_NEVER_INTERCEPT),
716 decision: TierDecision::Pass,
717 reason: Cow::Borrowed("no never-intercept match"),
718 });
719
720 let classification = classify_full(normalized);
722 let decision = if classification.is_compilation {
723 TierDecision::Pass
724 } else {
725 TierDecision::Reject
726 };
727 tiers.push(ClassificationTier {
728 tier: 4,
729 name: Cow::Borrowed(TIER_FULL_CLASSIFICATION),
730 decision,
731 reason: classification.reason.clone(),
732 });
733
734 ClassificationDetails {
735 original,
736 normalized: normalized.to_string(),
737 tiers,
738 classification,
739 }
740}
741
742pub fn normalize_command(cmd: &str) -> Cow<'_, str> {
744 let mut result = cmd.trim();
745
746 let wrappers = [
749 "sudo", "env", "time", "nice", "ionice", "strace", "ltrace", "perf", "taskset", "numactl",
750 ];
751
752 loop {
753 let mut changed = false;
754 for wrapper in wrappers {
755 if let Some(rest) = result.strip_prefix(wrapper) {
756 if rest.is_empty() || rest.starts_with(char::is_whitespace) {
759 result = rest.trim_start();
760 changed = true;
761
762 while result.starts_with('-') {
765 let end_idx = result.find(char::is_whitespace).unwrap_or(result.len());
767
768 result = result[end_idx..].trim_start();
770 }
771 }
772 }
773 }
774
775 let chars = result.chars();
779 let mut token_len = 0;
780 let mut in_quote = None; let mut escaped = false;
782 let mut has_equals = false;
783 let mut has_space = false;
784
785 for c in chars {
786 if escaped {
787 escaped = false;
788 token_len += c.len_utf8();
789 continue;
790 }
791
792 if c == '\\' {
793 escaped = true;
794 token_len += c.len_utf8();
795 continue;
796 }
797
798 if let Some(q) = in_quote {
799 if c == q {
800 in_quote = None;
801 } else if c == '=' {
802 has_equals = true;
803 }
804 token_len += c.len_utf8();
805 } else if c == '"' || c == '\'' {
806 in_quote = Some(c);
807 token_len += c.len_utf8();
808 } else if c.is_whitespace() {
809 has_space = true;
810 break;
811 } else {
812 if c == '=' {
813 has_equals = true;
814 }
815 token_len += c.len_utf8();
816 }
817 }
818
819 if has_equals && in_quote.is_none() && has_space {
820 result = result[token_len..].trim_start();
823 changed = true;
824 }
825
826 if result.starts_with('/') {
829 if let Some(space_idx) = result.find(' ') {
830 let cmd_part = &result[..space_idx];
831 if let Some(last_slash) = cmd_part.rfind('/') {
832 result = &result[last_slash + 1..];
834 changed = true;
835 }
836 } else {
837 if let Some(last_slash) = result.rfind('/') {
839 result = &result[last_slash + 1..];
840 changed = true;
841 }
842 }
843 }
844
845 if !changed {
846 break;
847 }
848 }
849
850 if result == cmd {
851 Cow::Borrowed(cmd)
852 } else {
853 Cow::Owned(result.to_string())
854 }
855}
856
857fn has_file_redirect(cmd: &str) -> bool {
866 let bytes = cmd.as_bytes();
867 let len = bytes.len();
868 let mut in_single = false;
869 let mut in_double = false;
870 let mut escaped = false;
871 let mut i = 0;
872
873 while i < len {
874 let b = bytes[i];
875
876 if escaped {
877 escaped = false;
878 i += 1;
879 continue;
880 }
881 if b == b'\\' {
882 escaped = true;
883 i += 1;
884 continue;
885 }
886 match b {
887 b'\'' if !in_double => {
888 in_single = !in_single;
889 }
890 b'"' if !in_single => {
891 in_double = !in_double;
892 }
893 b'>' if !in_single && !in_double => {
894 if i + 1 < len && bytes[i + 1] == b'&' {
896 i += 2; if i < len && bytes[i].is_ascii_digit() {
900 i += 1;
901 }
902 continue;
903 }
904 if i + 1 < len && bytes[i + 1] == b'(' {
907 i += 1;
908 continue;
909 }
910 return true; }
912 b'<' if !in_single && !in_double => {
913 if i + 1 < len && bytes[i + 1] == b'(' {
915 i += 1;
916 continue;
917 }
918 return true; }
920 _ => {}
921 }
922 i += 1;
923 }
924 false
925}
926
927fn check_structure(cmd: &str) -> Option<&'static str> {
944 let bytes = cmd.as_bytes();
945 let len = bytes.len();
946
947 let mut in_single = false;
948 let mut in_double = false;
949 let mut escaped = false;
950
951 let mut found_backgrounded = false;
954 let mut found_piped = false;
955 let mut found_subshell = false;
956 let mut found_output_redirect = false;
957 let mut found_input_redirect = false;
958 let mut found_semicolon = false;
959 let mut found_and_chain = false;
960 let mut found_or_chain = false;
961 let mut found_subshell_capture = false;
962
963 let mut i = 0;
964 while i < len {
965 let b = bytes[i];
966
967 if escaped {
969 escaped = false;
970 i += 1;
971 continue;
972 }
973 if b == b'\\' {
974 escaped = true;
975 i += 1;
976 continue;
977 }
978
979 if b == b'\'' && !in_double {
981 in_single = !in_single;
982 i += 1;
983 continue;
984 }
985 if b == b'"' && !in_single {
986 in_double = !in_double;
987 i += 1;
988 continue;
989 }
990
991 if in_single || in_double {
993 i += 1;
994 continue;
995 }
996
997 match b {
999 b'\n' | b'\r' => return Some("contains embedded newline"),
1001
1002 b'&' => {
1004 if i + 1 < len && bytes[i + 1] == b'&' {
1005 found_and_chain = true;
1006 i += 1; } else {
1008 let prev = if i > 0 { bytes[i - 1] } else { 0 };
1010 let next = if i + 1 < len { bytes[i + 1] } else { 0 };
1011 if prev != b'>' && next != b'>' {
1012 found_backgrounded = true;
1013 }
1014 }
1015 }
1016
1017 b'|' => {
1019 if i + 1 < len && bytes[i + 1] == b'|' {
1020 found_or_chain = true;
1021 i += 1; } else {
1023 found_piped = true;
1024 }
1025 }
1026
1027 b'(' => found_subshell = true,
1029
1030 b'>' => {
1033 if i + 1 < len && bytes[i + 1] == b'(' {
1034 found_subshell = true;
1035 } else if i + 1 < len && bytes[i + 1] == b'&' {
1036 i += 1; if i + 1 < len && bytes[i + 1].is_ascii_digit() {
1041 i += 1; }
1043 } else if i + 1 < len && bytes[i + 1] == b'>' {
1044 found_output_redirect = true;
1046 i += 1; } else {
1048 found_output_redirect = true;
1049 }
1050 }
1051
1052 b'<' => {
1054 if i + 1 < len && bytes[i + 1] == b'(' {
1055 found_subshell = true;
1056 } else {
1057 found_input_redirect = true;
1058 }
1059 }
1060
1061 b';' => found_semicolon = true,
1063
1064 b'`' => found_subshell_capture = true,
1066
1067 b'$' if i + 1 < len && bytes[i + 1] == b'(' => {
1069 found_subshell_capture = true;
1070 }
1071
1072 _ => {}
1073 }
1074
1075 i += 1;
1076 }
1077
1078 if found_backgrounded {
1080 return Some("backgrounded command");
1081 }
1082 if found_piped && !found_or_chain {
1084 return Some("piped command");
1085 }
1086 if found_subshell {
1087 return Some("subshell execution");
1088 }
1089 if found_output_redirect {
1090 return Some("output redirected");
1091 }
1092 if found_input_redirect {
1093 return Some("input redirected");
1094 }
1095 if found_semicolon {
1096 return Some("chained command (;)");
1097 }
1098 if found_and_chain {
1099 return Some("chained command (&&)");
1100 }
1101 if found_or_chain {
1102 return Some("chained command (||)");
1103 }
1104 if found_subshell_capture {
1105 return Some("subshell capture");
1106 }
1107
1108 None
1109}
1110
1111fn contains_compilation_keyword(cmd: &str) -> bool {
1113 let cmd_bytes = cmd.as_bytes();
1114 for keyword in COMPILATION_KEYWORDS {
1115 if memmem::find(cmd_bytes, keyword.as_bytes()).is_some() {
1116 return true;
1117 }
1118 }
1119 false
1120}
1121
1122fn classify_full(cmd: &str) -> Classification {
1124 if cmd.starts_with("cargo ") || cmd == "cargo" {
1126 return classify_cargo(cmd);
1127 }
1128
1129 if cmd.starts_with("rustc ") || cmd == "rustc" {
1131 return Classification::compilation(CompilationKind::Rustc, 0.95, "rustc invocation");
1132 }
1133
1134 if cmd.starts_with("gcc ")
1136 && (cmd.contains(" -c ") || cmd.contains(" -o ") || cmd.contains(".c"))
1137 {
1138 return Classification::compilation(CompilationKind::Gcc, 0.90, "gcc compilation");
1139 }
1140
1141 if cmd.starts_with("g++ ")
1143 && (cmd.contains(" -c ")
1144 || cmd.contains(" -o ")
1145 || cmd.contains(".cpp")
1146 || cmd.contains(".cc"))
1147 {
1148 return Classification::compilation(CompilationKind::Gpp, 0.90, "g++ compilation");
1149 }
1150
1151 if cmd.starts_with("clang ")
1153 && !cmd.starts_with("clang++ ")
1154 && (cmd.contains(" -c ") || cmd.contains(" -o ") || cmd.contains(".c"))
1155 {
1156 return Classification::compilation(CompilationKind::Clang, 0.90, "clang compilation");
1157 }
1158
1159 if cmd.starts_with("clang++ ")
1161 && (cmd.contains(" -c ")
1162 || cmd.contains(" -o ")
1163 || cmd.contains(".cpp")
1164 || cmd.contains(".cc"))
1165 {
1166 return Classification::compilation(CompilationKind::Clangpp, 0.90, "clang++ compilation");
1167 }
1168
1169 if cmd.starts_with("cc ")
1171 && (cmd.contains(" -c ") || cmd.contains(" -o ") || cmd.contains(".c"))
1172 {
1173 return Classification::compilation(CompilationKind::Gcc, 0.85, "cc compilation");
1174 }
1175
1176 if cmd.starts_with("c++ ")
1178 && (cmd.contains(" -c ")
1179 || cmd.contains(" -o ")
1180 || cmd.contains(".cpp")
1181 || cmd.contains(".cc"))
1182 {
1183 return Classification::compilation(CompilationKind::Gpp, 0.85, "c++ compilation");
1184 }
1185
1186 if cmd.starts_with("make") && (cmd == "make" || cmd.starts_with("make ")) {
1188 if cmd.contains("clean") || cmd.contains("install") || cmd.contains("distclean") {
1190 return Classification::not_compilation("make maintenance command");
1191 }
1192 return Classification::compilation(CompilationKind::Make, 0.85, "make build");
1193 }
1194
1195 if cmd.starts_with("cmake ") || cmd == "cmake" {
1197 let mut tokens = cmd.split_whitespace();
1198 let _ = tokens.next(); if tokens.any(|token| token == "--build" || token.starts_with("--build=")) {
1200 return Classification::compilation(CompilationKind::CmakeBuild, 0.90, "cmake --build");
1201 }
1202 }
1203
1204 if cmd.starts_with("ninja") && (cmd == "ninja" || cmd.starts_with("ninja ")) {
1206 if cmd.contains("-t clean") || cmd.contains("clean") {
1207 return Classification::not_compilation("ninja clean");
1208 }
1209 return Classification::compilation(CompilationKind::Ninja, 0.90, "ninja build");
1210 }
1211
1212 if cmd.starts_with("meson ") || cmd == "meson" {
1214 let mut tokens = cmd.split_whitespace();
1215 let _ = tokens.next(); let mut subcommand = None;
1217 for token in tokens {
1218 if token.starts_with('-') {
1219 continue;
1220 }
1221 subcommand = Some(token);
1222 break;
1223 }
1224 if matches!(subcommand, Some("compile")) {
1225 return Classification::compilation(CompilationKind::Meson, 0.85, "meson compile");
1226 }
1227 }
1228
1229 let mut tokens = cmd.split_whitespace();
1231 if tokens.next() == Some("bun") {
1232 match tokens.next() {
1233 Some("test") => {
1234 if tokens.any(|a| a == "-w" || a == "--watch") {
1237 return Classification::not_compilation(
1238 "bun test --watch is interactive (not intercepted)",
1239 );
1240 }
1241 return Classification::compilation(
1242 CompilationKind::BunTest,
1243 0.95,
1244 "bun test command",
1245 );
1246 }
1247 Some("typecheck") => {
1248 if tokens.any(|a| a == "-w" || a == "--watch") {
1250 return Classification::not_compilation(
1251 "bun typecheck --watch is interactive (not intercepted)",
1252 );
1253 }
1254 return Classification::compilation(
1255 CompilationKind::BunTypecheck,
1256 0.95,
1257 "bun typecheck command",
1258 );
1259 }
1260 Some("x") => {
1261 return Classification::not_compilation("bun x runs arbitrary packages");
1264 }
1265 _ => {}
1266 }
1267 }
1268
1269 Classification::not_compilation("no matching pattern")
1270}
1271
1272fn classify_cargo(cmd: &str) -> Classification {
1277 let mut tokens = cmd.split_whitespace();
1278
1279 let _cargo = tokens.next();
1281
1282 let Some(token1) = tokens.next() else {
1284 return Classification::not_compilation("bare cargo command");
1285 };
1286
1287 let subcommand = if token1.starts_with('+') {
1289 let Some(sub) = tokens.next() else {
1290 return Classification::not_compilation("cargo +toolchain without subcommand");
1291 };
1292 sub
1293 } else {
1294 token1
1295 };
1296
1297 match subcommand {
1298 "build" | "b" => {
1299 Classification::compilation(CompilationKind::CargoBuild, 0.95, "cargo build")
1300 }
1301 "test" | "t" => Classification::compilation(CompilationKind::CargoTest, 0.95, "cargo test"),
1302 "check" | "c" => {
1303 Classification::compilation(CompilationKind::CargoCheck, 0.90, "cargo check")
1304 }
1305 "clippy" => Classification::compilation(CompilationKind::CargoClippy, 0.90, "cargo clippy"),
1306 "doc" => Classification::compilation(CompilationKind::CargoDoc, 0.85, "cargo doc"),
1307 "run" | "r" => {
1308 Classification::compilation(
1310 CompilationKind::CargoBuild,
1311 0.85,
1312 "cargo run (includes build)",
1313 )
1314 }
1315 "bench" => Classification::compilation(CompilationKind::CargoBench, 0.90, "cargo bench"),
1316 "nextest" => {
1317 let Some(nextest_sub) = tokens.next() else {
1321 return Classification::not_compilation("bare cargo nextest without subcommand");
1322 };
1323
1324 match nextest_sub {
1325 "run" | "r" => Classification::compilation(
1326 CompilationKind::CargoNextest,
1327 0.95,
1328 "cargo nextest run",
1329 ),
1330 _ => Classification::not_compilation("cargo nextest subcommand not interceptable"),
1331 }
1332 }
1333 _ => Classification::not_compilation("cargo subcommand not interceptable"),
1334 }
1335}
1336
1337pub fn split_shell_commands(cmd: &str) -> Vec<&str> {
1348 let bytes = cmd.as_bytes();
1349 let len = bytes.len();
1350 let mut segments: Vec<&str> = Vec::new();
1351 let mut start = 0;
1352 let mut i = 0;
1353
1354 let mut in_single = false;
1356 let mut in_double = false;
1357 let mut in_backtick = false;
1358 let mut escaped = false;
1359
1360 while i < len {
1361 let b = bytes[i];
1362
1363 if escaped {
1364 escaped = false;
1365 i += 1;
1366 continue;
1367 }
1368
1369 if b == b'\\' {
1370 escaped = true;
1371 i += 1;
1372 continue;
1373 }
1374
1375 if !in_double && !in_backtick && b == b'\'' {
1377 in_single = !in_single;
1378 i += 1;
1379 continue;
1380 }
1381 if !in_single && !in_backtick && b == b'"' {
1382 in_double = !in_double;
1383 i += 1;
1384 continue;
1385 }
1386 if !in_single && !in_double && b == b'`' {
1387 in_backtick = !in_backtick;
1388 i += 1;
1389 continue;
1390 }
1391
1392 if in_single || in_double || in_backtick {
1394 i += 1;
1395 continue;
1396 }
1397
1398 if b == b'&' && i + 1 < len && bytes[i + 1] == b'&' {
1400 let seg = cmd[start..i].trim();
1401 if !seg.is_empty() {
1402 segments.push(seg);
1403 }
1404 i += 2;
1405 start = i;
1406 continue;
1407 }
1408
1409 if b == b'|' && i + 1 < len && bytes[i + 1] == b'|' {
1411 let seg = cmd[start..i].trim();
1412 if !seg.is_empty() {
1413 segments.push(seg);
1414 }
1415 i += 2;
1416 start = i;
1417 continue;
1418 }
1419
1420 if b == b';' {
1422 let seg = cmd[start..i].trim();
1423 if !seg.is_empty() {
1424 segments.push(seg);
1425 }
1426 i += 1;
1427 start = i;
1428 continue;
1429 }
1430
1431 i += 1;
1432 }
1433
1434 let seg = cmd[start..].trim();
1436 if !seg.is_empty() {
1437 segments.push(seg);
1438 }
1439
1440 segments
1441}
1442
1443#[cfg(test)]
1444mod tests {
1445 use super::*;
1446 use crate::test_guard;
1447
1448 #[test]
1449 fn test_cargo_build_with_toolchain() {
1450 let _guard = test_guard!();
1451 let result = classify_command("cargo +nightly build");
1452 assert!(result.is_compilation);
1453 assert_eq!(result.kind, Some(CompilationKind::CargoBuild));
1454 }
1455
1456 #[test]
1457 fn test_cargo_test_with_toolchain() {
1458 let _guard = test_guard!();
1459 let result = classify_command("cargo +1.80.0 test");
1460 assert!(result.is_compilation);
1461 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
1462 }
1463
1464 #[test]
1465 fn test_cargo_nextest_with_toolchain() {
1466 let _guard = test_guard!();
1467 let result = classify_command("cargo +nightly nextest run");
1468 assert!(result.is_compilation);
1469 assert_eq!(result.kind, Some(CompilationKind::CargoNextest));
1470 }
1471
1472 #[test]
1473 fn test_cargo_build() {
1474 let _guard = test_guard!();
1475 let result = classify_command("cargo build");
1476 assert!(result.is_compilation);
1477 assert_eq!(result.kind, Some(CompilationKind::CargoBuild));
1478 assert!(result.confidence >= 0.90);
1479 }
1480
1481 #[test]
1482 fn test_cargo_build_release() {
1483 let _guard = test_guard!();
1484 let result = classify_command("cargo build --release");
1485 assert!(result.is_compilation);
1486 assert_eq!(result.kind, Some(CompilationKind::CargoBuild));
1487 }
1488
1489 #[test]
1490 fn test_cargo_test() {
1491 let _guard = test_guard!();
1492 let result = classify_command("cargo test");
1493 assert!(result.is_compilation);
1494 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
1495 }
1496
1497 #[test]
1498 fn test_cargo_fmt_not_intercepted() {
1499 let _guard = test_guard!();
1500 let result = classify_command("cargo fmt");
1501 assert!(!result.is_compilation);
1502 assert!(result.reason.contains("never-intercept"));
1503 }
1504
1505 #[test]
1506 fn test_cargo_install_not_intercepted() {
1507 let _guard = test_guard!();
1508 let result = classify_command("cargo install ripgrep");
1509 assert!(!result.is_compilation);
1510 assert!(result.reason.contains("never-intercept"));
1511 }
1512
1513 #[test]
1514 fn test_piped_command_not_intercepted() {
1515 let _guard = test_guard!();
1516 let result = classify_command("cargo build 2>&1 | grep error");
1517 assert!(!result.is_compilation);
1518 assert!(result.reason.contains("piped"));
1519 }
1520
1521 #[test]
1522 fn test_backgrounded_not_intercepted() {
1523 let _guard = test_guard!();
1524 let result = classify_command("cargo build &");
1525 assert!(!result.is_compilation);
1526 assert!(result.reason.contains("background"));
1527 }
1528
1529 #[test]
1530 fn test_newline_injection_not_intercepted() {
1531 let _guard = test_guard!();
1532 let result = classify_command("cargo build\nrm -rf /");
1534 assert!(!result.is_compilation);
1535 assert!(result.reason.contains("newline"));
1536
1537 let result = classify_command("cargo build\r\nrm -rf /");
1538 assert!(!result.is_compilation);
1539 assert!(result.reason.contains("newline"));
1540 }
1541
1542 #[test]
1543 fn test_redirected_not_intercepted() {
1544 let _guard = test_guard!();
1545 let result = classify_command("cargo build > log.txt");
1546 assert!(!result.is_compilation);
1547 assert!(result.reason.contains("redirect"));
1548 }
1549
1550 #[test]
1551 fn test_input_redirected_not_intercepted() {
1552 let _guard = test_guard!();
1553 let result = classify_command("cargo build < input.txt");
1554 assert!(!result.is_compilation);
1555 assert!(result.reason.contains("input redirected"));
1556 }
1557
1558 #[test]
1559 fn test_process_substitution_not_intercepted() {
1560 let _guard = test_guard!();
1561 let result = classify_command("cargo build --config <(echo ...)");
1562 assert!(!result.is_compilation);
1563 assert!(result.reason.contains("subshell execution"));
1564 }
1565
1566 #[test]
1567 fn test_subshell_not_intercepted() {
1568 let _guard = test_guard!();
1569 let result = classify_command("(cargo build)");
1570 assert!(!result.is_compilation);
1571 assert!(result.reason.contains("subshell execution"));
1572 }
1573
1574 #[test]
1575 fn test_gcc_compile() {
1576 let _guard = test_guard!();
1577 let result = classify_command("gcc -c main.c -o main.o");
1578 assert!(result.is_compilation);
1579 assert_eq!(result.kind, Some(CompilationKind::Gcc));
1580 }
1581
1582 #[test]
1583 fn test_make() {
1584 let _guard = test_guard!();
1585 let result = classify_command("make -j8");
1586 assert!(result.is_compilation);
1587 assert_eq!(result.kind, Some(CompilationKind::Make));
1588 }
1589
1590 #[test]
1591 fn test_make_clean_not_intercepted() {
1592 let _guard = test_guard!();
1593 let result = classify_command("make clean");
1594 assert!(!result.is_compilation);
1595 assert!(result.reason.contains("make maintenance command"));
1596 }
1597
1598 #[test]
1599 fn test_cmake_build_requires_cmake_command() {
1600 let _guard = test_guard!();
1601 let result = classify_command("echo cmake --build .");
1602 assert!(!result.is_compilation);
1603 }
1604
1605 #[test]
1606 fn test_cmake_build_with_equals_flag() {
1607 let _guard = test_guard!();
1608 let result = classify_command("cmake --build=build");
1609 assert!(result.is_compilation);
1610 assert_eq!(result.kind, Some(CompilationKind::CmakeBuild));
1611 }
1612
1613 #[test]
1614 fn test_meson_compile_requires_meson_command() {
1615 let _guard = test_guard!();
1616 let result = classify_command("echo meson compile");
1617 assert!(!result.is_compilation);
1618 }
1619
1620 #[test]
1621 fn test_non_compilation() {
1622 let _guard = test_guard!();
1623 let result = classify_command("ls -la");
1624 assert!(!result.is_compilation);
1625 assert!(result.reason.contains("no compilation keyword"));
1626 }
1627
1628 #[test]
1629 fn test_empty_command() {
1630 let _guard = test_guard!();
1631 let result = classify_command("");
1632 assert!(!result.is_compilation);
1633 assert!(result.reason.contains("empty command"));
1634 }
1635
1636 #[test]
1639 fn test_bun_keyword_detected() {
1640 let _guard = test_guard!();
1641 assert!(contains_compilation_keyword("bun test"));
1643 assert!(contains_compilation_keyword("bun typecheck"));
1644 assert!(contains_compilation_keyword("bun install")); }
1646
1647 #[test]
1648 fn test_bun_install_not_intercepted() {
1649 let _guard = test_guard!();
1650 let result = classify_command("bun install");
1652 assert!(!result.is_compilation);
1653 assert!(result.reason.contains("never-intercept"));
1654 }
1655
1656 #[test]
1657 fn test_bun_add_not_intercepted() {
1658 let _guard = test_guard!();
1659 let result = classify_command("bun add lodash");
1661 assert!(!result.is_compilation);
1662 assert!(result.reason.contains("never-intercept"));
1663 }
1664
1665 #[test]
1666 fn test_bun_remove_not_intercepted() {
1667 let _guard = test_guard!();
1668 let result = classify_command("bun remove lodash");
1669 assert!(!result.is_compilation);
1670 assert!(result.reason.contains("never-intercept"));
1671 }
1672
1673 #[test]
1674 fn test_bun_run_not_intercepted() {
1675 let _guard = test_guard!();
1676 let result = classify_command("bun run build");
1678 assert!(!result.is_compilation);
1679 assert!(result.reason.contains("never-intercept"));
1680 }
1681
1682 #[test]
1683 fn test_bun_build_not_intercepted() {
1684 let _guard = test_guard!();
1685 let result = classify_command("bun build ./src/index.ts");
1687 assert!(!result.is_compilation);
1688 assert!(result.reason.contains("never-intercept"));
1689 }
1690
1691 #[test]
1692 fn test_bun_version_not_intercepted() {
1693 let _guard = test_guard!();
1694 let result = classify_command("bun --version");
1695 assert!(!result.is_compilation);
1696 assert!(result.reason.contains("never-intercept"));
1697
1698 let result = classify_command("bun -v");
1699 assert!(!result.is_compilation);
1700 assert!(result.reason.contains("never-intercept"));
1701 }
1702
1703 #[test]
1704 fn test_bun_dev_not_intercepted() {
1705 let _guard = test_guard!();
1706 let result = classify_command("bun dev");
1708 assert!(!result.is_compilation);
1709 assert!(result.reason.contains("never-intercept"));
1710 }
1711
1712 #[test]
1713 fn test_bun_repl_not_intercepted() {
1714 let _guard = test_guard!();
1715 let result = classify_command("bun repl");
1717 assert!(!result.is_compilation);
1718 assert!(result.reason.contains("never-intercept"));
1719 }
1720
1721 #[test]
1722 fn test_bun_link_not_intercepted() {
1723 let _guard = test_guard!();
1724 let result = classify_command("bun link");
1725 assert!(!result.is_compilation);
1726 assert!(result.reason.contains("never-intercept"));
1727 }
1728
1729 #[test]
1730 fn test_bun_init_not_intercepted() {
1731 let _guard = test_guard!();
1732 let result = classify_command("bun init");
1733 assert!(!result.is_compilation);
1734 assert!(result.reason.contains("never-intercept"));
1735 }
1736
1737 #[test]
1738 fn test_bun_create_not_intercepted() {
1739 let _guard = test_guard!();
1740 let result = classify_command("bun create next-app");
1741 assert!(!result.is_compilation);
1742 assert!(result.reason.contains("never-intercept"));
1743 }
1744
1745 #[test]
1746 fn test_bun_pm_not_intercepted() {
1747 let _guard = test_guard!();
1748 let result = classify_command("bun pm cache");
1749 assert!(!result.is_compilation);
1750 assert!(result.reason.contains("never-intercept"));
1751 }
1752
1753 #[test]
1754 fn test_bun_help_not_intercepted() {
1755 let _guard = test_guard!();
1756 let result = classify_command("bun --help");
1757 assert!(!result.is_compilation);
1758 assert!(result.reason.contains("never-intercept"));
1759
1760 let result = classify_command("bun -h");
1761 assert!(!result.is_compilation);
1762 assert!(result.reason.contains("never-intercept"));
1763 }
1764
1765 #[test]
1768 fn test_bun_test_classification() {
1769 let _guard = test_guard!();
1770 let result = classify_command("bun test");
1772 assert!(result.is_compilation);
1773 assert_eq!(result.kind, Some(CompilationKind::BunTest));
1774 assert!((result.confidence - 0.95).abs() < 0.001);
1775
1776 let result = classify_command("bun test src/");
1778 assert!(result.is_compilation);
1779 assert_eq!(result.kind, Some(CompilationKind::BunTest));
1780
1781 let result = classify_command("bun test --coverage");
1783 assert!(result.is_compilation);
1784 assert_eq!(result.kind, Some(CompilationKind::BunTest));
1785
1786 let result = classify_command("bun test auth.test.ts");
1788 assert!(result.is_compilation);
1789 assert_eq!(result.kind, Some(CompilationKind::BunTest));
1790
1791 let result = classify_command("bun test --bail --timeout 5000");
1793 assert!(result.is_compilation);
1794 assert_eq!(result.kind, Some(CompilationKind::BunTest));
1795
1796 let result = classify_command("bun test --reporter json");
1798 assert!(result.is_compilation);
1799 assert_eq!(result.kind, Some(CompilationKind::BunTest));
1800 }
1801
1802 #[test]
1803 fn test_bun_test_watch_not_intercepted() {
1804 let _guard = test_guard!();
1805 let result = classify_command("bun test --watch");
1807 assert!(!result.is_compilation);
1808 assert!(result.reason.contains("interactive"));
1809
1810 let result = classify_command("bun test --watch --coverage");
1812 assert!(!result.is_compilation);
1813 assert!(result.reason.contains("interactive"));
1814
1815 let result = classify_command("bun test src/ --watch");
1817 assert!(!result.is_compilation);
1818
1819 let result = classify_command("bun test -w");
1821 assert!(!result.is_compilation);
1822
1823 let result = classify_command("bun test -w src/");
1825 assert!(!result.is_compilation);
1826 }
1827
1828 #[test]
1829 fn test_bun_typecheck_watch_not_intercepted() {
1830 let _guard = test_guard!();
1831 let result = classify_command("bun typecheck --watch");
1833 assert!(!result.is_compilation);
1834 assert!(result.reason.contains("interactive"));
1835
1836 let result = classify_command("bun typecheck -w");
1838 assert!(!result.is_compilation);
1839 }
1840
1841 #[test]
1842 fn test_bun_typecheck_classification() {
1843 let _guard = test_guard!();
1844 let result = classify_command("bun typecheck");
1846 assert!(result.is_compilation);
1847 assert_eq!(result.kind, Some(CompilationKind::BunTypecheck));
1848 assert!((result.confidence - 0.95).abs() < 0.001);
1849
1850 let result = classify_command("bun typecheck src/");
1852 assert!(result.is_compilation);
1853 assert_eq!(result.kind, Some(CompilationKind::BunTypecheck));
1854
1855 }
1858
1859 #[test]
1860 fn test_bun_edge_cases_not_matched() {
1861 let _guard = test_guard!();
1862 let result = classify_command("bun testing");
1864 assert!(!result.is_compilation);
1865 assert!(result.reason.contains("no matching pattern"));
1866
1867 let result = classify_command("bun typechecker");
1868 assert!(!result.is_compilation);
1869 assert!(result.reason.contains("no matching pattern"));
1870
1871 let result = classify_command("bun type");
1872 assert!(!result.is_compilation);
1873 assert!(result.reason.contains("no matching pattern"));
1874
1875 let result = classify_command("bun x eslint");
1877 assert!(!result.is_compilation);
1878 assert!(result.reason.contains("bun x runs arbitrary packages"));
1879
1880 let result = classify_command("bun x prettier --write .");
1881 assert!(!result.is_compilation);
1882 assert!(result.reason.contains("bun x runs arbitrary packages"));
1883
1884 let result = classify_command("bun x vitest run");
1885 assert!(!result.is_compilation);
1886 assert!(result.reason.contains("bun x runs arbitrary packages"));
1887 }
1888
1889 #[test]
1890 fn test_bun_test_vs_never_intercept() {
1891 let _guard = test_guard!();
1892 let result = classify_command("bun test");
1894 assert!(!result.reason.contains("never-intercept"));
1895 assert!(result.is_compilation);
1896 }
1897
1898 #[test]
1899 fn test_bun_typecheck_vs_never_intercept() {
1900 let _guard = test_guard!();
1901 let result = classify_command("bun typecheck");
1903 assert!(!result.reason.contains("never-intercept"));
1904 assert!(result.is_compilation);
1905 }
1906
1907 #[test]
1910 fn test_bun_compilation_kind_serde() {
1911 let _guard = test_guard!();
1912 let kind = CompilationKind::BunTest;
1914 let json = serde_json::to_string(&kind).unwrap();
1915 assert_eq!(json, "\"bun_test\"");
1916 let parsed: CompilationKind = serde_json::from_str(&json).unwrap();
1917 assert_eq!(parsed, CompilationKind::BunTest);
1918
1919 let kind = CompilationKind::BunTypecheck;
1921 let json = serde_json::to_string(&kind).unwrap();
1922 assert_eq!(json, "\"bun_typecheck\"");
1923 let parsed: CompilationKind = serde_json::from_str(&json).unwrap();
1924 assert_eq!(parsed, CompilationKind::BunTypecheck);
1925 }
1926
1927 #[test]
1928 fn test_bun_kinds_are_distinct() {
1929 let _guard = test_guard!();
1930 assert_ne!(CompilationKind::BunTest, CompilationKind::BunTypecheck);
1932 assert_ne!(CompilationKind::BunTest, CompilationKind::CargoTest);
1934 }
1935
1936 #[test]
1937 fn test_wrapped_command_classification_repro() {
1938 let _guard = test_guard!();
1939 let result = classify_command("time cargo build");
1942 assert!(
1943 result.is_compilation,
1944 "Should classify 'time cargo build' as compilation"
1945 );
1946 assert_eq!(result.kind, Some(CompilationKind::CargoBuild));
1947
1948 let result = classify_command("sudo cargo check");
1949 assert!(
1950 result.is_compilation,
1951 "Should classify 'sudo cargo check' as compilation"
1952 );
1953
1954 let result = classify_command("env RUST_BACKTRACE=1 cargo test");
1955 assert!(
1956 result.is_compilation,
1957 "Should classify env-wrapped cargo test as compilation"
1958 );
1959
1960 let result = classify_command("env 'CARGO_TARGET_DIR=/data/tmp/rch-target' cargo build");
1961 assert!(
1962 result.is_compilation,
1963 "Should classify shell-quoted env assignment before cargo build as compilation"
1964 );
1965 assert_eq!(result.kind, Some(CompilationKind::CargoBuild));
1966 }
1967
1968 #[test]
1973 fn test_bun_piped_commands_not_intercepted() {
1974 let _guard = test_guard!();
1975 let result = classify_command("bun test | grep error");
1977 assert!(!result.is_compilation);
1978 assert!(
1979 result.reason.contains("piped"),
1980 "Should be rejected as piped command"
1981 );
1982
1983 let result = classify_command("bun test 2>&1 | tee output.log");
1985 assert!(!result.is_compilation);
1986 assert!(result.reason.contains("piped"));
1987
1988 let result = classify_command("bun typecheck | head -20");
1990 assert!(!result.is_compilation);
1991 assert!(result.reason.contains("piped"));
1992 }
1993
1994 #[test]
1995 fn test_bunx_tsc_not_intercepted() {
1996 let _guard = test_guard!();
1997 let result = classify_command("bun x tsc --noEmit");
2000 assert!(!result.is_compilation);
2001 assert!(result.reason.contains("bun x"));
2002
2003 let result = classify_command("bun x eslint .");
2005 assert!(!result.is_compilation);
2006
2007 let result = classify_command("bun x prettier --write .");
2008 assert!(!result.is_compilation);
2009
2010 let result = classify_command("bun x vitest run");
2011 assert!(!result.is_compilation);
2012 }
2013
2014 #[test]
2015 fn test_bun_redirected_not_intercepted() {
2016 let _guard = test_guard!();
2017 let result = classify_command("bun test > results.txt");
2019 assert!(!result.is_compilation);
2020 assert!(result.reason.contains("redirect"));
2021
2022 let result = classify_command("bun typecheck > errors.log");
2023 assert!(!result.is_compilation);
2024 }
2025
2026 #[test]
2027 fn test_bun_backgrounded_not_intercepted() {
2028 let _guard = test_guard!();
2029 let result = classify_command("bun test &");
2031 assert!(!result.is_compilation);
2032 assert!(result.reason.contains("background"));
2033 }
2034
2035 #[test]
2036 fn test_bun_chained_commands_classified() {
2037 let _guard = test_guard!();
2038 let result = classify_command("bun test && echo done");
2040 assert!(
2041 !result.is_compilation,
2042 "chained commands should be rejected"
2043 );
2044 assert!(result.reason.contains("chained"));
2045
2046 let result = classify_command("bun typecheck; bun test");
2047 assert!(
2048 !result.is_compilation,
2049 "chained commands should be rejected"
2050 );
2051 assert!(result.reason.contains("chained"));
2052 }
2053
2054 #[test]
2055 fn test_bun_subshell_capture_not_intercepted() {
2056 let _guard = test_guard!();
2057 let result = classify_command("bun test $(echo src/)");
2059 assert!(!result.is_compilation);
2060 assert!(result.reason.contains("subshell"));
2061 }
2062
2063 #[test]
2064 fn test_bun_wrapped_commands() {
2065 let _guard = test_guard!();
2066 let result = classify_command("time bun test");
2069 if result.is_compilation {
2072 assert_eq!(result.kind, Some(CompilationKind::BunTest));
2073 }
2074
2075 let result = classify_command("env DEBUG=1 bun test");
2076 if result.is_compilation {
2077 assert_eq!(result.kind, Some(CompilationKind::BunTest));
2078 }
2079 }
2080
2081 #[test]
2082 fn test_classify_command_detailed_matches_basic() {
2083 let _guard = test_guard!();
2084 let commands = [
2085 "cargo build",
2086 "cargo test --release",
2087 "bun typecheck",
2088 "gcc -c main.c -o main.o",
2089 "ls -la",
2090 ];
2091
2092 for cmd in commands {
2093 let basic = classify_command(cmd);
2094 let detailed = classify_command_detailed(cmd);
2095 assert_eq!(basic, detailed.classification);
2096 }
2097 }
2098
2099 #[test]
2100 fn test_classify_command_detailed_rejects_piped() {
2101 let _guard = test_guard!();
2102 let detailed = classify_command_detailed("cargo build | tee log.txt");
2103 assert!(!detailed.classification.is_compilation);
2104 let tier1 = detailed.tiers.iter().find(|t| t.tier == 1).unwrap();
2105 assert_eq!(tier1.decision, TierDecision::Reject);
2106 assert!(tier1.reason.contains("piped"));
2107 }
2108
2109 #[test]
2110 fn test_classify_command_detailed_normalizes_wrappers() {
2111 let _guard = test_guard!();
2112 let detailed = classify_command_detailed("sudo cargo check");
2113 assert_eq!(detailed.normalized, "cargo check");
2114 assert!(detailed.classification.is_compilation);
2115 }
2116
2117 #[test]
2122 fn test_cargo_test_release() {
2123 let _guard = test_guard!();
2124 let result = classify_command("cargo test --release");
2125 assert!(
2126 result.is_compilation,
2127 "cargo test --release should be classified as compilation"
2128 );
2129 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2130 assert!(result.confidence >= 0.90);
2131 }
2132
2133 #[test]
2134 fn test_cargo_test_specific_test_name() {
2135 let _guard = test_guard!();
2136 let result = classify_command("cargo test my_test_function");
2137 assert!(
2138 result.is_compilation,
2139 "cargo test with specific test name should be classified"
2140 );
2141 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2142 }
2143
2144 #[test]
2145 fn test_cargo_test_with_nocapture() {
2146 let _guard = test_guard!();
2147 let result = classify_command("cargo test -- --nocapture");
2148 assert!(
2149 result.is_compilation,
2150 "cargo test -- --nocapture should be classified"
2151 );
2152 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2153 }
2154
2155 #[test]
2156 fn test_cargo_test_workspace() {
2157 let _guard = test_guard!();
2158 let result = classify_command("cargo test --workspace");
2159 assert!(
2160 result.is_compilation,
2161 "cargo test --workspace should be classified"
2162 );
2163 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2164 }
2165
2166 #[test]
2167 fn test_cargo_test_package() {
2168 let _guard = test_guard!();
2169 let result = classify_command("cargo test -p rch-common");
2170 assert!(result.is_compilation, "cargo test -p should be classified");
2171 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2172 }
2173
2174 #[test]
2175 fn test_cargo_test_short_alias() {
2176 let _guard = test_guard!();
2177 let result = classify_command("cargo t");
2179 assert!(
2180 result.is_compilation,
2181 "cargo t (short alias) should be classified"
2182 );
2183 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2184 }
2185
2186 #[test]
2187 fn test_cargo_test_all_features() {
2188 let _guard = test_guard!();
2189 let result = classify_command("cargo test --all-features");
2190 assert!(
2191 result.is_compilation,
2192 "cargo test --all-features should be classified"
2193 );
2194 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2195 }
2196
2197 #[test]
2198 fn test_cargo_test_no_default_features() {
2199 let _guard = test_guard!();
2200 let result = classify_command("cargo test --no-default-features");
2201 assert!(
2202 result.is_compilation,
2203 "cargo test --no-default-features should be classified"
2204 );
2205 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2206 }
2207
2208 #[test]
2209 fn test_cargo_test_with_env_var() {
2210 let _guard = test_guard!();
2211 let result = classify_command("RUST_BACKTRACE=1 cargo test");
2212 assert!(
2213 result.is_compilation,
2214 "cargo test with env var should be classified"
2215 );
2216 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2217 }
2218
2219 #[test]
2220 fn test_cargo_test_with_multiple_flags() {
2221 let _guard = test_guard!();
2222 let result = classify_command("cargo test --release --workspace -p rch -- --nocapture");
2223 assert!(
2224 result.is_compilation,
2225 "cargo test with multiple flags should be classified"
2226 );
2227 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2228 }
2229
2230 #[test]
2231 fn test_cargo_test_with_jobs() {
2232 let _guard = test_guard!();
2233 let result = classify_command("cargo test -j 8");
2234 assert!(result.is_compilation, "cargo test -j should be classified");
2235 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2236 }
2237
2238 #[test]
2239 fn test_cargo_test_target() {
2240 let _guard = test_guard!();
2241 let result = classify_command("cargo test --target x86_64-unknown-linux-gnu");
2242 assert!(
2243 result.is_compilation,
2244 "cargo test --target should be classified"
2245 );
2246 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2247 }
2248
2249 #[test]
2250 fn test_cargo_test_lib() {
2251 let _guard = test_guard!();
2252 let result = classify_command("cargo test --lib");
2253 assert!(
2254 result.is_compilation,
2255 "cargo test --lib should be classified"
2256 );
2257 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2258 }
2259
2260 #[test]
2261 fn test_cargo_test_bins() {
2262 let _guard = test_guard!();
2263 let result = classify_command("cargo test --bins");
2264 assert!(
2265 result.is_compilation,
2266 "cargo test --bins should be classified"
2267 );
2268 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2269 }
2270
2271 #[test]
2272 fn test_cargo_test_doc() {
2273 let _guard = test_guard!();
2274 let result = classify_command("cargo test --doc");
2275 assert!(
2276 result.is_compilation,
2277 "cargo test --doc should be classified"
2278 );
2279 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2280 }
2281
2282 #[test]
2283 fn test_cargo_test_filter_pattern() {
2284 let _guard = test_guard!();
2285 let result = classify_command("cargo test test_classification");
2286 assert!(
2287 result.is_compilation,
2288 "cargo test with filter pattern should be classified"
2289 );
2290 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2291 }
2292
2293 #[test]
2294 fn test_cargo_test_exact() {
2295 let _guard = test_guard!();
2296 let result = classify_command("cargo test --exact my_test");
2297 assert!(
2298 result.is_compilation,
2299 "cargo test --exact should be classified"
2300 );
2301 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
2302 }
2303
2304 #[test]
2309 fn test_nextest_keyword_detected() {
2310 let _guard = test_guard!();
2311 assert!(contains_compilation_keyword("cargo nextest run"));
2313 assert!(contains_compilation_keyword("cargo nextest list"));
2314 }
2315
2316 #[test]
2317 fn test_cargo_nextest_run_classification() {
2318 let _guard = test_guard!();
2319 let result = classify_command("cargo nextest run");
2321 assert!(
2322 result.is_compilation,
2323 "cargo nextest run should be classified"
2324 );
2325 assert_eq!(result.kind, Some(CompilationKind::CargoNextest));
2326 assert!((result.confidence - 0.95).abs() < 0.001);
2327 }
2328
2329 #[test]
2330 fn test_cargo_nextest_run_with_flags() {
2331 let _guard = test_guard!();
2332 let result = classify_command("cargo nextest run --release");
2334 assert!(result.is_compilation);
2335 assert_eq!(result.kind, Some(CompilationKind::CargoNextest));
2336
2337 let result = classify_command("cargo nextest run --cargo-profile ci");
2339 assert!(result.is_compilation);
2340 assert_eq!(result.kind, Some(CompilationKind::CargoNextest));
2341
2342 let result = classify_command("cargo nextest run --workspace");
2344 assert!(result.is_compilation);
2345 assert_eq!(result.kind, Some(CompilationKind::CargoNextest));
2346
2347 let result = classify_command("cargo nextest run -p rch-common");
2349 assert!(result.is_compilation);
2350 assert_eq!(result.kind, Some(CompilationKind::CargoNextest));
2351
2352 let result = classify_command("cargo nextest run test_classification");
2354 assert!(result.is_compilation);
2355 assert_eq!(result.kind, Some(CompilationKind::CargoNextest));
2356
2357 let result = classify_command("cargo nextest run --release --no-fail-fast -j 8");
2359 assert!(result.is_compilation);
2360 assert_eq!(result.kind, Some(CompilationKind::CargoNextest));
2361 }
2362
2363 #[test]
2364 fn test_cargo_nextest_run_short_alias() {
2365 let _guard = test_guard!();
2366 let result = classify_command("cargo nextest r");
2368 assert!(
2369 result.is_compilation,
2370 "cargo nextest r should be classified"
2371 );
2372 assert_eq!(result.kind, Some(CompilationKind::CargoNextest));
2373 }
2374
2375 #[test]
2376 fn test_cargo_nextest_list_not_intercepted() {
2377 let _guard = test_guard!();
2378 let result = classify_command("cargo nextest list");
2380 assert!(!result.is_compilation);
2381 assert!(result.reason.contains("never-intercept"));
2382 }
2383
2384 #[test]
2385 fn test_cargo_nextest_archive_not_intercepted() {
2386 let _guard = test_guard!();
2387 let result = classify_command("cargo nextest archive");
2389 assert!(!result.is_compilation);
2390 assert!(result.reason.contains("never-intercept"));
2391 }
2392
2393 #[test]
2394 fn test_cargo_nextest_show_not_intercepted() {
2395 let _guard = test_guard!();
2396 let result = classify_command("cargo nextest show");
2398 assert!(!result.is_compilation);
2399 assert!(result.reason.contains("never-intercept"));
2400 }
2401
2402 #[test]
2403 fn test_bare_cargo_nextest_not_intercepted() {
2404 let _guard = test_guard!();
2405 let result = classify_command("cargo nextest");
2407 assert!(!result.is_compilation);
2408 assert!(result.reason.contains("without subcommand"));
2409 }
2410
2411 #[test]
2412 fn test_cargo_nextest_wrapped_commands() {
2413 let _guard = test_guard!();
2414 let result = classify_command("time cargo nextest run");
2416 assert!(
2417 result.is_compilation,
2418 "time cargo nextest run should be classified"
2419 );
2420 assert_eq!(result.kind, Some(CompilationKind::CargoNextest));
2421
2422 let result = classify_command("RUST_BACKTRACE=1 cargo nextest run");
2423 assert!(
2424 result.is_compilation,
2425 "env-wrapped cargo nextest run should be classified"
2426 );
2427 assert_eq!(result.kind, Some(CompilationKind::CargoNextest));
2428 }
2429
2430 #[test]
2431 fn test_cargo_nextest_compilation_kind_serde() {
2432 let _guard = test_guard!();
2433 let kind = CompilationKind::CargoNextest;
2435 let json = serde_json::to_string(&kind).unwrap();
2436 assert_eq!(json, "\"cargo_nextest\"");
2437 let parsed: CompilationKind = serde_json::from_str(&json).unwrap();
2438 assert_eq!(parsed, CompilationKind::CargoNextest);
2439 }
2440
2441 #[test]
2442 fn test_cargo_nextest_vs_cargo_test_distinct() {
2443 let _guard = test_guard!();
2444 assert_ne!(CompilationKind::CargoNextest, CompilationKind::CargoTest);
2446
2447 let nextest_result = classify_command("cargo nextest run");
2449 let test_result = classify_command("cargo test");
2450 assert!(nextest_result.is_compilation);
2451 assert!(test_result.is_compilation);
2452 assert_eq!(nextest_result.kind, Some(CompilationKind::CargoNextest));
2453 assert_eq!(test_result.kind, Some(CompilationKind::CargoTest));
2454 }
2455
2456 #[test]
2457 fn test_cargo_nextest_piped_not_intercepted() {
2458 let _guard = test_guard!();
2459 let result = classify_command("cargo nextest run | grep FAIL");
2461 assert!(!result.is_compilation);
2462 assert!(result.reason.contains("piped"));
2463 }
2464
2465 #[test]
2466 fn test_cargo_nextest_redirected_not_intercepted() {
2467 let _guard = test_guard!();
2468 let result = classify_command("cargo nextest run > results.txt");
2470 assert!(!result.is_compilation);
2471 assert!(result.reason.contains("redirect"));
2472 }
2473
2474 #[test]
2475 fn test_cargo_nextest_backgrounded_not_intercepted() {
2476 let _guard = test_guard!();
2477 let result = classify_command("cargo nextest run &");
2479 assert!(!result.is_compilation);
2480 assert!(result.reason.contains("background"));
2481 }
2482
2483 #[test]
2488 fn test_cargo_bench_classification() {
2489 let _guard = test_guard!();
2490 let result = classify_command("cargo bench");
2491 assert!(result.is_compilation, "cargo bench should be classified");
2492 assert_eq!(result.kind, Some(CompilationKind::CargoBench));
2493 assert!((result.confidence - 0.90).abs() < 0.001);
2494 }
2495
2496 #[test]
2497 fn test_cargo_bench_with_filter() {
2498 let _guard = test_guard!();
2499 let result = classify_command("cargo bench my_bench");
2501 assert!(result.is_compilation);
2502 assert_eq!(result.kind, Some(CompilationKind::CargoBench));
2503 }
2504
2505 #[test]
2506 fn test_cargo_bench_with_flags() {
2507 let _guard = test_guard!();
2508 let result = classify_command("cargo bench --release");
2510 assert!(result.is_compilation);
2511 assert_eq!(result.kind, Some(CompilationKind::CargoBench));
2512
2513 let result = classify_command("cargo bench -p rch-common");
2515 assert!(result.is_compilation);
2516 assert_eq!(result.kind, Some(CompilationKind::CargoBench));
2517
2518 let result = classify_command("cargo bench --features benchmarks");
2520 assert!(result.is_compilation);
2521 assert_eq!(result.kind, Some(CompilationKind::CargoBench));
2522
2523 let result = classify_command("cargo bench --bench criterion_bench");
2525 assert!(result.is_compilation);
2526 assert_eq!(result.kind, Some(CompilationKind::CargoBench));
2527 }
2528
2529 #[test]
2530 fn test_cargo_bench_wrapped() {
2531 let _guard = test_guard!();
2532 let result = classify_command("time cargo bench");
2534 assert!(
2535 result.is_compilation,
2536 "time cargo bench should be classified"
2537 );
2538 assert_eq!(result.kind, Some(CompilationKind::CargoBench));
2539
2540 let result = classify_command("CARGO_INCREMENTAL=0 cargo bench");
2542 assert!(result.is_compilation);
2543 assert_eq!(result.kind, Some(CompilationKind::CargoBench));
2544 }
2545
2546 #[test]
2547 fn test_cargo_bench_serde() {
2548 let _guard = test_guard!();
2549 let kind = CompilationKind::CargoBench;
2551 let json = serde_json::to_string(&kind).unwrap();
2552 assert_eq!(json, "\"cargo_bench\"");
2553 let parsed: CompilationKind = serde_json::from_str(&json).unwrap();
2554 assert_eq!(parsed, CompilationKind::CargoBench);
2555 }
2556
2557 #[test]
2558 fn test_cargo_bench_distinct_from_test() {
2559 let _guard = test_guard!();
2560 assert_ne!(CompilationKind::CargoBench, CompilationKind::CargoTest);
2562
2563 let bench_result = classify_command("cargo bench");
2565 let test_result = classify_command("cargo test");
2566 assert!(bench_result.is_compilation);
2567 assert!(test_result.is_compilation);
2568 assert_eq!(bench_result.kind, Some(CompilationKind::CargoBench));
2569 assert_eq!(test_result.kind, Some(CompilationKind::CargoTest));
2570 }
2571
2572 #[test]
2573 fn test_cargo_bench_piped_not_intercepted() {
2574 let _guard = test_guard!();
2575 let result = classify_command("cargo bench | tee output.txt");
2577 assert!(!result.is_compilation);
2578 assert!(result.reason.contains("piped"));
2579 }
2580
2581 #[test]
2582
2583 fn test_normalize_path_and_wrapper_bug_repro() {
2584 let _guard = test_guard!();
2585 let normalized = normalize_command("/usr/bin/time cargo build");
2594
2595 assert_eq!(normalized, "cargo build");
2596
2597 let result = classify_command("/usr/bin/time cargo build");
2598
2599 assert!(result.is_compilation);
2600 }
2601
2602 #[test]
2607 fn test_command_base_rust() {
2608 let _guard = test_guard!();
2609 assert_eq!(CompilationKind::CargoBuild.command_base(), "cargo");
2610 assert_eq!(CompilationKind::CargoTest.command_base(), "cargo");
2611 assert_eq!(CompilationKind::CargoCheck.command_base(), "cargo");
2612 assert_eq!(CompilationKind::CargoClippy.command_base(), "cargo");
2613 assert_eq!(CompilationKind::CargoDoc.command_base(), "cargo");
2614 assert_eq!(CompilationKind::CargoBench.command_base(), "cargo");
2615 assert_eq!(CompilationKind::CargoNextest.command_base(), "cargo");
2616 assert_eq!(CompilationKind::Rustc.command_base(), "rustc");
2617 }
2618
2619 #[test]
2620 fn test_command_base_c_cpp() {
2621 let _guard = test_guard!();
2622 assert_eq!(CompilationKind::Gcc.command_base(), "gcc");
2623 assert_eq!(CompilationKind::Gpp.command_base(), "g++");
2624 assert_eq!(CompilationKind::Clang.command_base(), "clang");
2625 assert_eq!(CompilationKind::Clangpp.command_base(), "clang++");
2626 }
2627
2628 #[test]
2629 fn test_command_base_build_systems() {
2630 let _guard = test_guard!();
2631 assert_eq!(CompilationKind::Make.command_base(), "make");
2632 assert_eq!(CompilationKind::CmakeBuild.command_base(), "cmake");
2633 assert_eq!(CompilationKind::Ninja.command_base(), "ninja");
2634 assert_eq!(CompilationKind::Meson.command_base(), "meson");
2635 }
2636
2637 #[test]
2638 fn test_command_base_bun() {
2639 let _guard = test_guard!();
2640 assert_eq!(CompilationKind::BunTest.command_base(), "bun");
2641 assert_eq!(CompilationKind::BunTypecheck.command_base(), "bun");
2642 }
2643
2644 mod proptest_classification {
2649 use super::*;
2650 use proptest::prelude::*;
2651
2652 fn arbitrary_string() -> impl Strategy<Value = String> {
2654 prop::string::string_regex(".{0,500}").unwrap()
2655 }
2656
2657 fn command_like_string() -> impl Strategy<Value = String> {
2659 let prefixes = prop::sample::select(vec![
2660 "cargo", "rustc", "gcc", "g++", "clang", "make", "cmake", "ninja", "bun", "ls",
2661 "cd", "echo", "cat", "grep", "find", "rm", "mv", "cp", "mkdir",
2662 ]);
2663 (prefixes, "[ a-zA-Z0-9_.-]{0,200}")
2664 .prop_map(|(prefix, suffix)| format!("{}{}", prefix, suffix))
2665 }
2666
2667 fn known_command_with_flags() -> impl Strategy<Value = String> {
2669 let base_commands = prop::sample::select(vec![
2670 "cargo build",
2671 "cargo test",
2672 "cargo check",
2673 "cargo clippy",
2674 "cargo run",
2675 "rustc",
2676 "gcc",
2677 "g++",
2678 "make",
2679 "bun test",
2680 "bun typecheck",
2681 ]);
2682 let flags = prop::collection::vec(
2683 prop::sample::select(vec![
2684 "--release",
2685 "--verbose",
2686 "-j8",
2687 "-p",
2688 "--all",
2689 "--workspace",
2690 "--lib",
2691 "--bin",
2692 "-o",
2693 "-c",
2694 ]),
2695 0..5,
2696 );
2697 (base_commands, flags).prop_map(|(cmd, flags)| {
2698 if flags.is_empty() {
2699 cmd.to_string()
2700 } else {
2701 format!("{} {}", cmd, flags.join(" "))
2702 }
2703 })
2704 }
2705
2706 fn unicode_and_control_chars() -> impl Strategy<Value = String> {
2708 prop::string::string_regex(
2709 r"[\x00-\x1f\x80-\xff\u{100}-\u{10FFFF}a-zA-Z0-9 |>&;$()]{0,100}",
2710 )
2711 .unwrap()
2712 }
2713
2714 fn shell_special_commands() -> impl Strategy<Value = String> {
2716 let components = prop::sample::select(vec![
2717 "cargo build",
2718 "ls -la",
2719 "echo test",
2720 "cat file.txt",
2721 "grep pattern",
2722 ]);
2723 let operators = prop::sample::select(vec![
2724 " | ", " && ", " || ", " ; ", " > ", " < ", " & ", " 2>&1 ", " $(", " `",
2725 ]);
2726 prop::collection::vec((components, operators), 1..4).prop_map(|pairs| {
2727 let mut result = String::new();
2728 for (i, (comp, op)) in pairs.iter().enumerate() {
2729 if i > 0 {
2730 result.push_str(op);
2731 }
2732 result.push_str(comp);
2733 }
2734 result
2735 })
2736 }
2737
2738 fn wrapped_commands() -> impl Strategy<Value = String> {
2740 let wrappers = prop::sample::select(vec![
2741 "sudo ",
2742 "env ",
2743 "time ",
2744 "nice ",
2745 "/usr/bin/time ",
2746 "RUST_BACKTRACE=1 ",
2747 "CC=clang ",
2748 "",
2749 ]);
2750 let commands = prop::sample::select(vec![
2751 "cargo build",
2752 "cargo test",
2753 "make",
2754 "gcc -c main.c",
2755 "ls -la",
2756 ]);
2757 (wrappers, commands).prop_map(|(wrapper, cmd)| format!("{}{}", wrapper, cmd))
2758 }
2759
2760 proptest! {
2761 #![proptest_config(ProptestConfig::with_cases(1000))]
2763
2764 #[test]
2766 fn test_classify_arbitrary_no_panic(s in arbitrary_string()) {
2767 let _ = classify_command(&s);
2769 }
2770
2771 #[test]
2773 fn test_classify_command_like_no_panic(s in command_like_string()) {
2774 let _ = classify_command(&s);
2775 }
2776
2777 #[test]
2779 fn test_classify_known_commands_valid(s in known_command_with_flags()) {
2780 let result = classify_command(&s);
2781 prop_assert!(result.confidence >= 0.0 && result.confidence <= 1.0,
2783 "Confidence {} out of range for command: {}", result.confidence, s);
2784 if result.is_compilation {
2786 prop_assert!(result.kind.is_some(),
2787 "is_compilation=true but kind=None for: {}", s);
2788 }
2789 }
2790
2791 #[test]
2793 fn test_classify_unicode_no_panic(s in unicode_and_control_chars()) {
2794 let _ = classify_command(&s);
2795 }
2796
2797 #[test]
2799 fn test_classify_shell_special(s in shell_special_commands()) {
2800 let result = classify_command(&s);
2801 if s.contains(" | ") || s.contains(" > ") || s.contains(" < ") || s.contains(" & ") {
2804 let _ = result; }
2807 }
2808
2809 #[test]
2811 fn test_classify_wrapped_commands(s in wrapped_commands()) {
2812 let result = classify_command(&s);
2813 prop_assert!(result.confidence >= 0.0 && result.confidence <= 1.0);
2814 }
2815
2816 #[test]
2818 fn test_classify_deterministic(s in arbitrary_string()) {
2819 let result1 = classify_command(&s);
2820 let result2 = classify_command(&s);
2821 prop_assert_eq!(result1, result2,
2822 "Non-deterministic classification for: {}", s);
2823 }
2824
2825 #[test]
2827 fn test_classify_whitespace_variants(s in "[ \t\n\r]{0,20}") {
2828 let _guard = test_guard!();
2829 let result = classify_command(&s);
2830 prop_assert!(!result.is_compilation,
2831 "Whitespace-only command should not be classified as compilation: {:?}", s);
2832 prop_assert!(result.reason.contains("empty") || result.reason.contains("keyword"),
2833 "Unexpected reason for whitespace: {}", result.reason);
2834 }
2835
2836 #[test]
2838 fn test_classify_long_commands(
2839 prefix in "cargo (build|test|check)",
2840 suffix in "[a-zA-Z0-9_ -]{0,10000}"
2841 ) {
2842 let long_cmd = format!("{} {}", prefix, suffix);
2843 let result = classify_command(&long_cmd);
2844 prop_assert!(result.confidence >= 0.0 && result.confidence <= 1.0);
2845 }
2846
2847 #[test]
2849 fn test_classify_special_bytes(s in prop::collection::vec(any::<u8>(), 0..200)) {
2850 if let Ok(valid_str) = String::from_utf8(s.clone()) {
2851 let _ = classify_command(&valid_str);
2852 }
2853 }
2855 }
2856
2857 proptest! {
2860 #![proptest_config(ProptestConfig::with_cases(500))]
2861
2862 #[test]
2864 fn test_cargo_subcommand_robustness(
2865 subcommand in "(build|test|check|clippy|fmt|clean|run|doc|bench|nextest)",
2866 suffix in "[a-zA-Z0-9_ -]{0,50}"
2867 ) {
2868 let cmd = format!("cargo {} {}", subcommand, suffix);
2869 let result = classify_command(&cmd);
2870 prop_assert!(result.confidence >= 0.0 && result.confidence <= 1.0);
2872 if subcommand == "fmt" || subcommand == "clean" {
2874 prop_assert!(!result.is_compilation,
2875 "cargo {} should not be compilation: {}", subcommand, cmd);
2876 }
2877 }
2878
2879 #[test]
2881 fn test_bun_command_robustness(
2882 subcommand in "(test|typecheck|install|add|remove|run|build|dev)",
2883 suffix in "[a-zA-Z0-9_ -]{0,30}"
2884 ) {
2885 let cmd = format!("bun {} {}", subcommand, suffix);
2886 let result = classify_command(&cmd);
2887 prop_assert!(result.confidence >= 0.0 && result.confidence <= 1.0);
2888 if matches!(subcommand.as_str(), "install" | "add" | "remove" | "run" | "build" | "dev") {
2890 prop_assert!(!result.is_compilation,
2891 "bun {} should not be compilation", subcommand);
2892 }
2893 }
2894
2895 #[test]
2897 fn test_c_compiler_robustness(
2898 compiler in "(gcc|g\\+\\+|clang|clang\\+\\+)",
2899 flags in "(-[cCoOgW][a-z0-9]* )*",
2900 file in "[a-zA-Z_][a-zA-Z0-9_]*\\.(c|cpp|cc|h|hpp)"
2901 ) {
2902 let cmd = format!("{} {}{}", compiler, flags, file);
2903 let result = classify_command(&cmd);
2904 prop_assert!(result.confidence >= 0.0 && result.confidence <= 1.0);
2905 }
2906
2907 #[test]
2909 fn test_quoted_args_handling(
2910 base in "(cargo build|cargo test|gcc|make)",
2911 quoted_content in "[a-zA-Z0-9 _-]{0,30}"
2912 ) {
2913 let cmd_single = format!("{} '{}'", base, quoted_content);
2915 let _ = classify_command(&cmd_single);
2916
2917 let cmd_double = format!("{} \"{}\"", base, quoted_content);
2919 let _ = classify_command(&cmd_double);
2920 }
2921 }
2922
2923 #[test]
2925 fn test_known_edge_cases() {
2926 let _guard = test_guard!();
2927 let edge_cases = [
2929 "",
2930 " ",
2931 "\t",
2932 "\n",
2933 "cargo",
2934 "cargo ",
2935 "cargo ",
2936 "cargo\tbuild",
2937 "cargo\nbuild",
2938 "cargo\rbuild",
2939 "cargo+nightly",
2940 "cargo +nightly",
2941 "cargo +nightly build",
2942 " cargo build ",
2943 "CARGO_TARGET_DIR=/tmp cargo build",
2944 "/usr/local/bin/cargo build",
2945 "~/.cargo/bin/cargo build",
2946 "./cargo build",
2947 "../cargo build",
2948 "cargo build 'test file.rs'",
2949 "cargo build \"test file.rs\"",
2950 "cargo build test\\ file.rs",
2951 "cargo build -- --test-threads=1",
2952 "cargo build --features \"feat1 feat2\"",
2953 "cargo build 2>&1",
2954 "cargo build 2>/dev/null",
2955 "gcc -DFOO=\"bar baz\" main.c",
2956 "make -j$(nproc)",
2957 "ninja -C build",
2958 "cmake --build . --target all",
2959 "bun test --timeout 5000 src/",
2960 "env -i PATH=/usr/bin cargo build",
2961 ];
2962
2963 for cmd in edge_cases {
2964 let result = classify_command(cmd);
2965 assert!(
2966 result.confidence >= 0.0 && result.confidence <= 1.0,
2967 "Invalid confidence for: {:?}",
2968 cmd
2969 );
2970 }
2971 }
2972
2973 #[test]
2975 fn test_detailed_matches_simple_proptest() {
2976 let _guard = test_guard!();
2977 let test_cases = [
2978 "cargo build --release",
2979 "cargo test",
2980 "make -j8",
2981 "ls -la",
2982 "echo hello",
2983 ];
2984
2985 for cmd in test_cases {
2986 let simple = classify_command(cmd);
2987 let detailed = classify_command_detailed(cmd);
2988 assert_eq!(simple, detailed.classification, "Mismatch for: {}", cmd);
2989 }
2990 }
2991
2992 #[test]
2995 fn test_split_single_command() {
2996 let _guard = test_guard!();
2997 assert_eq!(split_shell_commands("cargo build"), vec!["cargo build"]);
2998 }
2999
3000 #[test]
3001 fn test_split_and_operator() {
3002 let _guard = test_guard!();
3003 assert_eq!(
3004 split_shell_commands("cargo fmt && cargo build"),
3005 vec!["cargo fmt", "cargo build"]
3006 );
3007 }
3008
3009 #[test]
3010 fn test_split_quoted_operator() {
3011 let _guard = test_guard!();
3012 assert_eq!(
3013 split_shell_commands("echo '&&' && cargo build"),
3014 vec!["echo '&&'", "cargo build"]
3015 );
3016 }
3017
3018 #[test]
3019 fn test_split_mixed_operators() {
3020 let _guard = test_guard!();
3021 assert_eq!(
3022 split_shell_commands("cd /tmp && make -j4 || echo fail"),
3023 vec!["cd /tmp", "make -j4", "echo fail"]
3024 );
3025 }
3026
3027 #[test]
3028 fn test_split_semicolons() {
3029 let _guard = test_guard!();
3030 assert_eq!(split_shell_commands("a ; b ; c"), vec!["a", "b", "c"]);
3031 }
3032
3033 #[test]
3034 fn test_split_quoted_semicolon() {
3035 let _guard = test_guard!();
3036 assert_eq!(
3037 split_shell_commands("echo 'hello;world' && make"),
3038 vec!["echo 'hello;world'", "make"]
3039 );
3040 }
3041
3042 #[test]
3043 fn test_split_pipe_preserved() {
3044 let _guard = test_guard!();
3045 assert_eq!(
3047 split_shell_commands("cargo build 2>&1 | tee log"),
3048 vec!["cargo build 2>&1 | tee log"]
3049 );
3050 }
3051
3052 #[test]
3053 fn test_split_double_quoted_operator() {
3054 let _guard = test_guard!();
3055 assert_eq!(
3056 split_shell_commands(r#"echo "&&" && cargo build"#),
3057 vec![r#"echo "&&""#, "cargo build"]
3058 );
3059 }
3060
3061 #[test]
3062 fn test_split_nested_quotes() {
3063 let _guard = test_guard!();
3064 assert_eq!(
3065 split_shell_commands("echo \"he said 'hello && bye'\" && cargo test"),
3066 vec!["echo \"he said 'hello && bye'\"", "cargo test"]
3067 );
3068 }
3069
3070 #[test]
3071 fn test_split_escaped_quote() {
3072 let _guard = test_guard!();
3073 assert_eq!(
3075 split_shell_commands(r"echo it\'s && cargo build"),
3076 vec![r"echo it\'s", "cargo build"]
3077 );
3078 }
3079
3080 #[test]
3081 fn test_split_empty_string() {
3082 let _guard = test_guard!();
3083 assert!(split_shell_commands("").is_empty());
3084 }
3085
3086 #[test]
3087 fn test_split_only_whitespace() {
3088 let _guard = test_guard!();
3089 assert!(split_shell_commands(" ").is_empty());
3090 }
3091
3092 #[test]
3093 fn test_split_backtick_quoting() {
3094 let _guard = test_guard!();
3095 assert_eq!(
3096 split_shell_commands("echo `echo && fail` && cargo build"),
3097 vec!["echo `echo && fail`", "cargo build"]
3098 );
3099 }
3100
3101 #[test]
3102 fn test_split_trailing_operator() {
3103 let _guard = test_guard!();
3104 assert_eq!(split_shell_commands("cargo build &&"), vec!["cargo build"]);
3106 }
3107
3108 #[test]
3111 fn test_split_unclosed_single_quote() {
3112 let _guard = test_guard!();
3113 let result = split_shell_commands("echo 'hello && cargo build");
3115 assert_eq!(result, vec!["echo 'hello && cargo build"]);
3116 }
3117
3118 #[test]
3119 fn test_split_unclosed_double_quote() {
3120 let _guard = test_guard!();
3121 let result = split_shell_commands("echo \"hello && cargo build");
3122 assert_eq!(result, vec!["echo \"hello && cargo build"]);
3123 }
3124
3125 #[test]
3126 fn test_split_unclosed_backtick() {
3127 let _guard = test_guard!();
3128 let result = split_shell_commands("echo `hello && cargo build");
3129 assert_eq!(result, vec!["echo `hello && cargo build"]);
3130 }
3131
3132 #[test]
3133 fn test_split_embedded_nulls() {
3134 let _guard = test_guard!();
3135 let input = "cargo build\0 && echo done";
3137 let result = split_shell_commands(input);
3138 assert_eq!(result.len(), 2);
3139 }
3140
3141 #[test]
3142 fn test_split_unicode_input() {
3143 let _guard = test_guard!();
3144 let result = split_shell_commands("echo 'こんにちは' && cargo build");
3145 assert_eq!(result, vec!["echo 'こんにちは'", "cargo build"]);
3146 }
3147
3148 #[test]
3149 fn test_split_extremely_long_input() {
3150 let _guard = test_guard!();
3151 let long_cmd = format!("echo {} && cargo build", "x".repeat(20_000));
3153 let result = split_shell_commands(&long_cmd);
3154 assert_eq!(result.len(), 2);
3155 }
3156
3157 #[test]
3158 fn test_classify_long_input_skips_splitting() {
3159 let _guard = test_guard!();
3160 let long_cmd = format!("cargo build && echo {}", "x".repeat(11_000));
3162 let result = classify_command(&long_cmd);
3163 assert!(!result.is_compilation);
3165 assert!(
3166 result.reason.to_string().contains("chained"),
3167 "long input should be rejected by check_structure, not split"
3168 );
3169 }
3170
3171 #[test]
3172 fn test_split_only_operators() {
3173 let _guard = test_guard!();
3174 let result = split_shell_commands("&& || ;");
3176 assert!(
3177 result.is_empty(),
3178 "only operators should yield empty result"
3179 );
3180 }
3181
3182 #[test]
3183 fn test_split_consecutive_operators() {
3184 let _guard = test_guard!();
3185 let result = split_shell_commands("cargo build && && echo done");
3186 assert_eq!(result, vec!["cargo build", "echo done"]);
3188 }
3189
3190 #[test]
3191 fn test_classify_empty_after_split() {
3192 let _guard = test_guard!();
3193 let result = classify_command("echo hello && ls -la || pwd");
3195 assert!(!result.is_compilation);
3196 }
3197
3198 #[test]
3203 fn test_split_three_segment_chain() {
3204 let _guard = test_guard!();
3205 assert_eq!(
3206 split_shell_commands("cd /proj && cmake .. && make"),
3207 vec!["cd /proj", "cmake ..", "make"]
3208 );
3209 }
3210
3211 #[test]
3212 fn test_split_single_with_flags() {
3213 let _guard = test_guard!();
3214 assert_eq!(
3215 split_shell_commands("cargo build --release"),
3216 vec!["cargo build --release"]
3217 );
3218 }
3219
3220 #[test]
3221 fn test_split_nested_quotes_semicolon() {
3222 let _guard = test_guard!();
3223 assert_eq!(
3224 split_shell_commands("echo \"it's && done\" && make"),
3225 vec!["echo \"it's && done\"", "make"]
3226 );
3227 }
3228
3229 #[test]
3230 fn test_split_pipe_then_and() {
3231 let _guard = test_guard!();
3232 assert_eq!(
3234 split_shell_commands("make 2>&1 | grep error && echo done"),
3235 vec!["make 2>&1 | grep error", "echo done"]
3236 );
3237 }
3238
3239 #[test]
3240 fn test_split_leading_operator() {
3241 let _guard = test_guard!();
3242 assert_eq!(split_shell_commands("&& cargo build"), vec!["cargo build"]);
3244 }
3245
3246 #[test]
3251 fn test_classify_cargo_fmt_and_build() {
3252 let _guard = test_guard!();
3253 let result = classify_command("cargo fmt && cargo build");
3255 assert!(
3256 result.is_compilation,
3257 "compound command with compilation suffix should be accepted"
3258 );
3259 assert!(result.reason.contains("compound"));
3260 }
3261
3262 #[test]
3263 fn test_classify_cd_and_make() {
3264 let _guard = test_guard!();
3265 let result = classify_command("cd /project && make -j8");
3267 assert!(
3268 result.is_compilation,
3269 "compound command with compilation suffix should be accepted"
3270 );
3271 assert!(result.reason.contains("compound"));
3272 }
3273 #[test]
3274 fn test_classify_export_and_cargo_build() {
3275 let _guard = test_guard!();
3276 let result =
3278 classify_command("export RUSTFLAGS='-C opt-level=3' && cargo build --release");
3279 assert!(
3280 result.is_compilation,
3281 "compound command with compilation suffix should be accepted"
3282 );
3283 assert!(result.reason.contains("compound"));
3284 }
3285 #[test]
3286 fn test_classify_mkdir_cmake_chain() {
3287 let _guard = test_guard!();
3288 let result =
3290 classify_command("mkdir -p build && cmake -B build && cmake --build build");
3291 assert!(
3292 result.is_compilation,
3293 "compound command with compilation suffix should be accepted"
3294 );
3295 assert!(result.reason.contains("compound"));
3296 }
3297 #[test]
3298 fn test_classify_echo_and_cargo_test() {
3299 let _guard = test_guard!();
3300 let result = classify_command("echo 'Starting...' && cargo test");
3302 assert!(
3303 result.is_compilation,
3304 "compound command with compilation suffix should be accepted"
3305 );
3306 assert!(result.reason.contains("compound"));
3307 }
3308 #[test]
3309 fn test_classify_semicolon_chain_with_compilation() {
3310 let _guard = test_guard!();
3311 let result = classify_command("cargo fmt; cargo build; cargo test");
3313 assert!(
3314 !result.is_compilation,
3315 "semicolon chained commands should be rejected"
3316 );
3317 assert!(result.reason.contains("chained"));
3318 }
3319 #[test]
3322 fn test_classify_echo_chain_non_compilation() {
3323 let _guard = test_guard!();
3324 let result = classify_command("echo hello && echo world");
3325 assert!(!result.is_compilation);
3326 }
3327
3328 #[test]
3329 fn test_classify_ls_cat_non_compilation() {
3330 let _guard = test_guard!();
3331 let result = classify_command("ls -la && cat file.txt");
3332 assert!(!result.is_compilation);
3333 }
3334
3335 #[test]
3336 fn test_classify_git_chain_non_compilation() {
3337 let _guard = test_guard!();
3338 let result = classify_command("git status && git log");
3339 assert!(!result.is_compilation);
3340 }
3341
3342 #[test]
3345 fn test_classify_multi_command_performance() {
3346 let _guard = test_guard!();
3347 let start = std::time::Instant::now();
3348 for _ in 0..100 {
3349 let _ = classify_command("cargo fmt && cargo build && cargo test");
3350 }
3351 let elapsed = start.elapsed();
3352 let per_call_us = elapsed.as_micros() / 100;
3353 assert!(
3355 per_call_us < 5_000,
3356 "classify_command took {}us per call, should be <5000us",
3357 per_call_us
3358 );
3359 eprintln!(
3360 "[perf] classify_command multi-command: {}us avg per call",
3361 per_call_us
3362 );
3363 }
3364
3365 #[test]
3368 fn test_classify_pipe_preserved_in_segment() {
3369 let _guard = test_guard!();
3370 let result = classify_command("cargo build 2>&1 | tee log");
3373 assert!(!result.is_compilation, "piped command should be rejected");
3374 }
3375
3376 #[test]
3377 fn test_classify_pipe_segment_and_operator() {
3378 let _guard = test_guard!();
3379 let result = classify_command("make 2>&1 | grep error && cargo build");
3381 assert!(
3383 !result.is_compilation,
3384 "chained commands should be rejected"
3385 );
3386 assert!(result.reason.contains("piped") || result.reason.contains("chained"));
3391 }
3392 }
3393
3394 #[allow(clippy::ptr_arg)]
3405 fn assert_cow_borrowed(cow: &Cow<'static, str>, context: &str) {
3406 assert!(
3407 matches!(cow, Cow::Borrowed(_)),
3408 "{context}: expected Cow::Borrowed, got Cow::Owned({:?})",
3409 cow,
3410 );
3411 }
3412
3413 #[test]
3416 fn test_cow_reject_empty_command_correctness() {
3417 let _guard = test_guard!();
3418 let result = classify_command("");
3419 assert!(!result.is_compilation);
3420 assert_eq!(result.confidence, 0.0);
3421 assert_eq!(result.kind, None);
3422 assert!(result.reason.contains("empty"), "reason: {}", result.reason);
3423 }
3424
3425 #[test]
3426 fn test_cow_reject_non_compilation_commands() {
3427 let _guard = test_guard!();
3428 let non_compilation = [
3429 "ls -la",
3430 "cat file.txt",
3431 "echo hello",
3432 "git status",
3433 "pwd",
3434 "cd /tmp",
3435 ];
3436 for cmd in non_compilation {
3437 let result = classify_command(cmd);
3438 assert!(
3439 !result.is_compilation,
3440 "'{cmd}' should NOT be classified as compilation"
3441 );
3442 assert_eq!(result.confidence, 0.0, "'{cmd}' should have 0.0 confidence");
3443 assert_eq!(result.kind, None, "'{cmd}' should have no CompilationKind");
3444 assert!(!result.reason.is_empty(), "'{cmd}' should have a reason");
3445 }
3446 }
3447
3448 #[test]
3449 fn test_cow_accept_compilation_commands() {
3450 let _guard = test_guard!();
3451 let cases: &[(&str, CompilationKind)] = &[
3452 ("cargo build", CompilationKind::CargoBuild),
3453 ("cargo test", CompilationKind::CargoTest),
3454 ("cargo check", CompilationKind::CargoCheck),
3455 ("cargo clippy", CompilationKind::CargoClippy),
3456 ("gcc -o hello hello.c", CompilationKind::Gcc),
3457 ("make", CompilationKind::Make),
3458 ("bun test", CompilationKind::BunTest),
3459 ];
3460 for &(cmd, expected_kind) in cases {
3461 let result = classify_command(cmd);
3462 assert!(
3463 result.is_compilation,
3464 "'{cmd}' should be classified as compilation"
3465 );
3466 assert_eq!(
3467 result.kind,
3468 Some(expected_kind),
3469 "'{cmd}' should have kind {expected_kind:?}"
3470 );
3471 assert!(
3472 result.confidence > 0.0,
3473 "'{cmd}' should have positive confidence"
3474 );
3475 assert!(!result.reason.is_empty(), "'{cmd}' should have a reason");
3476 }
3477 }
3478
3479 #[test]
3482 fn test_cow_borrowed_on_tier0_reject() {
3483 let _guard = test_guard!();
3484 let result = classify_command("");
3486 assert_cow_borrowed(&result.reason, "Tier 0 empty command reason");
3487 }
3488
3489 #[test]
3490 fn test_cow_borrowed_on_tier1_reject() {
3491 let _guard = test_guard!();
3492 let result = classify_command("cargo build | tee log");
3494 assert!(!result.is_compilation);
3495 assert_cow_borrowed(&result.reason, "Tier 1 piped command reason");
3496
3497 let result = classify_command("cargo build &");
3499 assert!(!result.is_compilation);
3500 assert_cow_borrowed(&result.reason, "Tier 1 backgrounded command reason");
3501
3502 let result = classify_command("cargo build > log.txt");
3504 assert!(!result.is_compilation);
3505 assert_cow_borrowed(&result.reason, "Tier 1 redirected command reason");
3506 }
3507
3508 #[test]
3509 fn test_cow_borrowed_on_tier2_reject() {
3510 let _guard = test_guard!();
3511 let non_keyword_commands = [
3513 "ls -la",
3514 "cat file.txt",
3515 "echo hello world",
3516 "git status",
3517 "pwd",
3518 "cd /tmp",
3519 "grep pattern file",
3520 "find . -name '*.rs'",
3521 "cp src dst",
3522 "rm file.txt",
3523 ];
3524 for cmd in non_keyword_commands {
3525 let result = classify_command(cmd);
3526 assert!(!result.is_compilation, "'{cmd}' should be rejected");
3527 assert_cow_borrowed(&result.reason, &format!("Tier 2 reject for '{cmd}'"));
3528 }
3529 }
3530
3531 #[test]
3532 fn test_cow_borrowed_on_tier3_reject() {
3533 let _guard = test_guard!();
3534 let never_intercept = ["cargo fmt", "cargo install ripgrep", "cargo clean"];
3536 for cmd in never_intercept {
3537 let result = classify_command(cmd);
3538 assert!(!result.is_compilation, "'{cmd}' should be rejected");
3539 assert!(
3540 result.reason.contains("never-intercept"),
3541 "'{cmd}' reason should mention never-intercept: {}",
3542 result.reason
3543 );
3544 assert_cow_borrowed(&result.reason, &format!("Tier 3 reject for '{cmd}'"));
3546 }
3547 }
3548
3549 #[test]
3552 fn test_detailed_tiers_use_borrowed_names() {
3553 let _guard = test_guard!();
3554 let details = classify_command_detailed("ls -la");
3557 for tier in &details.tiers {
3558 assert_cow_borrowed(&tier.name, &format!("Tier {} name", tier.tier));
3559 }
3560 }
3561
3562 #[test]
3563 fn test_detailed_tier_reasons_borrowed_on_reject() {
3564 let _guard = test_guard!();
3565 let details = classify_command_detailed("ls -la");
3567 assert!(!details.classification.is_compilation);
3568 for tier in &details.tiers {
3569 assert_cow_borrowed(
3570 &tier.reason,
3571 &format!("Tier {} reason for 'ls -la'", tier.tier),
3572 );
3573 }
3574 }
3575
3576 #[test]
3577 fn test_detailed_compilation_tier_names_borrowed() {
3578 let _guard = test_guard!();
3579 let details = classify_command_detailed("cargo build");
3581 assert!(details.classification.is_compilation);
3582 for tier in &details.tiers {
3583 assert_cow_borrowed(
3584 &tier.name,
3585 &format!("Tier {} name for 'cargo build'", tier.tier),
3586 );
3587 }
3588 }
3589
3590 #[test]
3593 fn test_classification_serde_roundtrip_borrowed() {
3594 let _guard = test_guard!();
3595 let original = Classification::not_compilation("no compilation keyword");
3596 assert_cow_borrowed(&original.reason, "before serialize");
3597
3598 let json = serde_json::to_string(&original).expect("serialize");
3599 let deserialized: Classification = serde_json::from_str(&json).expect("deserialize");
3600
3601 assert_eq!(deserialized.is_compilation, original.is_compilation);
3602 assert_eq!(deserialized.confidence, original.confidence);
3603 assert_eq!(deserialized.kind, original.kind);
3604 assert_eq!(deserialized.reason, original.reason);
3605 }
3608
3609 #[test]
3610 fn test_classification_serde_roundtrip_compilation() {
3611 let _guard = test_guard!();
3612 let original =
3613 Classification::compilation(CompilationKind::CargoBuild, 0.95, "cargo build detected");
3614
3615 let json = serde_json::to_string(&original).expect("serialize");
3616 let deserialized: Classification = serde_json::from_str(&json).expect("deserialize");
3617
3618 assert_eq!(deserialized.is_compilation, original.is_compilation);
3619 assert_eq!(deserialized.confidence, original.confidence);
3620 assert_eq!(deserialized.kind, original.kind);
3621 assert_eq!(deserialized.reason.as_ref(), original.reason.as_ref());
3622 }
3623
3624 #[test]
3625 fn test_classification_tier_serde_roundtrip() {
3626 let _guard = test_guard!();
3627 let tier = ClassificationTier {
3628 tier: 2,
3629 name: Cow::Borrowed(TIER_KEYWORD_FILTER),
3630 decision: TierDecision::Reject,
3631 reason: Cow::Borrowed("no compilation keyword"),
3632 };
3633 assert_cow_borrowed(&tier.name, "tier name before serialize");
3634 assert_cow_borrowed(&tier.reason, "tier reason before serialize");
3635
3636 let json = serde_json::to_string(&tier).expect("serialize");
3637 let deserialized: ClassificationTier = serde_json::from_str(&json).expect("deserialize");
3638
3639 assert_eq!(deserialized.tier, tier.tier);
3640 assert_eq!(deserialized.name.as_ref(), tier.name.as_ref());
3641 assert_eq!(deserialized.decision, tier.decision);
3642 assert_eq!(deserialized.reason.as_ref(), tier.reason.as_ref());
3643 }
3644
3645 #[test]
3646 fn test_classification_details_serde_roundtrip() {
3647 let _guard = test_guard!();
3648 let details = classify_command_detailed("cargo build");
3649 assert!(details.classification.is_compilation);
3650
3651 let json = serde_json::to_string(&details).expect("serialize");
3652 let deserialized: ClassificationDetails = serde_json::from_str(&json).expect("deserialize");
3653
3654 assert_eq!(deserialized.original, details.original);
3655 assert_eq!(deserialized.normalized, details.normalized);
3656 assert_eq!(deserialized.tiers.len(), details.tiers.len());
3657 assert_eq!(
3658 deserialized.classification.is_compilation,
3659 details.classification.is_compilation
3660 );
3661 assert_eq!(
3662 deserialized.classification.kind,
3663 details.classification.kind
3664 );
3665 }
3666
3667 #[test]
3670 fn test_cow_reason_display() {
3671 let _guard = test_guard!();
3672 let result = classify_command("ls");
3673 let displayed = format!("{}", result.reason);
3675 assert!(!displayed.is_empty());
3676 let debugged = format!("{:?}", result.reason);
3677 assert!(!debugged.is_empty());
3678 }
3679
3680 #[test]
3681 fn test_cow_reason_comparison() {
3682 let _guard = test_guard!();
3683 let borrowed: Cow<'static, str> = Cow::Borrowed("no compilation keyword");
3685 let owned: Cow<'static, str> = Cow::Owned("no compilation keyword".to_string());
3686 assert_eq!(borrowed, owned);
3687
3688 let r1 = classify_command("ls");
3690 let r2 = classify_command("pwd");
3691 assert_eq!(r1.reason, r2.reason);
3693 }
3694}
3695
3696#[cfg(test)]
3697mod tests_bun_whitespace {
3698 use super::*;
3699 use crate::test_guard;
3700
3701 #[test]
3702 fn test_bun_whitespace_resilience() {
3703 let _guard = test_guard!();
3704
3705 let result = classify_command("bun test");
3707 assert!(result.is_compilation, "bun test failed");
3708 assert_eq!(result.kind, Some(CompilationKind::BunTest));
3709
3710 let result = classify_command("bun test");
3712 assert!(result.is_compilation, "bun test failed");
3713 assert_eq!(result.kind, Some(CompilationKind::BunTest));
3714
3715 let result = classify_command("bun\ttest");
3717 assert!(result.is_compilation, "bun\\ttest failed");
3718 assert_eq!(result.kind, Some(CompilationKind::BunTest));
3719
3720 let result = classify_command("bun typecheck src/");
3722 assert!(result.is_compilation, "bun typecheck with spaces failed");
3723 assert_eq!(result.kind, Some(CompilationKind::BunTypecheck));
3724 }
3725
3726 #[test]
3727 fn test_bun_watch_with_whitespace() {
3728 let _guard = test_guard!();
3729
3730 let result = classify_command("bun test --watch");
3732 assert!(
3733 !result.is_compilation,
3734 "bun test --watch should be rejected"
3735 );
3736 assert!(result.reason.contains("interactive"));
3737
3738 let result = classify_command("bun\ttypecheck\t-w");
3740 assert!(
3741 !result.is_compilation,
3742 "bun typecheck -w should be rejected"
3743 );
3744 assert!(result.reason.contains("interactive"));
3745 }
3746
3747 #[test]
3748 fn test_bun_x_whitespace() {
3749 let _guard = test_guard!();
3750
3751 let result = classify_command("bun x vitest");
3753 assert!(!result.is_compilation, "bun x should be rejected");
3754 assert!(result.reason.contains("bun x"));
3755 }
3756}
3757
3758#[cfg(test)]
3759mod tests_normalize_whitespace {
3760 use super::*;
3761 use crate::test_guard;
3762
3763 #[test]
3764 fn test_wrapper_whitespace_resilience() {
3765 let _guard = test_guard!();
3766
3767 assert_eq!(normalize_command("sudo cargo"), "cargo");
3769
3770 assert_eq!(normalize_command("sudo\tcargo"), "cargo");
3772
3773 assert_eq!(normalize_command("time\tsudo cargo"), "cargo");
3775
3776 assert_eq!(normalize_command("sudocargo"), "sudocargo");
3778
3779 assert_eq!(normalize_command("sudo"), "");
3781 }
3782
3783 #[test]
3784 fn test_env_var_whitespace() {
3785 let _guard = test_guard!();
3786
3787 assert_eq!(normalize_command("env RUST_BACKTRACE=1 cargo"), "cargo");
3789
3790 assert_eq!(normalize_command("env\tRUST_BACKTRACE=1\tcargo"), "cargo");
3792 }
3793}
3794
3795#[cfg(test)]
3804mod regression_classification {
3805 use super::*;
3806 use crate::test_guard;
3807
3808 struct Case {
3810 cmd: &'static str,
3811 expect_compilation: bool,
3812 expected_kind: Option<CompilationKind>,
3813 reason_contains: &'static str,
3815 min_confidence: f64,
3817 }
3818
3819 fn run_cases(cases: &[Case]) {
3820 for (i, c) in cases.iter().enumerate() {
3821 let result = classify_command(c.cmd);
3822 assert_eq!(
3823 result.is_compilation, c.expect_compilation,
3824 "Case {i} ({:?}): expected is_compilation={}, got={}. reason={:?}",
3825 c.cmd, c.expect_compilation, result.is_compilation, result.reason
3826 );
3827 if c.expect_compilation {
3828 assert_eq!(
3829 result.kind, c.expected_kind,
3830 "Case {i} ({:?}): expected kind={:?}, got={:?}",
3831 c.cmd, c.expected_kind, result.kind
3832 );
3833 assert!(
3834 result.confidence >= c.min_confidence,
3835 "Case {i} ({:?}): expected confidence >= {}, got={}",
3836 c.cmd,
3837 c.min_confidence,
3838 result.confidence
3839 );
3840 }
3841 if !c.reason_contains.is_empty() {
3842 assert!(
3843 result.reason.contains(c.reason_contains),
3844 "Case {i} ({:?}): expected reason containing {:?}, got {:?}",
3845 c.cmd,
3846 c.reason_contains,
3847 result.reason
3848 );
3849 }
3850 }
3851 }
3852
3853 #[test]
3858 fn regression_cargo_compilation_positive() {
3859 let _guard = test_guard!();
3860 let cases = [
3861 Case {
3862 cmd: "cargo build",
3863 expect_compilation: true,
3864 expected_kind: Some(CompilationKind::CargoBuild),
3865 reason_contains: "cargo build",
3866 min_confidence: 0.90,
3867 },
3868 Case {
3869 cmd: "cargo build --release",
3870 expect_compilation: true,
3871 expected_kind: Some(CompilationKind::CargoBuild),
3872 reason_contains: "cargo build",
3873 min_confidence: 0.90,
3874 },
3875 Case {
3876 cmd: "cargo build --workspace",
3877 expect_compilation: true,
3878 expected_kind: Some(CompilationKind::CargoBuild),
3879 reason_contains: "cargo build",
3880 min_confidence: 0.90,
3881 },
3882 Case {
3883 cmd: "cargo build -p my-crate",
3884 expect_compilation: true,
3885 expected_kind: Some(CompilationKind::CargoBuild),
3886 reason_contains: "cargo build",
3887 min_confidence: 0.90,
3888 },
3889 Case {
3890 cmd: "cargo b",
3891 expect_compilation: true,
3892 expected_kind: Some(CompilationKind::CargoBuild),
3893 reason_contains: "cargo build",
3894 min_confidence: 0.90,
3895 },
3896 Case {
3897 cmd: "cargo test",
3898 expect_compilation: true,
3899 expected_kind: Some(CompilationKind::CargoTest),
3900 reason_contains: "cargo test",
3901 min_confidence: 0.90,
3902 },
3903 Case {
3904 cmd: "cargo test --release",
3905 expect_compilation: true,
3906 expected_kind: Some(CompilationKind::CargoTest),
3907 reason_contains: "cargo test",
3908 min_confidence: 0.90,
3909 },
3910 Case {
3911 cmd: "cargo test my_test",
3912 expect_compilation: true,
3913 expected_kind: Some(CompilationKind::CargoTest),
3914 reason_contains: "cargo test",
3915 min_confidence: 0.90,
3916 },
3917 Case {
3918 cmd: "cargo test -- --nocapture",
3919 expect_compilation: true,
3920 expected_kind: Some(CompilationKind::CargoTest),
3921 reason_contains: "cargo test",
3922 min_confidence: 0.90,
3923 },
3924 Case {
3925 cmd: "cargo test --workspace",
3926 expect_compilation: true,
3927 expected_kind: Some(CompilationKind::CargoTest),
3928 reason_contains: "cargo test",
3929 min_confidence: 0.90,
3930 },
3931 Case {
3932 cmd: "cargo test -p rch-common",
3933 expect_compilation: true,
3934 expected_kind: Some(CompilationKind::CargoTest),
3935 reason_contains: "cargo test",
3936 min_confidence: 0.90,
3937 },
3938 Case {
3939 cmd: "cargo t",
3940 expect_compilation: true,
3941 expected_kind: Some(CompilationKind::CargoTest),
3942 reason_contains: "cargo test",
3943 min_confidence: 0.90,
3944 },
3945 Case {
3946 cmd: "cargo test --lib",
3947 expect_compilation: true,
3948 expected_kind: Some(CompilationKind::CargoTest),
3949 reason_contains: "cargo test",
3950 min_confidence: 0.90,
3951 },
3952 Case {
3953 cmd: "cargo test --bins",
3954 expect_compilation: true,
3955 expected_kind: Some(CompilationKind::CargoTest),
3956 reason_contains: "cargo test",
3957 min_confidence: 0.90,
3958 },
3959 Case {
3960 cmd: "cargo test --doc",
3961 expect_compilation: true,
3962 expected_kind: Some(CompilationKind::CargoTest),
3963 reason_contains: "cargo test",
3964 min_confidence: 0.90,
3965 },
3966 Case {
3967 cmd: "cargo test -- --test-threads=4",
3968 expect_compilation: true,
3969 expected_kind: Some(CompilationKind::CargoTest),
3970 reason_contains: "cargo test",
3971 min_confidence: 0.90,
3972 },
3973 Case {
3974 cmd: "cargo check",
3975 expect_compilation: true,
3976 expected_kind: Some(CompilationKind::CargoCheck),
3977 reason_contains: "cargo check",
3978 min_confidence: 0.85,
3979 },
3980 Case {
3981 cmd: "cargo check --workspace --all-targets",
3982 expect_compilation: true,
3983 expected_kind: Some(CompilationKind::CargoCheck),
3984 reason_contains: "cargo check",
3985 min_confidence: 0.85,
3986 },
3987 Case {
3988 cmd: "cargo c",
3989 expect_compilation: true,
3990 expected_kind: Some(CompilationKind::CargoCheck),
3991 reason_contains: "cargo check",
3992 min_confidence: 0.85,
3993 },
3994 Case {
3995 cmd: "cargo clippy",
3996 expect_compilation: true,
3997 expected_kind: Some(CompilationKind::CargoClippy),
3998 reason_contains: "cargo clippy",
3999 min_confidence: 0.85,
4000 },
4001 Case {
4002 cmd: "cargo clippy --workspace --all-targets -- -D warnings",
4003 expect_compilation: true,
4004 expected_kind: Some(CompilationKind::CargoClippy),
4005 reason_contains: "cargo clippy",
4006 min_confidence: 0.85,
4007 },
4008 Case {
4009 cmd: "cargo doc",
4010 expect_compilation: true,
4011 expected_kind: Some(CompilationKind::CargoDoc),
4012 reason_contains: "cargo doc",
4013 min_confidence: 0.80,
4014 },
4015 Case {
4016 cmd: "cargo doc --no-deps",
4017 expect_compilation: true,
4018 expected_kind: Some(CompilationKind::CargoDoc),
4019 reason_contains: "cargo doc",
4020 min_confidence: 0.80,
4021 },
4022 Case {
4023 cmd: "cargo run",
4024 expect_compilation: true,
4025 expected_kind: Some(CompilationKind::CargoBuild),
4026 reason_contains: "cargo run",
4027 min_confidence: 0.80,
4028 },
4029 Case {
4030 cmd: "cargo run --release",
4031 expect_compilation: true,
4032 expected_kind: Some(CompilationKind::CargoBuild),
4033 reason_contains: "cargo run",
4034 min_confidence: 0.80,
4035 },
4036 Case {
4037 cmd: "cargo r",
4038 expect_compilation: true,
4039 expected_kind: Some(CompilationKind::CargoBuild),
4040 reason_contains: "cargo run",
4041 min_confidence: 0.80,
4042 },
4043 Case {
4044 cmd: "cargo bench",
4045 expect_compilation: true,
4046 expected_kind: Some(CompilationKind::CargoBench),
4047 reason_contains: "cargo bench",
4048 min_confidence: 0.85,
4049 },
4050 Case {
4051 cmd: "cargo bench --bench my_bench",
4052 expect_compilation: true,
4053 expected_kind: Some(CompilationKind::CargoBench),
4054 reason_contains: "cargo bench",
4055 min_confidence: 0.85,
4056 },
4057 Case {
4058 cmd: "cargo nextest run",
4059 expect_compilation: true,
4060 expected_kind: Some(CompilationKind::CargoNextest),
4061 reason_contains: "cargo nextest run",
4062 min_confidence: 0.90,
4063 },
4064 Case {
4065 cmd: "cargo nextest run --workspace",
4066 expect_compilation: true,
4067 expected_kind: Some(CompilationKind::CargoNextest),
4068 reason_contains: "cargo nextest run",
4069 min_confidence: 0.90,
4070 },
4071 Case {
4072 cmd: "cargo nextest r",
4073 expect_compilation: true,
4074 expected_kind: Some(CompilationKind::CargoNextest),
4075 reason_contains: "cargo nextest run",
4076 min_confidence: 0.90,
4077 },
4078 Case {
4080 cmd: "cargo +nightly build",
4081 expect_compilation: true,
4082 expected_kind: Some(CompilationKind::CargoBuild),
4083 reason_contains: "cargo build",
4084 min_confidence: 0.90,
4085 },
4086 Case {
4087 cmd: "cargo +1.80.0 test",
4088 expect_compilation: true,
4089 expected_kind: Some(CompilationKind::CargoTest),
4090 reason_contains: "cargo test",
4091 min_confidence: 0.90,
4092 },
4093 Case {
4094 cmd: "cargo +nightly nextest run",
4095 expect_compilation: true,
4096 expected_kind: Some(CompilationKind::CargoNextest),
4097 reason_contains: "cargo nextest run",
4098 min_confidence: 0.90,
4099 },
4100 ];
4101 run_cases(&cases);
4102 }
4103
4104 #[test]
4109 fn regression_cargo_never_intercept() {
4110 let _guard = test_guard!();
4111 let cases = [
4112 Case {
4113 cmd: "cargo install ripgrep",
4114 expect_compilation: false,
4115 expected_kind: None,
4116 reason_contains: "never-intercept",
4117 min_confidence: 0.0,
4118 },
4119 Case {
4120 cmd: "cargo publish",
4121 expect_compilation: false,
4122 expected_kind: None,
4123 reason_contains: "never-intercept",
4124 min_confidence: 0.0,
4125 },
4126 Case {
4127 cmd: "cargo login",
4128 expect_compilation: false,
4129 expected_kind: None,
4130 reason_contains: "never-intercept",
4131 min_confidence: 0.0,
4132 },
4133 Case {
4134 cmd: "cargo fmt",
4135 expect_compilation: false,
4136 expected_kind: None,
4137 reason_contains: "never-intercept",
4138 min_confidence: 0.0,
4139 },
4140 Case {
4141 cmd: "cargo fmt --check",
4142 expect_compilation: false,
4143 expected_kind: None,
4144 reason_contains: "never-intercept",
4145 min_confidence: 0.0,
4146 },
4147 Case {
4148 cmd: "cargo fix",
4149 expect_compilation: false,
4150 expected_kind: None,
4151 reason_contains: "never-intercept",
4152 min_confidence: 0.0,
4153 },
4154 Case {
4155 cmd: "cargo clean",
4156 expect_compilation: false,
4157 expected_kind: None,
4158 reason_contains: "never-intercept",
4159 min_confidence: 0.0,
4160 },
4161 Case {
4162 cmd: "cargo new my_project",
4163 expect_compilation: false,
4164 expected_kind: None,
4165 reason_contains: "never-intercept",
4166 min_confidence: 0.0,
4167 },
4168 Case {
4169 cmd: "cargo init",
4170 expect_compilation: false,
4171 expected_kind: None,
4172 reason_contains: "never-intercept",
4173 min_confidence: 0.0,
4174 },
4175 Case {
4176 cmd: "cargo add serde",
4177 expect_compilation: false,
4178 expected_kind: None,
4179 reason_contains: "never-intercept",
4180 min_confidence: 0.0,
4181 },
4182 Case {
4183 cmd: "cargo remove serde",
4184 expect_compilation: false,
4185 expected_kind: None,
4186 reason_contains: "never-intercept",
4187 min_confidence: 0.0,
4188 },
4189 Case {
4190 cmd: "cargo update",
4191 expect_compilation: false,
4192 expected_kind: None,
4193 reason_contains: "never-intercept",
4194 min_confidence: 0.0,
4195 },
4196 Case {
4197 cmd: "cargo generate-lockfile",
4198 expect_compilation: false,
4199 expected_kind: None,
4200 reason_contains: "never-intercept",
4201 min_confidence: 0.0,
4202 },
4203 Case {
4204 cmd: "cargo watch -x test",
4205 expect_compilation: false,
4206 expected_kind: None,
4207 reason_contains: "never-intercept",
4208 min_confidence: 0.0,
4209 },
4210 Case {
4211 cmd: "cargo --version",
4212 expect_compilation: false,
4213 expected_kind: None,
4214 reason_contains: "never-intercept",
4215 min_confidence: 0.0,
4216 },
4217 Case {
4218 cmd: "cargo -V",
4219 expect_compilation: false,
4220 expected_kind: None,
4221 reason_contains: "never-intercept",
4222 min_confidence: 0.0,
4223 },
4224 Case {
4226 cmd: "cargo nextest list",
4227 expect_compilation: false,
4228 expected_kind: None,
4229 reason_contains: "never-intercept",
4230 min_confidence: 0.0,
4231 },
4232 Case {
4233 cmd: "cargo nextest archive",
4234 expect_compilation: false,
4235 expected_kind: None,
4236 reason_contains: "never-intercept",
4237 min_confidence: 0.0,
4238 },
4239 Case {
4240 cmd: "cargo nextest show",
4241 expect_compilation: false,
4242 expected_kind: None,
4243 reason_contains: "never-intercept",
4244 min_confidence: 0.0,
4245 },
4246 Case {
4248 cmd: "cargo",
4249 expect_compilation: false,
4250 expected_kind: None,
4251 reason_contains: "bare cargo",
4252 min_confidence: 0.0,
4253 },
4254 Case {
4255 cmd: "cargo nextest",
4256 expect_compilation: false,
4257 expected_kind: None,
4258 reason_contains: "bare cargo nextest",
4259 min_confidence: 0.0,
4260 },
4261 Case {
4263 cmd: "cargo tree",
4264 expect_compilation: false,
4265 expected_kind: None,
4266 reason_contains: "not interceptable",
4267 min_confidence: 0.0,
4268 },
4269 Case {
4270 cmd: "cargo metadata",
4271 expect_compilation: false,
4272 expected_kind: None,
4273 reason_contains: "not interceptable",
4274 min_confidence: 0.0,
4275 },
4276 Case {
4277 cmd: "cargo search serde",
4278 expect_compilation: false,
4279 expected_kind: None,
4280 reason_contains: "not interceptable",
4281 min_confidence: 0.0,
4282 },
4283 Case {
4284 cmd: "cargo vendor",
4285 expect_compilation: false,
4286 expected_kind: None,
4287 reason_contains: "not interceptable",
4288 min_confidence: 0.0,
4289 },
4290 ];
4291 run_cases(&cases);
4292 }
4293
4294 #[test]
4299 fn regression_bun_compilation_positive() {
4300 let _guard = test_guard!();
4301 let cases = [
4302 Case {
4303 cmd: "bun test",
4304 expect_compilation: true,
4305 expected_kind: Some(CompilationKind::BunTest),
4306 reason_contains: "bun test",
4307 min_confidence: 0.90,
4308 },
4309 Case {
4310 cmd: "bun test src/",
4311 expect_compilation: true,
4312 expected_kind: Some(CompilationKind::BunTest),
4313 reason_contains: "bun test",
4314 min_confidence: 0.90,
4315 },
4316 Case {
4317 cmd: "bun test --bail",
4318 expect_compilation: true,
4319 expected_kind: Some(CompilationKind::BunTest),
4320 reason_contains: "bun test",
4321 min_confidence: 0.90,
4322 },
4323 Case {
4324 cmd: "bun test --timeout 5000",
4325 expect_compilation: true,
4326 expected_kind: Some(CompilationKind::BunTest),
4327 reason_contains: "bun test",
4328 min_confidence: 0.90,
4329 },
4330 Case {
4331 cmd: "bun typecheck",
4332 expect_compilation: true,
4333 expected_kind: Some(CompilationKind::BunTypecheck),
4334 reason_contains: "bun typecheck",
4335 min_confidence: 0.90,
4336 },
4337 Case {
4338 cmd: "bun typecheck src/",
4339 expect_compilation: true,
4340 expected_kind: Some(CompilationKind::BunTypecheck),
4341 reason_contains: "bun typecheck",
4342 min_confidence: 0.90,
4343 },
4344 ];
4345 run_cases(&cases);
4346 }
4347
4348 #[test]
4353 fn regression_bun_never_intercept() {
4354 let _guard = test_guard!();
4355 let cases = [
4356 Case {
4358 cmd: "bun install",
4359 expect_compilation: false,
4360 expected_kind: None,
4361 reason_contains: "never-intercept",
4362 min_confidence: 0.0,
4363 },
4364 Case {
4365 cmd: "bun add react",
4366 expect_compilation: false,
4367 expected_kind: None,
4368 reason_contains: "never-intercept",
4369 min_confidence: 0.0,
4370 },
4371 Case {
4372 cmd: "bun remove react",
4373 expect_compilation: false,
4374 expected_kind: None,
4375 reason_contains: "never-intercept",
4376 min_confidence: 0.0,
4377 },
4378 Case {
4379 cmd: "bun link",
4380 expect_compilation: false,
4381 expected_kind: None,
4382 reason_contains: "never-intercept",
4383 min_confidence: 0.0,
4384 },
4385 Case {
4386 cmd: "bun unlink",
4387 expect_compilation: false,
4388 expected_kind: None,
4389 reason_contains: "never-intercept",
4390 min_confidence: 0.0,
4391 },
4392 Case {
4393 cmd: "bun pm ls",
4394 expect_compilation: false,
4395 expected_kind: None,
4396 reason_contains: "never-intercept",
4397 min_confidence: 0.0,
4398 },
4399 Case {
4400 cmd: "bun init",
4401 expect_compilation: false,
4402 expected_kind: None,
4403 reason_contains: "never-intercept",
4404 min_confidence: 0.0,
4405 },
4406 Case {
4407 cmd: "bun create my-app",
4408 expect_compilation: false,
4409 expected_kind: None,
4410 reason_contains: "never-intercept",
4411 min_confidence: 0.0,
4412 },
4413 Case {
4414 cmd: "bun upgrade",
4415 expect_compilation: false,
4416 expected_kind: None,
4417 reason_contains: "never-intercept",
4418 min_confidence: 0.0,
4419 },
4420 Case {
4422 cmd: "bun run dev",
4423 expect_compilation: false,
4424 expected_kind: None,
4425 reason_contains: "never-intercept",
4426 min_confidence: 0.0,
4427 },
4428 Case {
4429 cmd: "bun build src/index.ts",
4430 expect_compilation: false,
4431 expected_kind: None,
4432 reason_contains: "never-intercept",
4433 min_confidence: 0.0,
4434 },
4435 Case {
4436 cmd: "bun dev",
4437 expect_compilation: false,
4438 expected_kind: None,
4439 reason_contains: "never-intercept",
4440 min_confidence: 0.0,
4441 },
4442 Case {
4443 cmd: "bun repl",
4444 expect_compilation: false,
4445 expected_kind: None,
4446 reason_contains: "never-intercept",
4447 min_confidence: 0.0,
4448 },
4449 Case {
4451 cmd: "bun x vitest",
4452 expect_compilation: false,
4453 expected_kind: None,
4454 reason_contains: "bun x",
4455 min_confidence: 0.0,
4456 },
4457 Case {
4458 cmd: "bun x tsc --noEmit",
4459 expect_compilation: false,
4460 expected_kind: None,
4461 reason_contains: "bun x",
4462 min_confidence: 0.0,
4463 },
4464 Case {
4466 cmd: "bun test --watch",
4467 expect_compilation: false,
4468 expected_kind: None,
4469 reason_contains: "interactive",
4470 min_confidence: 0.0,
4471 },
4472 Case {
4473 cmd: "bun test -w",
4474 expect_compilation: false,
4475 expected_kind: None,
4476 reason_contains: "interactive",
4477 min_confidence: 0.0,
4478 },
4479 Case {
4480 cmd: "bun typecheck --watch",
4481 expect_compilation: false,
4482 expected_kind: None,
4483 reason_contains: "interactive",
4484 min_confidence: 0.0,
4485 },
4486 Case {
4487 cmd: "bun typecheck -w",
4488 expect_compilation: false,
4489 expected_kind: None,
4490 reason_contains: "interactive",
4491 min_confidence: 0.0,
4492 },
4493 Case {
4495 cmd: "bun --version",
4496 expect_compilation: false,
4497 expected_kind: None,
4498 reason_contains: "never-intercept",
4499 min_confidence: 0.0,
4500 },
4501 Case {
4502 cmd: "bun -v",
4503 expect_compilation: false,
4504 expected_kind: None,
4505 reason_contains: "never-intercept",
4506 min_confidence: 0.0,
4507 },
4508 Case {
4509 cmd: "bun --help",
4510 expect_compilation: false,
4511 expected_kind: None,
4512 reason_contains: "never-intercept",
4513 min_confidence: 0.0,
4514 },
4515 Case {
4516 cmd: "bun -h",
4517 expect_compilation: false,
4518 expected_kind: None,
4519 reason_contains: "never-intercept",
4520 min_confidence: 0.0,
4521 },
4522 Case {
4523 cmd: "bun completions",
4524 expect_compilation: false,
4525 expected_kind: None,
4526 reason_contains: "never-intercept",
4527 min_confidence: 0.0,
4528 },
4529 ];
4530 run_cases(&cases);
4531 }
4532
4533 #[test]
4538 fn regression_c_cpp_compilation_positive() {
4539 let _guard = test_guard!();
4540 let cases = [
4541 Case {
4542 cmd: "gcc -c main.c -o main.o",
4543 expect_compilation: true,
4544 expected_kind: Some(CompilationKind::Gcc),
4545 reason_contains: "gcc",
4546 min_confidence: 0.85,
4547 },
4548 Case {
4549 cmd: "gcc main.c -o main",
4550 expect_compilation: true,
4551 expected_kind: Some(CompilationKind::Gcc),
4552 reason_contains: "gcc",
4553 min_confidence: 0.85,
4554 },
4555 Case {
4556 cmd: "g++ -c main.cpp -o main.o",
4557 expect_compilation: true,
4558 expected_kind: Some(CompilationKind::Gpp),
4559 reason_contains: "g++",
4560 min_confidence: 0.85,
4561 },
4562 Case {
4563 cmd: "g++ main.cc -o main",
4564 expect_compilation: true,
4565 expected_kind: Some(CompilationKind::Gpp),
4566 reason_contains: "g++",
4567 min_confidence: 0.85,
4568 },
4569 Case {
4570 cmd: "clang -c main.c -o main.o",
4571 expect_compilation: true,
4572 expected_kind: Some(CompilationKind::Clang),
4573 reason_contains: "clang",
4574 min_confidence: 0.85,
4575 },
4576 Case {
4577 cmd: "clang++ -c main.cpp -o main.o",
4578 expect_compilation: true,
4579 expected_kind: Some(CompilationKind::Clangpp),
4580 reason_contains: "clang++",
4581 min_confidence: 0.85,
4582 },
4583 Case {
4584 cmd: "clang++ main.cc -o main",
4585 expect_compilation: true,
4586 expected_kind: Some(CompilationKind::Clangpp),
4587 reason_contains: "clang++",
4588 min_confidence: 0.85,
4589 },
4590 Case {
4591 cmd: "cc -c main.c -o main.o",
4592 expect_compilation: true,
4593 expected_kind: Some(CompilationKind::Gcc),
4594 reason_contains: "cc",
4595 min_confidence: 0.80,
4596 },
4597 Case {
4598 cmd: "c++ -c main.cpp -o main.o",
4599 expect_compilation: true,
4600 expected_kind: Some(CompilationKind::Gpp),
4601 reason_contains: "c++",
4602 min_confidence: 0.80,
4603 },
4604 ];
4605 run_cases(&cases);
4606 }
4607
4608 #[test]
4613 fn regression_c_cpp_version_checks_not_intercepted() {
4614 let _guard = test_guard!();
4615 let cases = [
4616 Case {
4617 cmd: "rustc --version",
4618 expect_compilation: false,
4619 expected_kind: None,
4620 reason_contains: "never-intercept",
4621 min_confidence: 0.0,
4622 },
4623 Case {
4624 cmd: "rustc -V",
4625 expect_compilation: false,
4626 expected_kind: None,
4627 reason_contains: "never-intercept",
4628 min_confidence: 0.0,
4629 },
4630 Case {
4631 cmd: "gcc --version",
4632 expect_compilation: false,
4633 expected_kind: None,
4634 reason_contains: "never-intercept",
4635 min_confidence: 0.0,
4636 },
4637 Case {
4638 cmd: "gcc -v",
4639 expect_compilation: false,
4640 expected_kind: None,
4641 reason_contains: "never-intercept",
4642 min_confidence: 0.0,
4643 },
4644 Case {
4645 cmd: "clang --version",
4646 expect_compilation: false,
4647 expected_kind: None,
4648 reason_contains: "never-intercept",
4649 min_confidence: 0.0,
4650 },
4651 Case {
4652 cmd: "clang -v",
4653 expect_compilation: false,
4654 expected_kind: None,
4655 reason_contains: "never-intercept",
4656 min_confidence: 0.0,
4657 },
4658 Case {
4659 cmd: "make --version",
4660 expect_compilation: false,
4661 expected_kind: None,
4662 reason_contains: "never-intercept",
4663 min_confidence: 0.0,
4664 },
4665 Case {
4666 cmd: "make -v",
4667 expect_compilation: false,
4668 expected_kind: None,
4669 reason_contains: "never-intercept",
4670 min_confidence: 0.0,
4671 },
4672 Case {
4673 cmd: "cmake --version",
4674 expect_compilation: false,
4675 expected_kind: None,
4676 reason_contains: "never-intercept",
4677 min_confidence: 0.0,
4678 },
4679 ];
4680 run_cases(&cases);
4681 }
4682
4683 #[test]
4688 fn regression_build_systems_positive() {
4689 let _guard = test_guard!();
4690 let cases = [
4691 Case {
4692 cmd: "make",
4693 expect_compilation: true,
4694 expected_kind: Some(CompilationKind::Make),
4695 reason_contains: "make build",
4696 min_confidence: 0.80,
4697 },
4698 Case {
4699 cmd: "make -j8",
4700 expect_compilation: true,
4701 expected_kind: Some(CompilationKind::Make),
4702 reason_contains: "make build",
4703 min_confidence: 0.80,
4704 },
4705 Case {
4706 cmd: "make all",
4707 expect_compilation: true,
4708 expected_kind: Some(CompilationKind::Make),
4709 reason_contains: "make build",
4710 min_confidence: 0.80,
4711 },
4712 Case {
4713 cmd: "cmake --build build",
4714 expect_compilation: true,
4715 expected_kind: Some(CompilationKind::CmakeBuild),
4716 reason_contains: "cmake --build",
4717 min_confidence: 0.85,
4718 },
4719 Case {
4720 cmd: "cmake --build=build",
4721 expect_compilation: true,
4722 expected_kind: Some(CompilationKind::CmakeBuild),
4723 reason_contains: "cmake --build",
4724 min_confidence: 0.85,
4725 },
4726 Case {
4727 cmd: "ninja",
4728 expect_compilation: true,
4729 expected_kind: Some(CompilationKind::Ninja),
4730 reason_contains: "ninja build",
4731 min_confidence: 0.85,
4732 },
4733 Case {
4734 cmd: "ninja -j4",
4735 expect_compilation: true,
4736 expected_kind: Some(CompilationKind::Ninja),
4737 reason_contains: "ninja build",
4738 min_confidence: 0.85,
4739 },
4740 Case {
4741 cmd: "meson compile",
4742 expect_compilation: true,
4743 expected_kind: Some(CompilationKind::Meson),
4744 reason_contains: "meson compile",
4745 min_confidence: 0.80,
4746 },
4747 ];
4748 run_cases(&cases);
4749 }
4750
4751 #[test]
4756 fn regression_build_systems_not_intercepted() {
4757 let _guard = test_guard!();
4758 let cases = [
4759 Case {
4760 cmd: "make clean",
4761 expect_compilation: false,
4762 expected_kind: None,
4763 reason_contains: "make maintenance",
4764 min_confidence: 0.0,
4765 },
4766 Case {
4767 cmd: "make install",
4768 expect_compilation: false,
4769 expected_kind: None,
4770 reason_contains: "make maintenance",
4771 min_confidence: 0.0,
4772 },
4773 Case {
4774 cmd: "make distclean",
4775 expect_compilation: false,
4776 expected_kind: None,
4777 reason_contains: "make maintenance",
4778 min_confidence: 0.0,
4779 },
4780 Case {
4781 cmd: "ninja -t clean",
4782 expect_compilation: false,
4783 expected_kind: None,
4784 reason_contains: "ninja clean",
4785 min_confidence: 0.0,
4786 },
4787 Case {
4788 cmd: "ninja clean",
4789 expect_compilation: false,
4790 expected_kind: None,
4791 reason_contains: "ninja clean",
4792 min_confidence: 0.0,
4793 },
4794 Case {
4796 cmd: "meson setup build",
4797 expect_compilation: false,
4798 expected_kind: None,
4799 reason_contains: "no matching pattern",
4800 min_confidence: 0.0,
4801 },
4802 Case {
4803 cmd: "meson configure",
4804 expect_compilation: false,
4805 expected_kind: None,
4806 reason_contains: "no matching pattern",
4807 min_confidence: 0.0,
4808 },
4809 Case {
4811 cmd: "cmake .",
4812 expect_compilation: false,
4813 expected_kind: None,
4814 reason_contains: "no matching pattern",
4815 min_confidence: 0.0,
4816 },
4817 Case {
4818 cmd: "cmake -DCMAKE_BUILD_TYPE=Release ..",
4819 expect_compilation: false,
4820 expected_kind: None,
4821 reason_contains: "no matching pattern",
4822 min_confidence: 0.0,
4823 },
4824 ];
4825 run_cases(&cases);
4826 }
4827
4828 #[test]
4833 fn regression_rustc_positive() {
4834 let _guard = test_guard!();
4835 let cases = [
4836 Case {
4837 cmd: "rustc main.rs",
4838 expect_compilation: true,
4839 expected_kind: Some(CompilationKind::Rustc),
4840 reason_contains: "rustc",
4841 min_confidence: 0.90,
4842 },
4843 Case {
4844 cmd: "rustc main.rs -o main",
4845 expect_compilation: true,
4846 expected_kind: Some(CompilationKind::Rustc),
4847 reason_contains: "rustc",
4848 min_confidence: 0.90,
4849 },
4850 Case {
4851 cmd: "rustc --edition 2021 main.rs",
4852 expect_compilation: true,
4853 expected_kind: Some(CompilationKind::Rustc),
4854 reason_contains: "rustc",
4855 min_confidence: 0.90,
4856 },
4857 ];
4858 run_cases(&cases);
4859 }
4860
4861 #[test]
4866 fn regression_shell_structure_exclusions() {
4867 let _guard = test_guard!();
4868 let cases = [
4869 Case {
4871 cmd: "cargo build 2>&1 | grep error",
4872 expect_compilation: false,
4873 expected_kind: None,
4874 reason_contains: "piped",
4875 min_confidence: 0.0,
4876 },
4877 Case {
4878 cmd: "cargo test | tee output.log",
4879 expect_compilation: false,
4880 expected_kind: None,
4881 reason_contains: "piped",
4882 min_confidence: 0.0,
4883 },
4884 Case {
4886 cmd: "cargo build &",
4887 expect_compilation: false,
4888 expected_kind: None,
4889 reason_contains: "background",
4890 min_confidence: 0.0,
4891 },
4892 Case {
4894 cmd: "cargo build > log.txt",
4895 expect_compilation: false,
4896 expected_kind: None,
4897 reason_contains: "redirect",
4898 min_confidence: 0.0,
4899 },
4900 Case {
4901 cmd: "cargo build >> log.txt",
4902 expect_compilation: false,
4903 expected_kind: None,
4904 reason_contains: "redirect",
4905 min_confidence: 0.0,
4906 },
4907 Case {
4909 cmd: "cargo build < input.txt",
4910 expect_compilation: false,
4911 expected_kind: None,
4912 reason_contains: "input redirect",
4913 min_confidence: 0.0,
4914 },
4915 Case {
4917 cmd: "(cargo build)",
4918 expect_compilation: false,
4919 expected_kind: None,
4920 reason_contains: "subshell",
4921 min_confidence: 0.0,
4922 },
4923 Case {
4924 cmd: "$(cargo build)",
4925 expect_compilation: false,
4926 expected_kind: None,
4927 reason_contains: "subshell",
4928 min_confidence: 0.0,
4929 },
4930 Case {
4931 cmd: "`cargo build`",
4932 expect_compilation: false,
4933 expected_kind: None,
4934 reason_contains: "subshell capture",
4935 min_confidence: 0.0,
4936 },
4937 Case {
4939 cmd: "cargo build --config <(echo ...)",
4940 expect_compilation: false,
4941 expected_kind: None,
4942 reason_contains: "subshell",
4943 min_confidence: 0.0,
4944 },
4945 Case {
4947 cmd: "cargo build; cargo test",
4948 expect_compilation: false,
4949 expected_kind: None,
4950 reason_contains: "chained",
4951 min_confidence: 0.0,
4952 },
4953 Case {
4955 cmd: "cargo build || echo failed",
4956 expect_compilation: false,
4957 expected_kind: None,
4958 reason_contains: "chained",
4959 min_confidence: 0.0,
4960 },
4961 Case {
4963 cmd: "cargo build\nrm -rf /",
4964 expect_compilation: false,
4965 expected_kind: None,
4966 reason_contains: "newline",
4967 min_confidence: 0.0,
4968 },
4969 Case {
4970 cmd: "cargo build\r\nevil",
4971 expect_compilation: false,
4972 expected_kind: None,
4973 reason_contains: "newline",
4974 min_confidence: 0.0,
4975 },
4976 ];
4977 run_cases(&cases);
4978 }
4979
4980 #[test]
4985 fn regression_fd_redirect_safe() {
4986 let _guard = test_guard!();
4987 let result = classify_command("cargo build 2>&1");
4993 assert!(
4994 result.is_compilation,
4995 "cargo build 2>&1 should be classified as compilation (fd redirect is safe), got: {:?}",
4996 result.reason
4997 );
4998 assert_eq!(result.kind, Some(CompilationKind::CargoBuild));
4999 }
5000
5001 #[test]
5006 fn regression_compound_commands() {
5007 let _guard = test_guard!();
5008 let result = classify_command("cd /path && cargo build");
5010 assert!(
5011 result.is_compilation,
5012 "cd && cargo build should compile, got: {:?}",
5013 result.reason
5014 );
5015 assert_eq!(result.kind, Some(CompilationKind::CargoBuild));
5016 assert!(result.command_prefix.is_some(), "should have prefix");
5017 assert!(
5018 result.extracted_command.is_some(),
5019 "should have extracted command"
5020 );
5021
5022 let result = classify_command("cd /path && cargo test --workspace");
5023 assert!(
5024 result.is_compilation,
5025 "cd && cargo test should compile, got: {:?}",
5026 result.reason
5027 );
5028 assert_eq!(result.kind, Some(CompilationKind::CargoTest));
5029
5030 let result = classify_command("cd /path && cargo fmt");
5032 assert!(!result.is_compilation, "cd && cargo fmt should NOT compile");
5033
5034 let result = classify_command("cd /path && ls -la");
5035 assert!(!result.is_compilation, "cd && ls should NOT compile");
5036 }
5037
5038 #[test]
5043 fn regression_wrapper_normalization() {
5044 let _guard = test_guard!();
5045 let cases = [
5046 Case {
5047 cmd: "sudo cargo build",
5048 expect_compilation: true,
5049 expected_kind: Some(CompilationKind::CargoBuild),
5050 reason_contains: "cargo build",
5051 min_confidence: 0.90,
5052 },
5053 Case {
5054 cmd: "time cargo test",
5055 expect_compilation: true,
5056 expected_kind: Some(CompilationKind::CargoTest),
5057 reason_contains: "cargo test",
5058 min_confidence: 0.90,
5059 },
5060 Case {
5061 cmd: "env RUST_BACKTRACE=1 cargo test",
5062 expect_compilation: true,
5063 expected_kind: Some(CompilationKind::CargoTest),
5064 reason_contains: "cargo test",
5065 min_confidence: 0.90,
5066 },
5067 Case {
5068 cmd: "RUST_BACKTRACE=1 cargo test",
5069 expect_compilation: true,
5070 expected_kind: Some(CompilationKind::CargoTest),
5071 reason_contains: "cargo test",
5072 min_confidence: 0.90,
5073 },
5074 Case {
5075 cmd: "nice cargo build --release",
5076 expect_compilation: true,
5077 expected_kind: Some(CompilationKind::CargoBuild),
5078 reason_contains: "cargo build",
5079 min_confidence: 0.90,
5080 },
5081 Case {
5082 cmd: "/usr/bin/cargo build",
5083 expect_compilation: true,
5084 expected_kind: Some(CompilationKind::CargoBuild),
5085 reason_contains: "cargo build",
5086 min_confidence: 0.90,
5087 },
5088 Case {
5089 cmd: "sudo env RUSTFLAGS=-Dwarnings cargo clippy",
5090 expect_compilation: true,
5091 expected_kind: Some(CompilationKind::CargoClippy),
5092 reason_contains: "cargo clippy",
5093 min_confidence: 0.85,
5094 },
5095 ];
5096 run_cases(&cases);
5097 }
5098
5099 #[test]
5104 fn regression_non_compilation_passthrough() {
5105 let _guard = test_guard!();
5106 let cases = [
5107 Case {
5108 cmd: "ls -la",
5109 expect_compilation: false,
5110 expected_kind: None,
5111 reason_contains: "no compilation keyword",
5112 min_confidence: 0.0,
5113 },
5114 Case {
5115 cmd: "pwd",
5116 expect_compilation: false,
5117 expected_kind: None,
5118 reason_contains: "no compilation keyword",
5119 min_confidence: 0.0,
5120 },
5121 Case {
5122 cmd: "echo hello",
5123 expect_compilation: false,
5124 expected_kind: None,
5125 reason_contains: "no compilation keyword",
5126 min_confidence: 0.0,
5127 },
5128 Case {
5129 cmd: "cat Cargo.toml",
5130 expect_compilation: false,
5131 expected_kind: None,
5132 reason_contains: "no compilation keyword",
5133 min_confidence: 0.0,
5134 },
5135 Case {
5136 cmd: "git status",
5137 expect_compilation: false,
5138 expected_kind: None,
5139 reason_contains: "no compilation keyword",
5140 min_confidence: 0.0,
5141 },
5142 Case {
5143 cmd: "git commit -m 'fix'",
5144 expect_compilation: false,
5145 expected_kind: None,
5146 reason_contains: "no compilation keyword",
5147 min_confidence: 0.0,
5148 },
5149 Case {
5150 cmd: "npm install",
5151 expect_compilation: false,
5152 expected_kind: None,
5153 reason_contains: "no compilation keyword",
5154 min_confidence: 0.0,
5155 },
5156 Case {
5157 cmd: "yarn build",
5158 expect_compilation: false,
5159 expected_kind: None,
5160 reason_contains: "no compilation keyword",
5161 min_confidence: 0.0,
5162 },
5163 Case {
5164 cmd: "python main.py",
5165 expect_compilation: false,
5166 expected_kind: None,
5167 reason_contains: "no compilation keyword",
5168 min_confidence: 0.0,
5169 },
5170 Case {
5171 cmd: "go build .",
5172 expect_compilation: false,
5173 expected_kind: None,
5174 reason_contains: "no compilation keyword",
5175 min_confidence: 0.0,
5176 },
5177 Case {
5178 cmd: "docker build -t myapp .",
5179 expect_compilation: false,
5180 expected_kind: None,
5181 reason_contains: "no compilation keyword",
5182 min_confidence: 0.0,
5183 },
5184 Case {
5185 cmd: "pip install -r requirements.txt",
5186 expect_compilation: false,
5187 expected_kind: None,
5188 reason_contains: "no compilation keyword",
5189 min_confidence: 0.0,
5190 },
5191 Case {
5192 cmd: "curl https://example.com",
5193 expect_compilation: false,
5194 expected_kind: None,
5195 reason_contains: "no compilation keyword",
5196 min_confidence: 0.0,
5197 },
5198 Case {
5199 cmd: "mkdir -p build",
5200 expect_compilation: false,
5201 expected_kind: None,
5202 reason_contains: "no compilation keyword",
5203 min_confidence: 0.0,
5204 },
5205 Case {
5206 cmd: "rm -rf target/",
5207 expect_compilation: false,
5208 expected_kind: None,
5209 reason_contains: "no compilation keyword",
5210 min_confidence: 0.0,
5211 },
5212 Case {
5213 cmd: "cp -r src/ backup/",
5214 expect_compilation: false,
5215 expected_kind: None,
5216 reason_contains: "no compilation keyword",
5217 min_confidence: 0.0,
5218 },
5219 Case {
5220 cmd: "",
5221 expect_compilation: false,
5222 expected_kind: None,
5223 reason_contains: "empty command",
5224 min_confidence: 0.0,
5225 },
5226 Case {
5227 cmd: " ",
5228 expect_compilation: false,
5229 expected_kind: None,
5230 reason_contains: "empty command",
5231 min_confidence: 0.0,
5232 },
5233 ];
5234 run_cases(&cases);
5235 }
5236
5237 #[test]
5242 fn regression_classification_details_fields() {
5243 let _guard = test_guard!();
5244
5245 let details = classify_command_detailed("cargo build --release");
5247 assert_eq!(details.original, "cargo build --release");
5248 assert!(details.classification.is_compilation);
5249 assert_eq!(
5250 details.classification.kind,
5251 Some(CompilationKind::CargoBuild)
5252 );
5253 assert!(details.classification.confidence >= 0.90);
5254 assert_eq!(details.tiers.len(), 5, "Should have 5 tier decisions");
5256 for tier in &details.tiers[..4] {
5257 assert_eq!(tier.decision, TierDecision::Pass);
5258 }
5259 assert_eq!(details.tiers[4].decision, TierDecision::Pass);
5261 assert_eq!(details.tiers[4].tier, 4);
5262
5263 let details = classify_command_detailed("ls -la");
5265 assert!(!details.classification.is_compilation);
5266 assert!(
5267 details.tiers.len() >= 3,
5268 "Should have at least 3 tiers for keyword rejection"
5269 );
5270 assert_eq!(details.tiers.last().unwrap().decision, TierDecision::Reject);
5271
5272 let details = classify_command_detailed("cargo fmt --check");
5274 assert!(!details.classification.is_compilation);
5275 assert!(
5276 details.tiers.len() >= 4,
5277 "Should have at least 4 tiers for never-intercept rejection"
5278 );
5279 let tier3 = details.tiers.iter().find(|t| t.tier == 3).unwrap();
5281 assert_eq!(tier3.decision, TierDecision::Reject);
5282 assert!(
5283 tier3.reason.contains("never-intercept"),
5284 "Tier 3 reason should mention never-intercept, got: {:?}",
5285 tier3.reason
5286 );
5287
5288 let details = classify_command_detailed("cargo build | grep error");
5290 assert!(!details.classification.is_compilation);
5291 let tier1 = details.tiers.iter().find(|t| t.tier == 1).unwrap();
5292 assert_eq!(tier1.decision, TierDecision::Reject);
5293 assert!(tier1.reason.contains("piped"));
5294
5295 let details = classify_command_detailed("");
5297 assert!(!details.classification.is_compilation);
5298 assert_eq!(details.tiers[0].tier, 0);
5299 assert_eq!(details.tiers[0].decision, TierDecision::Reject);
5300 }
5301
5302 #[test]
5307 fn regression_every_compilation_has_reason_and_confidence() {
5308 let _guard = test_guard!();
5309 let compilation_cmds = [
5310 "cargo build",
5311 "cargo test",
5312 "cargo check",
5313 "cargo clippy",
5314 "cargo doc",
5315 "cargo run",
5316 "cargo bench",
5317 "cargo nextest run",
5318 "rustc main.rs",
5319 "gcc -c main.c -o main.o",
5320 "g++ -c main.cpp -o main.o",
5321 "clang -c main.c -o main.o",
5322 "clang++ -c main.cpp -o main.o",
5323 "make",
5324 "cmake --build build",
5325 "ninja",
5326 "meson compile",
5327 "bun test",
5328 "bun typecheck",
5329 ];
5330 for cmd in compilation_cmds {
5331 let result = classify_command(cmd);
5332 assert!(result.is_compilation, "{cmd:?} should be compilation");
5333 assert!(
5334 result.kind.is_some(),
5335 "{cmd:?} should have a CompilationKind"
5336 );
5337 assert!(
5338 result.confidence > 0.0,
5339 "{cmd:?} should have non-zero confidence"
5340 );
5341 assert!(
5342 !result.reason.is_empty(),
5343 "{cmd:?} should have a non-empty reason"
5344 );
5345 }
5346 }
5347
5348 #[test]
5353 fn regression_every_rejection_has_reason() {
5354 let _guard = test_guard!();
5355 let non_compilation_cmds = [
5356 "",
5357 "ls",
5358 "pwd",
5359 "git status",
5360 "cargo fmt",
5361 "cargo install ripgrep",
5362 "cargo clean",
5363 "bun install",
5364 "bun add react",
5365 "bun run dev",
5366 "bun dev",
5367 "bun repl",
5368 "bun x vitest",
5369 "bun test --watch",
5370 "bun typecheck -w",
5371 "make clean",
5372 "ninja clean",
5373 "gcc --version",
5374 "rustc --version",
5375 "cargo build | grep error",
5376 "cargo build &",
5377 "cargo build > log.txt",
5378 "(cargo build)",
5379 ];
5380 for cmd in non_compilation_cmds {
5381 let result = classify_command(cmd);
5382 assert!(
5383 !result.is_compilation,
5384 "{cmd:?} should NOT be compilation, got kind={:?}",
5385 result.kind
5386 );
5387 assert!(
5388 !result.reason.is_empty(),
5389 "{cmd:?} should have a non-empty rejection reason"
5390 );
5391 assert_eq!(result.kind, None, "{cmd:?} should have no CompilationKind");
5392 assert_eq!(
5393 result.confidence, 0.0,
5394 "{cmd:?} should have zero confidence"
5395 );
5396 }
5397 }
5398
5399 #[test]
5404 fn regression_real_world_agent_workflows() {
5405 let _guard = test_guard!();
5406
5407 let cd_build = classify_command(
5409 "cd /data/projects/remote_compilation_helper && cargo build --workspace --all-targets",
5410 );
5411 assert!(
5412 cd_build.is_compilation,
5413 "agent cd+build workflow should be intercepted"
5414 );
5415
5416 let test_ws = classify_command("cargo test --workspace -- --nocapture");
5418 assert!(test_ws.is_compilation);
5419 assert_eq!(test_ws.kind, Some(CompilationKind::CargoTest));
5420
5421 let clippy = classify_command("cargo clippy --workspace --all-targets -- -D warnings");
5423 assert!(clippy.is_compilation);
5424 assert_eq!(clippy.kind, Some(CompilationKind::CargoClippy));
5425
5426 let fmt = classify_command("cargo fmt --check");
5428 assert!(!fmt.is_compilation);
5429
5430 let cat = classify_command("cat Cargo.toml");
5432 assert!(!cat.is_compilation);
5433
5434 let gs = classify_command("git status");
5436 assert!(!gs.is_compilation);
5437
5438 let install = classify_command("cargo install cargo-nextest");
5440 assert!(!install.is_compilation);
5441
5442 let pkg_test = classify_command("cargo test -p rch-common");
5444 assert!(pkg_test.is_compilation);
5445 assert_eq!(pkg_test.kind, Some(CompilationKind::CargoTest));
5446
5447 let env_test = classify_command("RUST_BACKTRACE=1 cargo test --workspace");
5449 assert!(env_test.is_compilation);
5450 assert_eq!(env_test.kind, Some(CompilationKind::CargoTest));
5451
5452 let nxt = classify_command("cargo nextest run --workspace --no-fail-fast");
5454 assert!(nxt.is_compilation);
5455 assert_eq!(nxt.kind, Some(CompilationKind::CargoNextest));
5456 }
5457
5458 #[test]
5463 fn regression_keyword_in_non_compilation_context() {
5464 let _guard = test_guard!();
5465 let result = classify_command("echo cargo build");
5467 assert!(
5471 !result.is_compilation,
5472 "echo cargo should not compile, got: {:?}",
5473 result.reason
5474 );
5475
5476 let result = classify_command("grep make Makefile");
5478 assert!(
5480 !result.is_compilation,
5481 "grep make should not compile, got: {:?}",
5482 result.reason
5483 );
5484
5485 let result = classify_command("cat gcc_output.log");
5487 assert!(
5489 !result.is_compilation,
5490 "cat gcc_output should not compile, got: {:?}",
5491 result.reason
5492 );
5493 }
5494
5495 #[test]
5500 fn regression_compilation_kind_properties() {
5501 let _guard = test_guard!();
5502
5503 assert!(CompilationKind::CargoTest.is_test_command());
5505 assert!(CompilationKind::CargoNextest.is_test_command());
5506 assert!(CompilationKind::CargoBench.is_test_command());
5507 assert!(CompilationKind::BunTest.is_test_command());
5508
5509 assert!(!CompilationKind::CargoBuild.is_test_command());
5511 assert!(!CompilationKind::CargoCheck.is_test_command());
5512 assert!(!CompilationKind::CargoClippy.is_test_command());
5513 assert!(!CompilationKind::CargoDoc.is_test_command());
5514 assert!(!CompilationKind::BunTypecheck.is_test_command());
5515 assert!(!CompilationKind::Gcc.is_test_command());
5516 assert!(!CompilationKind::Gpp.is_test_command());
5517 assert!(!CompilationKind::Clang.is_test_command());
5518 assert!(!CompilationKind::Clangpp.is_test_command());
5519 assert!(!CompilationKind::Make.is_test_command());
5520 assert!(!CompilationKind::CmakeBuild.is_test_command());
5521 assert!(!CompilationKind::Ninja.is_test_command());
5522 assert!(!CompilationKind::Meson.is_test_command());
5523 assert!(!CompilationKind::Rustc.is_test_command());
5524
5525 assert_eq!(CompilationKind::CargoBuild.command_base(), "cargo");
5527 assert_eq!(CompilationKind::CargoTest.command_base(), "cargo");
5528 assert_eq!(CompilationKind::CargoNextest.command_base(), "cargo");
5529 assert_eq!(CompilationKind::Rustc.command_base(), "rustc");
5530 assert_eq!(CompilationKind::Gcc.command_base(), "gcc");
5531 assert_eq!(CompilationKind::Gpp.command_base(), "g++");
5532 assert_eq!(CompilationKind::Clang.command_base(), "clang");
5533 assert_eq!(CompilationKind::Clangpp.command_base(), "clang++");
5534 assert_eq!(CompilationKind::Make.command_base(), "make");
5535 assert_eq!(CompilationKind::CmakeBuild.command_base(), "cmake");
5536 assert_eq!(CompilationKind::Ninja.command_base(), "ninja");
5537 assert_eq!(CompilationKind::Meson.command_base(), "meson");
5538 assert_eq!(CompilationKind::BunTest.command_base(), "bun");
5539 assert_eq!(CompilationKind::BunTypecheck.command_base(), "bun");
5540 }
5541}