Skip to main content

rch_common/
patterns.rs

1//! Command classification patterns for identifying compilation commands.
2//!
3//! Implements the 5-tier classification system:
4//! - Tier 0: Instant reject (non-Bash, empty)
5//! - Tier 1: Structure analysis (pipes, redirects, background)
6//! - Tier 2: SIMD keyword filter
7//! - Tier 3: Negative pattern check
8//! - Tier 4: Full classification with confidence
9
10use memchr::memmem;
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use std::borrow::Cow;
14
15// Tier name constants (avoid allocations on hot path).
16const 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
22/// Keywords that indicate a potential compilation command.
23/// Used for SIMD-accelerated quick filtering (Tier 2).
24pub static COMPILATION_KEYWORDS: &[&str] = &[
25    "cargo", "rustc", "gcc", "g++", "clang", "clang++", "make", "cmake", "ninja", "meson", "cc",
26    "c++", "bun", "nextest",
27];
28
29/// Commands that should NEVER be intercepted, even if they contain compilation keywords.
30/// These either modify local state or have dependencies on local execution.
31pub static NEVER_INTERCEPT: &[&str] = &[
32    // Cargo commands that modify local state or shouldn't be intercepted
33    "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    // Compiler version checks
49    "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 package management - MUST NOT intercept (modifies local state)
59    "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 execution that shouldn't be intercepted
70    "bun run",
71    "bun build",
72    "bun --help",
73    "bun -h",
74    "bun --version",
75    "bun -v",
76    // Bun dev/repl - require local interactivity
77    "bun dev",
78    "bun repl",
79    // cargo-nextest commands that shouldn't be intercepted
80    "cargo nextest list",    // Lists tests only, doesn't run them
81    "cargo nextest archive", // Creates test archives
82    "cargo nextest show",    // Shows config/setup info
83];
84
85/// Result of command classification.
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
87pub struct Classification {
88    /// Whether this is a compilation command.
89    pub is_compilation: bool,
90    /// Confidence score (0.0-1.0).
91    pub confidence: f64,
92    /// The kind of compilation if detected.
93    pub kind: Option<CompilationKind>,
94    /// Reason for the classification decision.
95    pub reason: Cow<'static, str>,
96    /// For compound commands (e.g., "cd /path && cargo build"), this contains
97    /// the prefix that should run before the compilation ("cd /path && ").
98    /// None for simple commands.
99    pub command_prefix: Option<String>,
100    /// For compound commands, this contains the extracted compilation command
101    /// (e.g., "cargo build --release"). None for simple commands where the
102    /// entire input is the compilation command.
103    pub extracted_command: Option<String>,
104}
105
106/// Decision outcome for a classification tier.
107#[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/// Detailed result for a single classification tier.
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
116pub struct ClassificationTier {
117    /// Tier index (0-4).
118    pub tier: u8,
119    /// Tier name.
120    pub name: Cow<'static, str>,
121    /// Decision for this tier.
122    pub decision: TierDecision,
123    /// Reason for the decision.
124    pub reason: Cow<'static, str>,
125}
126
127/// Detailed classification results with per-tier decisions.
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
129pub struct ClassificationDetails {
130    /// Original command string.
131    pub original: String,
132    /// Normalized command string (wrappers stripped).
133    pub normalized: String,
134    /// Per-tier decisions.
135    pub tiers: Vec<ClassificationTier>,
136    /// Final classification result.
137    pub classification: Classification,
138}
139
140impl Classification {
141    /// Create a non-compilation classification.
142    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    /// Create a compilation classification.
154    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    /// Create a compilation classification for a compound command.
170    /// The prefix contains everything before the compilation command (including the final "&&" or ";").
171    /// The extracted_command is the actual compilation command to intercept.
172    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/// Kind of compilation command detected.
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
192#[serde(rename_all = "snake_case")]
193pub enum CompilationKind {
194    // Rust commands
195    /// cargo build, cargo test, cargo check, etc.
196    CargoBuild,
197    CargoTest,
198    CargoCheck,
199    CargoClippy,
200    CargoDoc,
201    /// cargo nextest run - next-generation test runner
202    CargoNextest,
203    /// cargo bench - run benchmarks
204    CargoBench,
205    /// rustc invocation
206    Rustc,
207
208    // C/C++ commands
209    /// GCC compilation
210    Gcc,
211    /// G++ compilation
212    Gpp,
213    /// Clang compilation
214    Clang,
215    /// Clang++ compilation
216    Clangpp,
217
218    // Build systems
219    /// make build
220    Make,
221    /// cmake --build
222    CmakeBuild,
223    /// ninja build
224    Ninja,
225    /// meson compile
226    Meson,
227
228    // Bun commands
229    /// bun test - Runs Bun's built-in test runner
230    BunTest,
231    /// bun typecheck - Runs TypeScript type checking
232    BunTypecheck,
233}
234
235impl CompilationKind {
236    /// Returns true if this is a test-related command.
237    ///
238    /// Test commands have special cache affinity behavior because test binaries
239    /// (e.g., target/debug/deps/) are expensive to compile and benefit more
240    /// from warm caches than regular builds.
241    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    /// Returns the base command name for allowlist matching (bd-785w).
252    ///
253    /// This is the primary executable name that should appear in the
254    /// execution allowlist configuration.
255    pub fn command_base(&self) -> &'static str {
256        match self {
257            // Rust commands
258            CompilationKind::CargoBuild
259            | CompilationKind::CargoTest
260            | CompilationKind::CargoCheck
261            | CompilationKind::CargoClippy
262            | CompilationKind::CargoDoc
263            | CompilationKind::CargoBench => "cargo",
264            CompilationKind::CargoNextest => "cargo", // cargo nextest, base is still cargo
265            CompilationKind::Rustc => "rustc",
266            // C/C++ commands
267            CompilationKind::Gcc => "gcc",
268            CompilationKind::Gpp => "g++",
269            CompilationKind::Clang => "clang",
270            CompilationKind::Clangpp => "clang++",
271            // Build systems
272            CompilationKind::Make => "make",
273            CompilationKind::CmakeBuild => "cmake",
274            CompilationKind::Ninja => "ninja",
275            CompilationKind::Meson => "meson",
276            // Bun commands
277            CompilationKind::BunTest | CompilationKind::BunTypecheck => "bun",
278        }
279    }
280}
281
282/// Classify a shell command.
283///
284/// Implements the 5-tier classification system for maximum precision with
285/// minimal latency on non-compilation commands.
286///
287/// Multi-command strings (joined by `&&`, `||`, or `;`) are rejected by
288/// Tier 1 structure analysis to prevent partial interception issues.
289pub fn classify_command(cmd: &str) -> Classification {
290    classify_command_inner(cmd, 0)
291}
292
293/// Maximum recursion depth for multi-command splitting.
294/// Depth 0 = top-level call that may split; depth 1 = sub-command (no further splitting).
295#[allow(dead_code)] // Reserved for future multi-command classification
296const MAX_CLASSIFY_DEPTH: u8 = 1;
297
298/// Maximum command length for multi-command splitting (10 KB).
299/// Commands longer than this skip splitting and are classified as a single command.
300#[allow(dead_code)] // Reserved for future multi-command classification
301const MAX_SPLIT_INPUT_LEN: usize = 10 * 1024;
302
303fn classify_command_inner(cmd: &str, depth: u8) -> Classification {
304    let cmd = cmd.trim();
305
306    // Tier 0: Instant reject - empty command
307    if cmd.is_empty() {
308        return Classification::not_compilation("empty command");
309    }
310
311    // Tier 0.5: Compound command handling (only at depth 0)
312    // For commands like "cd /path && cargo build", extract and classify the last segment.
313    // We only handle && chains (not || or ;) at depth 0 to avoid complex rewriting.
314    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    // Tier 1: Structure analysis - reject complex shell structures
322    if let Some(reason) = check_structure(cmd) {
323        return Classification::not_compilation(reason);
324    }
325
326    // Tier 2: SIMD keyword filter - quick check for compilation keywords
327    if !contains_compilation_keyword(cmd) {
328        return Classification::not_compilation("no compilation keyword");
329    }
330
331    // Normalize command for Tier 3 and 4
332    let normalized_cow = normalize_command(cmd);
333    let normalized = normalized_cow.as_ref();
334
335    // Tier 3: Negative pattern check - never intercept these
336    // Performance: use static string to avoid format! allocation on hot rejection path
337    for pattern in NEVER_INTERCEPT {
338        if let Some(rest) = normalized.strip_prefix(pattern) {
339            // Ensure exact match or boundary match (e.g. "cargo clean" matches "cargo clean"
340            // or "cargo clean ", but NOT "cargo cleanup")
341            if rest.is_empty() || rest.starts_with(' ') {
342                return Classification::not_compilation("matches never-intercept pattern");
343            }
344        }
345    }
346
347    // Tier 4: Full classification
348    classify_full(normalized)
349}
350
351/// Try to classify a compound command (e.g., "cd /path && cargo build").
352///
353/// For && chains, we check if the LAST segment is a compilation command.
354/// If so, we return a compound_compilation with the prefix (everything before
355/// the compilation) and the extracted compilation command.
356///
357/// This allows commands like:
358/// - "cd /path && cargo build" → prefix="cd /path && ", extracted="cargo build"
359/// - "touch file && cargo test" → prefix="touch file && ", extracted="cargo test"
360///
361/// We only handle && chains, not || or ; for safety (those have different semantics).
362/// We also reject commands with pipes, redirects, or subshells in ANY segment.
363fn try_classify_compound_command(cmd: &str) -> Option<Classification> {
364    // Quick check: must contain && but not other problematic operators
365    if !cmd.contains("&&") {
366        return None;
367    }
368
369    // Reject if command contains pipes, redirects, or subshells (anywhere)
370    // These make command rewriting unsafe
371    if cmd.contains('|') {
372        return None; // Pipe or || chain
373    }
374    if has_file_redirect(cmd) {
375        return None; // File redirects (but NOT fd-to-fd like 2>&1)
376    }
377    if cmd.contains('(') || cmd.contains('`') || cmd.contains("$(") {
378        return None; // Subshells
379    }
380    if cmd.contains(';') {
381        return None; // Only handle pure && chains for now
382    }
383
384    // Split on && (simple split, respects quotes via split_and_chain)
385    let segments = split_and_chain(cmd)?;
386    if segments.is_empty() {
387        return None;
388    }
389
390    // Get the last segment and classify it
391    let last_segment = segments.last()?.trim();
392    if last_segment.is_empty() {
393        return None;
394    }
395
396    // Classify the last segment at depth 1 (prevents infinite recursion)
397    let last_classification = classify_command_inner(last_segment, 1);
398
399    if !last_classification.is_compilation {
400        return None;
401    }
402
403    // Build the prefix (everything before the last segment, including the final "&&")
404    let prefix = if segments.len() == 1 {
405        // No prefix, just the command itself
406        return None;
407    } else {
408        // Find where the last segment starts in the original command
409        // and take everything before it
410        let last_pos = cmd.rfind(last_segment)?;
411        cmd[..last_pos].to_string()
412    };
413
414    // Return compound classification
415    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
424/// Split a command on && only (simpler than split_multi_command).
425/// Returns None if no && found.
426fn 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; // Skip second '&'
471        }
472
473        i += 1;
474    }
475
476    // Add final segment
477    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/// Split a multi-command string on `&&`, `||`, and `;` while respecting quotes.
491///
492/// Returns `None` if no multi-command operators are found.
493/// Returns `Some(segments)` where each segment is a trimmed sub-command.
494#[allow(dead_code)] // May be used for future enhancements
495fn split_multi_command(cmd: &str) -> Option<Vec<&str>> {
496    // Quick check: if no operators, return None early
497    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            // Check for operators
530            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; // Skip second '&'
545            } 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; // Skip second '|'
553            }
554        }
555
556        i += 1;
557    }
558
559    // Add final segment
560    let final_segment = &cmd[current_start..];
561    let trimmed = final_segment.trim();
562    if !trimmed.is_empty() {
563        segments.push(trimmed);
564    }
565
566    // Only return Some if we actually split something
567    if segments.len() > 1 {
568        Some(segments)
569    } else {
570        None
571    }
572}
573
574/// Convert char index to byte index for string slicing.
575#[allow(dead_code)] // Reserved for future multi-command classification
576fn byte_index(chars: &[char], char_idx: usize) -> usize {
577    chars.iter().take(char_idx).map(|c| c.len_utf8()).sum()
578}
579
580/// Classify a multi-command string by evaluating each sub-command independently.
581///
582/// Returns compilation if ANY sub-command is compilation (highest confidence wins).
583/// Returns non-compilation only if ALL sub-commands are non-compilation.
584#[allow(dead_code)] // Reserved for future multi-command classification
585fn 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
608/// Classify a shell command with detailed tier decisions for diagnostics.
609pub 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    // Tier 0: Instant reject - empty command
615    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    // Tier 1: Structure analysis
639    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    // Tier 2: SIMD keyword filter
663    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    // Normalize command for Tier 3 and 4
687    let normalized_cow = normalize_command(cmd);
688    let normalized = normalized_cow.as_ref();
689
690    // Tier 3: Negative pattern check - never intercept these
691    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    // Tier 4: Full classification
721    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
742/// Normalize a command by stripping common wrappers (sudo, time, env, etc.)
743pub fn normalize_command(cmd: &str) -> Cow<'_, str> {
744    let mut result = cmd.trim();
745
746    // Strip common command prefixes/wrappers
747    // Note: We match the command name, then ensure it's followed by whitespace
748    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                // Must be followed by whitespace or end of string
757                // (e.g., "sudo" matches "sudo ls", but not "sudoku")
758                if rest.is_empty() || rest.starts_with(char::is_whitespace) {
759                    result = rest.trim_start();
760                    changed = true;
761
762                    // Strip flags that might follow the wrapper (e.g., "time -v", "sudo -E")
763                    // We heuristically strip any token starting with '-' until we find a non-flag.
764                    while result.starts_with('-') {
765                        // Find the end of this token
766                        let end_idx = result.find(char::is_whitespace).unwrap_or(result.len());
767
768                        // Safety: we checked starts_with('-'), so token is not empty
769                        result = result[end_idx..].trim_start();
770                    }
771                }
772            }
773        }
774
775        // Handle env VAR=val syntax
776        // Logic: if result starts with VAR=val (potentially quoted), strip it.
777        // We need to parse the first token respecting quotes.
778        let chars = result.chars();
779        let mut token_len = 0;
780        let mut in_quote = None; // None, Some('\''), Some('"')
781        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            // It was a complete token with an equals sign followed by space.
821            // We assume it's an env var and strip it.
822            result = result[token_len..].trim_start();
823            changed = true;
824        }
825
826        // Strip absolute paths: /usr/bin/cargo -> cargo
827        // We assume the command is the first word
828        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 becomes substring starting after the last slash of the command word
833                    result = &result[last_slash + 1..];
834                    changed = true;
835                }
836            } else {
837                // Single word command
838                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
857/// Check if a command contains file redirects (NOT fd-to-fd redirects like `2>&1`).
858///
859/// Returns true for file redirects: `> file`, `>> file`, `2> file`, `&> file`
860/// Returns false for fd-to-fd redirects: `2>&1`, `>&2`, `1>&2`
861/// Also returns true for input redirects: `< file`
862///
863/// This is used by `try_classify_compound_command` which needs a quick text-level
864/// check before doing more expensive parsing.
865fn 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                // Check if this is a fd-to-fd redirect (N>&M or >&N)
895                if i + 1 < len && bytes[i + 1] == b'&' {
896                    // >&N or N>&M — fd-to-fd, skip
897                    i += 2; // skip > and &
898                    // skip optional digit
899                    if i < len && bytes[i].is_ascii_digit() {
900                        i += 1;
901                    }
902                    continue;
903                }
904                // >( is process substitution, not a file redirect per se,
905                // but it's caught by the subshell check separately
906                if i + 1 < len && bytes[i + 1] == b'(' {
907                    i += 1;
908                    continue;
909                }
910                return true; // File redirect: > file, >> file, N> file
911            }
912            b'<' if !in_single && !in_double => {
913                // <( is process substitution, caught separately
914                if i + 1 < len && bytes[i + 1] == b'(' {
915                    i += 1;
916                    continue;
917                }
918                return true; // Input redirect: < file
919            }
920            _ => {}
921        }
922        i += 1;
923    }
924    false
925}
926
927/// Check command structure for patterns that shouldn't be intercepted.
928///
929/// Performance: single-pass O(n) state machine instead of 11+ separate scans.
930/// This is critical because structure analysis runs on EVERY command.
931///
932/// Detection priority (matches original behavior):
933/// 1. Embedded newlines (security - command injection)
934/// 2. Standalone & (backgrounding)
935/// 3. Single | pipe (not ||)
936/// 4. Subshell ( or >( or <(
937/// 5. Output redirection >
938/// 6. Input redirection <
939/// 7. Semicolon ;
940/// 8. && chaining
941/// 9. || chaining
942/// 10. Subshell capture $( or backtick
943fn 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    // Track what we find (in priority order, lower = higher priority)
952    // Using Option to track if found, with priority encoded by check order
953    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        // Handle escape sequences
968        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        // Handle quote state transitions
980        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        // Skip quoted content
992        if in_single || in_double {
993            i += 1;
994            continue;
995        }
996
997        // Check for problematic characters/patterns (unquoted)
998        match b {
999            // Embedded newlines - return immediately (highest priority, security issue)
1000            b'\n' | b'\r' => return Some("contains embedded newline"),
1001
1002            // Ampersand: check for && vs standalone &
1003            b'&' => {
1004                if i + 1 < len && bytes[i + 1] == b'&' {
1005                    found_and_chain = true;
1006                    i += 1; // Skip second &
1007                } else {
1008                    // Check for redirection patterns like 2>&1 or &>
1009                    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            // Pipe: check for || vs single |
1018            b'|' => {
1019                if i + 1 < len && bytes[i + 1] == b'|' {
1020                    found_or_chain = true;
1021                    i += 1; // Skip second |
1022                } else {
1023                    found_piped = true;
1024                }
1025            }
1026
1027            // Subshell/process substitution
1028            b'(' => found_subshell = true,
1029
1030            // Output redirection (>( is process substitution, caught by ( check)
1031            // fd-to-fd redirects like 2>&1, >&2 are safe to intercept
1032            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                    // Pattern: N>&M or >&N (fd-to-fd redirect) — safe, skip
1037                    // e.g. 2>&1, 1>&2, >&2
1038                    // Skip the '&' and any following digit
1039                    i += 1; // skip '&'
1040                    if i + 1 < len && bytes[i + 1].is_ascii_digit() {
1041                        i += 1; // skip digit after &
1042                    }
1043                } else if i + 1 < len && bytes[i + 1] == b'>' {
1044                    // Append redirect >> (to file) — NOT safe
1045                    found_output_redirect = true;
1046                    i += 1; // skip second >
1047                } else {
1048                    found_output_redirect = true;
1049                }
1050            }
1051
1052            // Input redirection (<( is process substitution, caught by ( check)
1053            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            // Semicolon chaining
1062            b';' => found_semicolon = true,
1063
1064            // Backtick subshell capture
1065            b'`' => found_subshell_capture = true,
1066
1067            // Dollar sign: check for $( subshell capture
1068            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    // Return in priority order (matches original check_structure behavior)
1079    if found_backgrounded {
1080        return Some("backgrounded command");
1081    }
1082    // Pipe check: only if not ||
1083    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
1111/// Check if command contains any compilation keyword (SIMD-accelerated).
1112fn 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
1122/// Full classification of a command (Tier 4).
1123fn classify_full(cmd: &str) -> Classification {
1124    // Cargo commands
1125    if cmd.starts_with("cargo ") || cmd == "cargo" {
1126        return classify_cargo(cmd);
1127    }
1128
1129    // rustc
1130    if cmd.starts_with("rustc ") || cmd == "rustc" {
1131        return Classification::compilation(CompilationKind::Rustc, 0.95, "rustc invocation");
1132    }
1133
1134    // GCC
1135    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    // G++
1142    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    // Clang
1152    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    // Clang++
1160    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    // cc (standard C compiler)
1170    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    // c++ (standard C++ compiler)
1177    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    // Make
1187    if cmd.starts_with("make") && (cmd == "make" || cmd.starts_with("make ")) {
1188        // Don't intercept "make clean", "make install", etc.
1189        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    // CMake build (only when cmake is the invoked command)
1196    if cmd.starts_with("cmake ") || cmd == "cmake" {
1197        let mut tokens = cmd.split_whitespace();
1198        let _ = tokens.next(); // "cmake"
1199        if tokens.any(|token| token == "--build" || token.starts_with("--build=")) {
1200            return Classification::compilation(CompilationKind::CmakeBuild, 0.90, "cmake --build");
1201        }
1202    }
1203
1204    // Ninja
1205    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    // Meson (only when meson is the invoked command)
1213    if cmd.starts_with("meson ") || cmd == "meson" {
1214        let mut tokens = cmd.split_whitespace();
1215        let _ = tokens.next(); // "meson"
1216        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    // Bun commands
1230    let mut tokens = cmd.split_whitespace();
1231    if tokens.next() == Some("bun") {
1232        match tokens.next() {
1233            Some("test") => {
1234                // Check for --watch flag - don't intercept interactive mode
1235                // Clone the iterator to check remaining args
1236                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                // Check for --watch flag - don't intercept interactive mode
1249                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                // bun x (alias for bunx - package runner)
1262                // NOT intercepted - similar to npx, runs arbitrary packages
1263                return Classification::not_compilation("bun x runs arbitrary packages");
1264            }
1265            _ => {}
1266        }
1267    }
1268
1269    Classification::not_compilation("no matching pattern")
1270}
1271
1272/// Classify cargo subcommands.
1273///
1274/// Performance: uses iterator `.nth()` to avoid Vec allocation on the hot path.
1275/// We only need tokens 0-3 (cargo, [+toolchain], subcommand, [nextest-subcommand]).
1276fn classify_cargo(cmd: &str) -> Classification {
1277    let mut tokens = cmd.split_whitespace();
1278
1279    // Token 0: "cargo" (already validated by caller)
1280    let _cargo = tokens.next();
1281
1282    // Token 1: subcommand or +toolchain
1283    let Some(token1) = tokens.next() else {
1284        return Classification::not_compilation("bare cargo command");
1285    };
1286
1287    // Handle toolchain overrides (e.g., cargo +nightly build)
1288    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            // cargo run compiles first, so it's a compilation command
1309            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            // cargo nextest has subcommands: run, list, archive, show
1318            // Only intercept "run" - the actual test execution
1319            // Next token is the nextest subcommand (e.g., "run", "list")
1320            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
1337/// Split a shell command string on unquoted `;`, `&&`, and `||` operators.
1338///
1339/// Returns a list of sub-commands with each segment trimmed of whitespace.
1340/// Characters inside single quotes, double quotes, or backticks are treated
1341/// as literal and are never split. Escaped quotes (`\'`, `\"`) do not toggle
1342/// quote state. Pipes (`|`) within a segment are preserved (they are part of
1343/// one command, not a command separator).
1344///
1345/// Performance: single-pass O(n) character state machine, no regex, no heap
1346/// allocation for the common single-command case.
1347pub 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    // Quote state
1355    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        // Toggle quote state
1376        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        // Only split when outside all quotes
1393        if in_single || in_double || in_backtick {
1394            i += 1;
1395            continue;
1396        }
1397
1398        // Check for `&&`
1399        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        // Check for `||`
1410        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        // Check for `;`
1421        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    // Push the final segment
1435    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        // Newlines could cause command injection via `sh -c`
1533        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    // Bun tests - keyword detection and never-intercept patterns
1637
1638    #[test]
1639    fn test_bun_keyword_detected() {
1640        let _guard = test_guard!();
1641        // Verify "bun" triggers keyword detection (Tier 2 passes)
1642        assert!(contains_compilation_keyword("bun test"));
1643        assert!(contains_compilation_keyword("bun typecheck"));
1644        assert!(contains_compilation_keyword("bun install")); // keyword present, but will be blocked in Tier 3
1645    }
1646
1647    #[test]
1648    fn test_bun_install_not_intercepted() {
1649        let _guard = test_guard!();
1650        // Package management - modifies node_modules
1651        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        // Adding packages - modifies package.json and node_modules
1660        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        // Generic script runner - could do anything
1677        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        // Creates bundles in local directory
1686        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        // Development server needs local ports
1707        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        // Interactive REPL
1716        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    // Bun Tier 4 classification tests
1766
1767    #[test]
1768    fn test_bun_test_classification() {
1769        let _guard = test_guard!();
1770        // Basic command
1771        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        // With directory argument
1777        let result = classify_command("bun test src/");
1778        assert!(result.is_compilation);
1779        assert_eq!(result.kind, Some(CompilationKind::BunTest));
1780
1781        // With coverage flag (non-interactive)
1782        let result = classify_command("bun test --coverage");
1783        assert!(result.is_compilation);
1784        assert_eq!(result.kind, Some(CompilationKind::BunTest));
1785
1786        // Specific file
1787        let result = classify_command("bun test auth.test.ts");
1788        assert!(result.is_compilation);
1789        assert_eq!(result.kind, Some(CompilationKind::BunTest));
1790
1791        // With multiple flags
1792        let result = classify_command("bun test --bail --timeout 5000");
1793        assert!(result.is_compilation);
1794        assert_eq!(result.kind, Some(CompilationKind::BunTest));
1795
1796        // With reporter
1797        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        // --watch mode is interactive and should NOT be intercepted
1806        let result = classify_command("bun test --watch");
1807        assert!(!result.is_compilation);
1808        assert!(result.reason.contains("interactive"));
1809
1810        // Watch with other flags
1811        let result = classify_command("bun test --watch --coverage");
1812        assert!(!result.is_compilation);
1813        assert!(result.reason.contains("interactive"));
1814
1815        // Watch with directory
1816        let result = classify_command("bun test src/ --watch");
1817        assert!(!result.is_compilation);
1818
1819        // Short form -w
1820        let result = classify_command("bun test -w");
1821        assert!(!result.is_compilation);
1822
1823        // Short form with args
1824        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        // --watch mode is interactive and should NOT be intercepted
1832        let result = classify_command("bun typecheck --watch");
1833        assert!(!result.is_compilation);
1834        assert!(result.reason.contains("interactive"));
1835
1836        // Short form -w
1837        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        // Basic command
1845        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        // With directory argument
1851        let result = classify_command("bun typecheck src/");
1852        assert!(result.is_compilation);
1853        assert_eq!(result.kind, Some(CompilationKind::BunTypecheck));
1854
1855        // NOTE: --watch mode is tested separately in test_bun_typecheck_watch_not_intercepted
1856        // It should NOT be intercepted as it's interactive
1857    }
1858
1859    #[test]
1860    fn test_bun_edge_cases_not_matched() {
1861        let _guard = test_guard!();
1862        // Invalid commands should not match
1863        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        // bun x should not be intercepted (runs arbitrary packages like npx)
1876        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        // bun test should NOT be blocked by never-intercept
1893        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        // bun typecheck should NOT be blocked by never-intercept
1902        let result = classify_command("bun typecheck");
1903        assert!(!result.reason.contains("never-intercept"));
1904        assert!(result.is_compilation);
1905    }
1906
1907    // Bun CompilationKind serialization tests
1908
1909    #[test]
1910    fn test_bun_compilation_kind_serde() {
1911        let _guard = test_guard!();
1912        // BunTest serialization
1913        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        // BunTypecheck serialization
1920        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        // BunTest and BunTypecheck are different
1931        assert_ne!(CompilationKind::BunTest, CompilationKind::BunTypecheck);
1932        // BunTest is different from CargoTest (different ecosystems)
1933        assert_ne!(CompilationKind::BunTest, CompilationKind::CargoTest);
1934    }
1935
1936    #[test]
1937    fn test_wrapped_command_classification_repro() {
1938        let _guard = test_guard!();
1939        // This fails currently because classify_full expects command to start with "cargo"
1940        // Wrapper tools like "time", "sudo", "env" break this logic
1941        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    // =========================================================================
1969    // Bun E2E Edge Case Tests (from bead remote_compilation_helper-65m)
1970    // =========================================================================
1971
1972    #[test]
1973    fn test_bun_piped_commands_not_intercepted() {
1974        let _guard = test_guard!();
1975        // Piped commands should be rejected at Tier 1 (structure analysis)
1976        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        // Piped with output filtering
1984        let result = classify_command("bun test 2>&1 | tee output.log");
1985        assert!(!result.is_compilation);
1986        assert!(result.reason.contains("piped"));
1987
1988        // Piped typecheck
1989        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        // bunx (bun x) runs arbitrary packages - should NOT be intercepted
1998        // even for typecheck-like commands like tsc
1999        let result = classify_command("bun x tsc --noEmit");
2000        assert!(!result.is_compilation);
2001        assert!(result.reason.contains("bun x"));
2002
2003        // bunx with other tools
2004        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        // Output redirection should be rejected at Tier 1
2018        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        // Backgrounded commands should be rejected at Tier 1
2030        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        // Multi-command strings should be rejected
2039        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        // Subshell capture should be rejected at Tier 1
2058        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        // Wrapped bun commands should still be classified
2067        // (requires normalize_command to handle wrappers)
2068        let result = classify_command("time bun test");
2069        // Currently may or may not work depending on normalize_command
2070        // This test documents current behavior
2071        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    // =========================================================================
2118    // Comprehensive cargo test variant tests (bead remote_compilation_helper-xcvl)
2119    // =========================================================================
2120
2121    #[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        // cargo t is an alias for cargo test
2178        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    // =========================================================================
2305    // cargo-nextest tests (bead remote_compilation_helper-c7ky)
2306    // =========================================================================
2307
2308    #[test]
2309    fn test_nextest_keyword_detected() {
2310        let _guard = test_guard!();
2311        // Verify "nextest" triggers keyword detection (Tier 2 passes)
2312        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        // Basic command
2320        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        // With release flag
2333        let result = classify_command("cargo nextest run --release");
2334        assert!(result.is_compilation);
2335        assert_eq!(result.kind, Some(CompilationKind::CargoNextest));
2336
2337        // With profile flag
2338        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        // With workspace flag
2343        let result = classify_command("cargo nextest run --workspace");
2344        assert!(result.is_compilation);
2345        assert_eq!(result.kind, Some(CompilationKind::CargoNextest));
2346
2347        // With package filter
2348        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        // With test filter
2353        let result = classify_command("cargo nextest run test_classification");
2354        assert!(result.is_compilation);
2355        assert_eq!(result.kind, Some(CompilationKind::CargoNextest));
2356
2357        // With multiple flags
2358        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        // cargo nextest r is an alias for cargo nextest run
2367        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        // cargo nextest list only shows tests, doesn't run them
2379        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        // cargo nextest archive creates archives
2388        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        // cargo nextest show displays config info
2397        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        // bare "cargo nextest" without subcommand
2406        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        // Wrapped nextest commands should still be classified
2415        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        // CargoNextest serialization
2434        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        // CargoNextest and CargoTest are different
2445        assert_ne!(CompilationKind::CargoNextest, CompilationKind::CargoTest);
2446
2447        // Both should be classified but as different kinds
2448        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        // Piped commands should be rejected at Tier 1
2460        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        // Redirected commands should be rejected at Tier 1
2469        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        // Backgrounded commands should be rejected at Tier 1
2478        let result = classify_command("cargo nextest run &");
2479        assert!(!result.is_compilation);
2480        assert!(result.reason.contains("background"));
2481    }
2482
2483    // =========================================================================
2484    // cargo bench tests (bead remote_compilation_helper-8o52)
2485    // =========================================================================
2486
2487    #[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        // Benchmarks with name filter
2500        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        // With release flag (benchmarks typically use release)
2509        let result = classify_command("cargo bench --release");
2510        assert!(result.is_compilation);
2511        assert_eq!(result.kind, Some(CompilationKind::CargoBench));
2512
2513        // With package flag
2514        let result = classify_command("cargo bench -p rch-common");
2515        assert!(result.is_compilation);
2516        assert_eq!(result.kind, Some(CompilationKind::CargoBench));
2517
2518        // With features
2519        let result = classify_command("cargo bench --features benchmarks");
2520        assert!(result.is_compilation);
2521        assert_eq!(result.kind, Some(CompilationKind::CargoBench));
2522
2523        // With target filter
2524        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        // Wrapped with time
2533        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        // With env var
2541        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        // CargoBench serialization
2550        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        // CargoBench and CargoTest are different
2561        assert_ne!(CompilationKind::CargoBench, CompilationKind::CargoTest);
2562
2563        // Both should be classified but as different kinds
2564        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        // Piped commands should be rejected at Tier 1
2576        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        // Case: /usr/bin/time cargo build
2586
2587        // Current behavior: strips path -> "time cargo build", then returns.
2588
2589        // Desired behavior: strips path -> "time cargo build", then strips wrapper -> "cargo build".
2590
2591        // This test asserts the CURRENT BROKEN behavior.
2592
2593        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    // ==========================================================================
2603    // Command Base Tests (bd-785w)
2604    // ==========================================================================
2605
2606    #[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    // ==========================================================================
2645    // Proptest: Command Classification with Random Inputs (bd-2kdm)
2646    // ==========================================================================
2647
2648    mod proptest_classification {
2649        use super::*;
2650        use proptest::prelude::*;
2651
2652        // Strategy for arbitrary strings (pure random)
2653        fn arbitrary_string() -> impl Strategy<Value = String> {
2654            prop::string::string_regex(".{0,500}").unwrap()
2655        }
2656
2657        // Strategy for command-like strings with random suffixes
2658        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        // Strategy for known compilation commands with random flags
2668        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        // Strategy for strings with unicode and control characters
2707        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        // Strategy for shell-like commands with special characters
2715        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        // Strategy for wrapper-prefixed commands
2739        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            // Configure proptest for high-volume testing
2762            #![proptest_config(ProptestConfig::with_cases(1000))]
2763
2764            // Test 1: Arbitrary strings never cause panics
2765            #[test]
2766            fn test_classify_arbitrary_no_panic(s in arbitrary_string()) {
2767                // Just call classify_command - if it panics, proptest will catch it
2768                let _ = classify_command(&s);
2769            }
2770
2771            // Test 2: Command-like strings never cause panics
2772            #[test]
2773            fn test_classify_command_like_no_panic(s in command_like_string()) {
2774                let _ = classify_command(&s);
2775            }
2776
2777            // Test 3: Known commands with flags produce valid classification
2778            #[test]
2779            fn test_classify_known_commands_valid(s in known_command_with_flags()) {
2780                let result = classify_command(&s);
2781                // Confidence should be in valid range
2782                prop_assert!(result.confidence >= 0.0 && result.confidence <= 1.0,
2783                    "Confidence {} out of range for command: {}", result.confidence, s);
2784                // If is_compilation, must have a kind
2785                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 4: Unicode and control characters never panic
2792            #[test]
2793            fn test_classify_unicode_no_panic(s in unicode_and_control_chars()) {
2794                let _ = classify_command(&s);
2795            }
2796
2797            // Test 5: Shell special commands are handled
2798            #[test]
2799            fn test_classify_shell_special(s in shell_special_commands()) {
2800                let result = classify_command(&s);
2801                // Commands with shell operators should typically NOT be intercepted
2802                // (pipes, redirects, backgrounding, chaining)
2803                if s.contains(" | ") || s.contains(" > ") || s.contains(" < ") || s.contains(" & ") {
2804                    // May or may not be classified depending on quote handling
2805                    let _ = result; // Just ensure no panic
2806                }
2807            }
2808
2809            // Test 6: Wrapped commands are handled
2810            #[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 7: Classification is deterministic
2817            #[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 8: Empty-ish strings handled correctly
2826            #[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 9: Very long commands don't panic
2837            #[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 10: Null bytes and special sequences
2848            #[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                // Non-UTF8 sequences can't be tested with &str
2854            }
2855        }
2856
2857        // Additional targeted proptest tests
2858
2859        proptest! {
2860            #![proptest_config(ProptestConfig::with_cases(500))]
2861
2862            // Test: Cargo subcommands with arbitrary suffixes
2863            #[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                // Ensure valid result structure
2871                prop_assert!(result.confidence >= 0.0 && result.confidence <= 1.0);
2872                // fmt and clean should NEVER be classified as compilation
2873                if subcommand == "fmt" || subcommand == "clean" {
2874                    prop_assert!(!result.is_compilation,
2875                        "cargo {} should not be compilation: {}", subcommand, cmd);
2876                }
2877            }
2878
2879            // Test: Bun commands robustness
2880            #[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                // install, add, remove, run, build, dev should NOT be compilation
2889                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: GCC/Clang commands with file patterns
2896            #[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: Commands with quoted strings preserve correctness
2908            #[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                // Single quotes
2914                let cmd_single = format!("{} '{}'", base, quoted_content);
2915                let _ = classify_command(&cmd_single);
2916
2917                // Double quotes
2918                let cmd_double = format!("{} \"{}\"", base, quoted_content);
2919                let _ = classify_command(&cmd_double);
2920            }
2921        }
2922
2923        // Regression test: ensure specific problematic inputs are handled
2924        #[test]
2925        fn test_known_edge_cases() {
2926            let _guard = test_guard!();
2927            // These are edge cases that might have caused issues
2928            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 that detailed classification matches simple classification
2974        #[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        // =================== split_shell_commands tests ===================
2993
2994        #[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            // Single pipe is NOT a command separator
3046            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            // Escaped quotes don't toggle state
3074            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            // Trailing && with nothing after should yield just the first segment
3105            assert_eq!(split_shell_commands("cargo build &&"), vec!["cargo build"]);
3106        }
3107
3108        // =================== fail-open safeguard tests (bd-16t3) ===================
3109
3110        #[test]
3111        fn test_split_unclosed_single_quote() {
3112            let _guard = test_guard!();
3113            // Unclosed quote: everything stays "inside quotes", no split found
3114            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            // Embedded null bytes should not cause panic
3136            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            // 20KB string should still work (split_shell_commands has no length limit)
3152            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            // Commands >10KB skip multi-command splitting in classify_command
3161            let long_cmd = format!("cargo build && echo {}", "x".repeat(11_000));
3162            let result = classify_command(&long_cmd);
3163            // Falls through to single-command classification which rejects at check_structure
3164            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            // Just operators with no commands
3175            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            // Middle empty segment is dropped, yielding 2 segments
3187            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            // All sub-commands are non-compilation
3194            let result = classify_command("echo hello && ls -la || pwd");
3195            assert!(!result.is_compilation);
3196        }
3197
3198        // =================== comprehensive multi-command tests (bd-1q0e) ===================
3199
3200        // --- Split edge cases ---
3201
3202        #[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            // Single pipe within segment, && between segments
3233            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            // Leading && with nothing before
3243            assert_eq!(split_shell_commands("&& cargo build"), vec!["cargo build"]);
3244        }
3245
3246        // --- Compound command classification tests ---
3247        // Compound && commands with compilation suffix are now ACCEPTED
3248        // This enables patterns like "cd /path && cargo build"
3249
3250        #[test]
3251        fn test_classify_cargo_fmt_and_build() {
3252            let _guard = test_guard!();
3253            // Last segment is "cargo build" (compilation) → accepted
3254            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            // Last segment is "make -j8" (compilation) → accepted
3266            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            // Last segment is "cargo build --release" (compilation) → accepted
3277            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            // Last segment is "cmake --build build" (compilation) → accepted
3289            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            // Last segment is "cargo test" (compilation) → accepted
3301            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            // Semicolon chains are NOT supported (only && is supported)
3312            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        // --- Classification integration: should classify as NON-COMPILATION ---
3320
3321        #[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        // --- Performance test ---
3343
3344        #[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            // Each classify_command call should be well under 5ms
3354            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        // --- Pipe preservation with split ---
3366
3367        #[test]
3368        fn test_classify_pipe_preserved_in_segment() {
3369            let _guard = test_guard!();
3370            // Pipe within a segment is NOT a split point; segment stays whole
3371            // and gets rejected by check_structure (piped command)
3372            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            // Pipe within first segment, && separates from second
3380            let result = classify_command("make 2>&1 | grep error && cargo build");
3381            // Should be rejected due to chaining (&&) AND piping (|)
3382            assert!(
3383                !result.is_compilation,
3384                "chained commands should be rejected"
3385            );
3386            // The reason will likely be "chained command (&&)" because check_structure checks separators first or pipes first?
3387            // Let's check check_structure impl: pipes are checked AFTER backgrounding, separators are after pipes?
3388            // Actually, check_structure checks pipes, then redirects, then chaining.
3389            // So "piped command" might be the reason. Either is fine.
3390            assert!(result.reason.contains("piped") || result.reason.contains("chained"));
3391        }
3392    }
3393
3394    // =========================================================================
3395    // WS2.4: Zero-allocation reject path tests (bd-3mog)
3396    //
3397    // Verify that the Cow<'static, str> conversion in Classification and
3398    // ClassificationTier produces Cow::Borrowed on the reject path (zero heap
3399    // allocations) and that serde roundtrips work correctly.
3400    // =========================================================================
3401
3402    /// Helper: assert that a Cow is the Borrowed variant (no heap allocation).
3403    /// We need the actual &Cow to distinguish Borrowed from Owned.
3404    #[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    // --- 1. Classification correctness after Cow conversion ---
3414
3415    #[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    // --- 2. Zero-allocation verification on reject path ---
3480
3481    #[test]
3482    fn test_cow_borrowed_on_tier0_reject() {
3483        let _guard = test_guard!();
3484        // Empty command -> Tier 0 instant reject
3485        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        // Piped command -> Tier 1 structure analysis reject
3493        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        // Backgrounded command
3498        let result = classify_command("cargo build &");
3499        assert!(!result.is_compilation);
3500        assert_cow_borrowed(&result.reason, "Tier 1 backgrounded command reason");
3501
3502        // Output redirected
3503        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        // Commands with no compilation keyword -> Tier 2 keyword filter reject
3512        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        // Never-intercept commands now use static strings for performance (no format! allocation)
3535        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            // Tier 3 now uses static string for performance -> Cow::Borrowed
3545            assert_cow_borrowed(&result.reason, &format!("Tier 3 reject for '{cmd}'"));
3546        }
3547    }
3548
3549    // --- 3. ClassificationDetails tier Cow verification ---
3550
3551    #[test]
3552    fn test_detailed_tiers_use_borrowed_names() {
3553        let _guard = test_guard!();
3554        // classify_command_detailed produces ClassificationTier entries
3555        // All tier names should be Cow::Borrowed (static constants)
3556        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        // Non-compilation commands should have Cow::Borrowed reasons on all tiers
3566        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        // Even for compilation commands, tier NAMES should always be Cow::Borrowed
3580        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    // --- 4. Serde roundtrip tests for Cow fields ---
3591
3592    #[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        // Note: after deserialization, Cow will be Owned (serde deserializes into String)
3606        // This is correct behavior — only the pre-serialization path needs to be allocation-free
3607    }
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    // --- 5. Cow display and comparison tests ---
3668
3669    #[test]
3670    fn test_cow_reason_display() {
3671        let _guard = test_guard!();
3672        let result = classify_command("ls");
3673        // Cow<str> should Display/Debug correctly
3674        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        // Cow::Borrowed and Cow::Owned with same content should be equal
3684        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        // Classification reasons should be comparable
3689        let r1 = classify_command("ls");
3690        let r2 = classify_command("pwd");
3691        // Both should be "no compilation keyword" (Tier 2 reject)
3692        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        // Standard single space
3706        let result = classify_command("bun test");
3707        assert!(result.is_compilation, "bun test failed");
3708        assert_eq!(result.kind, Some(CompilationKind::BunTest));
3709
3710        // Multiple spaces
3711        let result = classify_command("bun  test");
3712        assert!(result.is_compilation, "bun  test failed");
3713        assert_eq!(result.kind, Some(CompilationKind::BunTest));
3714
3715        // Tab separation
3716        let result = classify_command("bun\ttest");
3717        assert!(result.is_compilation, "bun\\ttest failed");
3718        assert_eq!(result.kind, Some(CompilationKind::BunTest));
3719
3720        // Typecheck with extra spaces
3721        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        // Watch with extra spaces
3731        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        // Watch with short flag and tabs
3739        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        // bun x with extra spaces
3752        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        // Multiple spaces
3768        assert_eq!(normalize_command("sudo  cargo"), "cargo");
3769
3770        // Tabs
3771        assert_eq!(normalize_command("sudo\tcargo"), "cargo");
3772
3773        // Mixed wrappers with weird spacing
3774        assert_eq!(normalize_command("time\tsudo  cargo"), "cargo");
3775
3776        // Prefix matching safety
3777        assert_eq!(normalize_command("sudocargo"), "sudocargo");
3778
3779        // Bare wrapper (should become empty)
3780        assert_eq!(normalize_command("sudo"), "");
3781    }
3782
3783    #[test]
3784    fn test_env_var_whitespace() {
3785        let _guard = test_guard!();
3786
3787        // env with multiple spaces before VAR
3788        assert_eq!(normalize_command("env  RUST_BACKTRACE=1 cargo"), "cargo");
3789
3790        // env with tabs
3791        assert_eq!(normalize_command("env\tRUST_BACKTRACE=1\tcargo"), "cargo");
3792    }
3793}
3794
3795// ============================================================================
3796// REGRESSION SUITE: Command Classification (bd-vvmd.2.9)
3797//
3798// Table-driven regression tests protecting non-compilation UX. Every entry
3799// documents expected behavior with rationale so accidental interception of
3800// local-only commands is caught immediately.
3801// ============================================================================
3802
3803#[cfg(test)]
3804mod regression_classification {
3805    use super::*;
3806    use crate::test_guard;
3807
3808    /// A single regression test case.
3809    struct Case {
3810        cmd: &'static str,
3811        expect_compilation: bool,
3812        expected_kind: Option<CompilationKind>,
3813        /// Substring that MUST appear in the classification reason.
3814        reason_contains: &'static str,
3815        /// Minimum confidence (only checked when expect_compilation is true).
3816        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    // ------------------------------------------------------------------
3854    // 1. Cargo: Positive compilation commands
3855    // ------------------------------------------------------------------
3856
3857    #[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            // Toolchain override variants
4079            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    // ------------------------------------------------------------------
4105    // 2. Cargo: Negative (NEVER intercept — must stay local)
4106    // ------------------------------------------------------------------
4107
4108    #[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            // cargo nextest non-run subcommands
4225            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            // Bare cargo / bare cargo nextest
4247            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            // Non-interceptable subcommands
4262            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    // ------------------------------------------------------------------
4295    // 3. Bun: Positive compilation (test, typecheck)
4296    // ------------------------------------------------------------------
4297
4298    #[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    // ------------------------------------------------------------------
4349    // 4. Bun: Negative (package management, execution, interactive)
4350    // ------------------------------------------------------------------
4351
4352    #[test]
4353    fn regression_bun_never_intercept() {
4354        let _guard = test_guard!();
4355        let cases = [
4356            // Package management — modifies local node_modules
4357            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            // Execution — binds local ports or runs scripts
4421            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            // Package runner — arbitrary execution like npx
4450            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            // Interactive watch modes — must run locally for interactivity
4465            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            // Version/help
4494            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    // ------------------------------------------------------------------
4534    // 5. C/C++ compilers: Positive compilation
4535    // ------------------------------------------------------------------
4536
4537    #[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    // ------------------------------------------------------------------
4609    // 6. C/C++ compilers: Version checks (NEVER intercept)
4610    // ------------------------------------------------------------------
4611
4612    #[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    // ------------------------------------------------------------------
4684    // 7. Build systems: Positive compilation
4685    // ------------------------------------------------------------------
4686
4687    #[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    // ------------------------------------------------------------------
4752    // 8. Build systems: Negative (clean, install, version)
4753    // ------------------------------------------------------------------
4754
4755    #[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            // meson without compile subcommand
4795            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            // cmake without --build
4810            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    // ------------------------------------------------------------------
4829    // 9. Rustc: Direct invocation
4830    // ------------------------------------------------------------------
4831
4832    #[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    // ------------------------------------------------------------------
4862    // 10. Shell structure exclusions (pipes, redirects, bg, chains)
4863    // ------------------------------------------------------------------
4864
4865    #[test]
4866    fn regression_shell_structure_exclusions() {
4867        let _guard = test_guard!();
4868        let cases = [
4869            // Pipes
4870            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            // Background
4885            Case {
4886                cmd: "cargo build &",
4887                expect_compilation: false,
4888                expected_kind: None,
4889                reason_contains: "background",
4890                min_confidence: 0.0,
4891            },
4892            // Output redirect
4893            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            // Input redirect
4908            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            // Subshell
4916            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            // Process substitution
4938            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            // Semicolon chains
4946            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            // || chains
4954            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            // Newline injection (security)
4962            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    // ------------------------------------------------------------------
4981    // 11. fd-to-fd redirects are SAFE and should NOT block interception
4982    // ------------------------------------------------------------------
4983
4984    #[test]
4985    fn regression_fd_redirect_safe() {
4986        let _guard = test_guard!();
4987        // Compound: "cd /path && cargo build 2>&1" should detect cargo build
4988        // via compound command handling (depth-0 splits on &&, depth-1 checks segment)
4989        // Simple fd-to-fd redirect like "cargo build 2>&1" is handled by check_structure
4990        // which explicitly skips fd-to-fd redirects. So "cargo build 2>&1" should be
4991        // classified as compilation.
4992        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    // ------------------------------------------------------------------
5002    // 12. Compound commands: && chains with compilation suffix
5003    // ------------------------------------------------------------------
5004
5005    #[test]
5006    fn regression_compound_commands() {
5007        let _guard = test_guard!();
5008        // cd && cargo build should be detected as compilation
5009        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        // Non-compilation suffix should not match
5031        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    // ------------------------------------------------------------------
5039    // 13. Wrapper normalization: env, sudo, time, etc.
5040    // ------------------------------------------------------------------
5041
5042    #[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    // ------------------------------------------------------------------
5100    // 14. Non-compilation commands (must never trigger)
5101    // ------------------------------------------------------------------
5102
5103    #[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    // ------------------------------------------------------------------
5238    // 15. ClassificationDetails: structured log field verification
5239    // ------------------------------------------------------------------
5240
5241    #[test]
5242    fn regression_classification_details_fields() {
5243        let _guard = test_guard!();
5244
5245        // Compilation: verify all tiers pass and final has kind + confidence
5246        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        // Should have 5 tiers: 0-4
5255        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        // Tier 4 should also pass (compilation detected)
5260        assert_eq!(details.tiers[4].decision, TierDecision::Pass);
5261        assert_eq!(details.tiers[4].tier, 4);
5262
5263        // Non-compilation: should reject early at Tier 2 (keyword filter)
5264        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        // Never-intercept: should reject at Tier 3
5273        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        // Find tier 3
5280        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        // Piped command: should reject at Tier 1 (structure analysis)
5289        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        // Empty command: should reject at Tier 0
5296        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    // ------------------------------------------------------------------
5303    // 16. Decision path completeness: every positive case has reason code
5304    // ------------------------------------------------------------------
5305
5306    #[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    // ------------------------------------------------------------------
5349    // 17. Decision path completeness: every negative case has reason code
5350    // ------------------------------------------------------------------
5351
5352    #[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    // ------------------------------------------------------------------
5400    // 18. Representative real-world agent workflow commands
5401    // ------------------------------------------------------------------
5402
5403    #[test]
5404    fn regression_real_world_agent_workflows() {
5405        let _guard = test_guard!();
5406
5407        // Typical Claude Code agent workflow: navigation + build
5408        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        // Agent running tests after code change
5417        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        // Agent running clippy check
5422        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        // Agent checking format (should NOT intercept — modifies source)
5427        let fmt = classify_command("cargo fmt --check");
5428        assert!(!fmt.is_compilation);
5429
5430        // Agent reading Cargo.toml (should NOT intercept)
5431        let cat = classify_command("cat Cargo.toml");
5432        assert!(!cat.is_compilation);
5433
5434        // Agent checking git status (should NOT intercept)
5435        let gs = classify_command("git status");
5436        assert!(!gs.is_compilation);
5437
5438        // Agent installing a tool (should NOT intercept)
5439        let install = classify_command("cargo install cargo-nextest");
5440        assert!(!install.is_compilation);
5441
5442        // Agent running specific package tests
5443        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        // Agent running with environment variable
5448        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        // Agent running nextest
5453        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    // ------------------------------------------------------------------
5459    // 19. Boundary: commands that contain keywords but are NOT compilation
5460    // ------------------------------------------------------------------
5461
5462    #[test]
5463    fn regression_keyword_in_non_compilation_context() {
5464        let _guard = test_guard!();
5465        // "cargo" appears as substring in path or argument, not as command
5466        let result = classify_command("echo cargo build");
5467        // "echo" has no compilation keyword... wait, "cargo" IS a keyword
5468        // but the command starts with "echo", which isn't a compilation command.
5469        // After normalization, it should still start with "echo".
5470        assert!(
5471            !result.is_compilation,
5472            "echo cargo should not compile, got: {:?}",
5473            result.reason
5474        );
5475
5476        // grep for "make" in a log file - keyword present but not a build command
5477        let result = classify_command("grep make Makefile");
5478        // "make" is a keyword so passes Tier 2, but starts with "grep", not "make"
5479        assert!(
5480            !result.is_compilation,
5481            "grep make should not compile, got: {:?}",
5482            result.reason
5483        );
5484
5485        // cat a file with "gcc" in its name
5486        let result = classify_command("cat gcc_output.log");
5487        // "gcc" keyword passes Tier 2, but the command is "cat"
5488        assert!(
5489            !result.is_compilation,
5490            "cat gcc_output should not compile, got: {:?}",
5491            result.reason
5492        );
5493    }
5494
5495    // ------------------------------------------------------------------
5496    // 20. CompilationKind properties: is_test_command, command_base
5497    // ------------------------------------------------------------------
5498
5499    #[test]
5500    fn regression_compilation_kind_properties() {
5501        let _guard = test_guard!();
5502
5503        // Test commands
5504        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        // Non-test commands
5510        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        // command_base
5526        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}