1pub mod biome;
22pub mod builtin_filters;
23pub mod bun;
24pub mod cargo;
25pub mod eslint;
26pub mod generic;
27pub mod git;
28pub mod go;
29pub mod mypy;
30pub mod next;
31pub mod npm;
32pub mod playwright;
33pub mod pnpm;
34pub mod prettier;
35pub mod pytest;
36pub mod ruff;
37pub mod toml_filter;
38pub mod trust;
39pub mod tsc;
40pub mod vitest;
41
42use crate::context::AppContext;
43use crate::harness::Harness;
44use biome::BiomeCompressor;
45use bun::BunCompressor;
46use cargo::CargoCompressor;
47use eslint::EslintCompressor;
48use generic::{strip_ansi, GenericCompressor};
49use git::GitCompressor;
50use go::{GoCompressor, GolangciLintCompressor};
51use mypy::MypyCompressor;
52use next::NextCompressor;
53use npm::NpmCompressor;
54use playwright::PlaywrightCompressor;
55use pnpm::PnpmCompressor;
56use prettier::PrettierCompressor;
57use pytest::PytestCompressor;
58use ruff::RuffCompressor;
59use std::fs;
60use std::path::{Path, PathBuf};
61use std::sync::{Arc, RwLock};
62use toml_filter::{apply_filter, FilterRegistry};
63use tsc::TscCompressor;
64use vitest::VitestCompressor;
65
66pub type SharedFilterRegistry = Arc<RwLock<FilterRegistry>>;
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum Specificity {
90 Specific,
91 PackageManager,
92}
93
94pub trait Compressor: Send + Sync {
97 fn matches(&self, command: &str) -> bool;
100
101 fn compress(&self, command: &str, output: &str) -> String;
103
104 fn specificity(&self) -> Specificity {
105 Specificity::Specific
106 }
107
108 fn matches_output(&self, _output: &str) -> bool {
113 false
114 }
115
116 fn compress_output_match(&self, output: &str) -> String {
119 self.compress("", output)
120 }
121}
122
123pub fn compress(command: &str, output: String, ctx: &AppContext) -> String {
129 if !ctx.config().experimental_bash_compress {
130 return output;
131 }
132 let registry_handle = ctx.shared_filter_registry();
133 let guard = match registry_handle.read() {
134 Ok(g) => g,
135 Err(poisoned) => poisoned.into_inner(),
136 };
137 compress_with_registry(command, &output, &guard)
138}
139
140pub fn compress_with_registry(command: &str, output: &str, registry: &FilterRegistry) -> String {
146 let stripped_for_generic = strip_ansi(output);
147
148 let normalized = normalize_command_for_dispatch(command);
154 let dispatch_cmd = normalized.as_deref().unwrap_or(command);
155
156 let compressors: [&dyn Compressor; 17] = [
157 &GitCompressor,
158 &CargoCompressor,
159 &TscCompressor,
160 &NpmCompressor,
161 &BunCompressor,
162 &PnpmCompressor,
163 &PytestCompressor,
164 &EslintCompressor,
165 &VitestCompressor,
166 &BiomeCompressor,
167 &PrettierCompressor,
168 &RuffCompressor,
169 &MypyCompressor,
170 &GoCompressor,
171 &GolangciLintCompressor,
172 &PlaywrightCompressor,
173 &NextCompressor,
174 ];
175
176 for compressor in compressors
178 .iter()
179 .filter(|c| c.specificity() == Specificity::Specific)
180 {
181 if compressor.matches(dispatch_cmd) {
182 return compressor.compress(dispatch_cmd, &stripped_for_generic);
183 }
184 }
185
186 for specificity in [Specificity::Specific, Specificity::PackageManager] {
192 for compressor in compressors
193 .iter()
194 .filter(|c| c.specificity() == specificity)
195 {
196 if compressor.matches_output(&stripped_for_generic) {
197 return compressor.compress_output_match(&stripped_for_generic);
198 }
199 }
200 }
201
202 for compressor in compressors
204 .iter()
205 .filter(|c| c.specificity() == Specificity::PackageManager)
206 {
207 if compressor.matches(dispatch_cmd) {
208 return compressor.compress(dispatch_cmd, &stripped_for_generic);
209 }
210 }
211
212 if let Some(filter) = registry.lookup(dispatch_cmd) {
215 return apply_filter(filter, output);
216 }
217
218 GenericCompressor.compress(command, &stripped_for_generic)
220}
221
222pub fn build_registry_for_context(ctx: &AppContext) -> FilterRegistry {
231 let harness = ctx.harness.borrow().unwrap_or(Harness::Opencode);
232 let config = ctx.config();
233 let storage_dir = config.storage_dir.clone();
234 let project_root = config.project_root.clone();
235 drop(config);
236
237 let user_dir = storage_dir.as_ref().map(|dir| {
238 repair_legacy_user_filter_dir(dir, harness);
239 user_filter_dir(dir, harness)
240 });
241 let project_dir = match (project_root.as_ref(), storage_dir.as_ref()) {
242 (Some(root), Some(storage)) => {
243 if trust::is_project_trusted(Some(storage), root) {
244 Some(root.join(".aft").join("filters"))
245 } else {
246 None
247 }
248 }
249 _ => None,
250 };
251
252 toml_filter::build_registry(
253 builtin_filters::ALL,
254 user_dir.as_deref(),
255 project_dir.as_deref(),
256 )
257}
258
259pub fn normalize_command_for_dispatch(command: &str) -> Option<String> {
283 let trimmed = command.trim_start();
284 if trimmed.is_empty() {
285 return None;
286 }
287
288 let (open_paren, after_paren) = if let Some(rest) = trimmed.strip_prefix('(') {
290 (true, rest.trim_start())
291 } else {
292 (false, trimmed)
293 };
294
295 let mut current = after_paren.to_string();
296 let mut changed = open_paren;
297
298 loop {
300 if let Some(stripped) = strip_leading_assignment_prefix(¤t) {
304 current = stripped;
305 changed = true;
306 continue;
307 }
308
309 let head: String = current.split_whitespace().next().unwrap_or("").to_string();
310
311 if head == "cd" {
313 if let Some(stripped) = strip_cd_prefix(¤t) {
316 current = stripped;
317 changed = true;
318 continue;
319 }
320 }
321
322 if head == "env" {
324 if let Some(stripped) = strip_env_prefix(¤t) {
325 current = stripped;
326 changed = true;
327 continue;
328 }
329 }
330
331 if head == "timeout" {
333 if let Some(stripped) = strip_timeout_prefix(¤t) {
334 current = stripped;
335 changed = true;
336 continue;
337 }
338 }
339
340 if head == "nohup" {
342 if let Some(rest) = current.strip_prefix("nohup").and_then(|s| {
343 let trimmed = s.trim_start();
344 if trimmed.is_empty() {
345 None
346 } else {
347 Some(trimmed.to_string())
348 }
349 }) {
350 current = rest;
351 changed = true;
352 continue;
353 }
354 }
355
356 break;
357 }
358
359 if changed {
360 Some(current)
361 } else {
362 None
363 }
364}
365
366fn strip_cd_prefix(command: &str) -> Option<String> {
367 let bytes = command.as_bytes();
369 let mut in_single = false;
370 let mut in_double = false;
371 let mut i = 0;
372 while i < bytes.len() {
373 let ch = bytes[i] as char;
374 if !in_double && ch == '\'' {
375 in_single = !in_single;
376 } else if !in_single && ch == '"' {
377 in_double = !in_double;
378 } else if !in_single && !in_double {
379 if ch == '&' && i + 1 < bytes.len() && bytes[i + 1] as char == '&' {
380 let rest = command[i + 2..].trim_start();
381 if rest.is_empty() {
382 return None;
383 }
384 return Some(rest.to_string());
385 }
386 if ch == ';' {
387 let rest = command[i + 1..].trim_start();
388 if rest.is_empty() {
389 return None;
390 }
391 return Some(rest.to_string());
392 }
393 }
394 i += 1;
395 }
396 None
397}
398
399fn strip_env_prefix(command: &str) -> Option<String> {
400 let rest = command.strip_prefix("env")?.trim_start();
402 strip_leading_assignment_prefix(rest)
403}
404
405fn strip_leading_assignment_prefix(command: &str) -> Option<String> {
406 let mut index = 0usize;
407 let mut consumed_assignment = false;
408
409 loop {
410 index = skip_whitespace(command, index);
411 if index >= command.len() {
412 break;
413 }
414
415 let word_end = shell_word_end(command, index)?;
416 if word_end == index {
417 break;
418 }
419
420 let word = &command[index..word_end];
421 if !is_env_assignment(word) {
422 break;
423 }
424
425 consumed_assignment = true;
426 index = word_end;
427 }
428
429 if !consumed_assignment {
430 return None;
431 }
432
433 let after = command[index..].trim_start();
434 if after.is_empty() {
435 None
436 } else {
437 Some(after.to_string())
438 }
439}
440
441fn skip_whitespace(input: &str, mut index: usize) -> usize {
442 while index < input.len() {
443 let Some(ch) = input[index..].chars().next() else {
444 break;
445 };
446 if !ch.is_whitespace() {
447 break;
448 }
449 index += ch.len_utf8();
450 }
451 index
452}
453
454fn shell_word_end(command: &str, start: usize) -> Option<usize> {
455 let mut in_single = false;
456 let mut in_double = false;
457 let mut escaped = false;
458
459 for (offset, ch) in command[start..].char_indices() {
460 let index = start + offset;
461
462 if escaped {
463 escaped = false;
464 continue;
465 }
466
467 if ch == '\\' && !in_single {
468 escaped = true;
469 continue;
470 }
471
472 if ch == '\'' && !in_double {
473 in_single = !in_single;
474 continue;
475 }
476
477 if ch == '"' && !in_single {
478 in_double = !in_double;
479 continue;
480 }
481
482 if !in_single && !in_double && (ch.is_whitespace() || matches!(ch, ';' | '&' | '|')) {
483 return Some(index);
484 }
485 }
486
487 if in_single || in_double || escaped {
488 None
489 } else {
490 Some(command.len())
491 }
492}
493
494fn is_env_assignment(token: &str) -> bool {
495 if token.starts_with('-') {
496 return false;
497 }
498 let Some((name, _value)) = token.split_once('=') else {
499 return false;
500 };
501 let mut chars = name.chars();
502 let Some(first) = chars.next() else {
503 return false;
504 };
505 (first.is_ascii_alphabetic() || first == '_')
506 && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
507}
508
509fn strip_timeout_prefix(command: &str) -> Option<String> {
510 let rest = command.strip_prefix("timeout")?.trim_start();
511 let mut iter = rest.splitn(2, char::is_whitespace);
513 let duration = iter.next()?;
514 let after = iter.next()?.trim_start();
515 if after.is_empty() || !looks_like_duration(duration) {
516 return None;
517 }
518 Some(after.to_string())
519}
520
521fn looks_like_duration(token: &str) -> bool {
522 if token.is_empty() {
523 return false;
524 }
525 let mut chars = token.chars().peekable();
526 let mut saw_digit = false;
527 while let Some(&ch) = chars.peek() {
528 if ch.is_ascii_digit() {
529 saw_digit = true;
530 chars.next();
531 } else {
532 break;
533 }
534 }
535 if !saw_digit {
536 return false;
537 }
538 match chars.next() {
539 None => true,
540 Some(unit) => matches!(unit, 's' | 'm' | 'h' | 'd') && chars.next().is_none(),
541 }
542}
543
544pub fn user_filter_dir(storage_dir: &Path, harness: Harness) -> PathBuf {
547 storage_dir.join(harness.as_str()).join("filters")
548}
549
550fn legacy_user_filter_dir(storage_dir: &Path) -> PathBuf {
551 storage_dir.join("filters")
552}
553
554pub(crate) fn repair_legacy_user_filter_dir(storage_dir: &Path, harness: Harness) {
558 let legacy_dir = legacy_user_filter_dir(storage_dir);
559 if !legacy_dir.exists() {
560 return;
561 }
562
563 let entries = match fs::read_dir(&legacy_dir) {
564 Ok(entries) => entries.filter_map(Result::ok).collect::<Vec<_>>(),
565 Err(_) => return,
566 };
567 if entries.is_empty() {
568 let _ = fs::remove_dir(&legacy_dir);
569 return;
570 }
571
572 let harness_dir = user_filter_dir(storage_dir, harness);
573 if fs::create_dir_all(&harness_dir).is_err() {
574 return;
575 }
576
577 for entry in entries {
578 let target = harness_dir.join(entry.file_name());
579 if target.exists() {
580 continue;
581 }
582 let _ = fs::rename(entry.path(), target);
583 }
584
585 if fs::read_dir(&legacy_dir)
586 .map(|mut entries| entries.next().is_none())
587 .unwrap_or(false)
588 {
589 let _ = fs::remove_dir(&legacy_dir);
590 }
591}
592
593pub fn project_filter_dir(project_root: &Path) -> PathBuf {
597 project_root.join(".aft").join("filters")
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603
604 #[test]
605 fn user_and_project_filter_dir_helpers() {
606 let storage = Path::new("/tmp/aft-storage");
607 assert_eq!(
608 user_filter_dir(storage, Harness::Opencode),
609 Path::new("/tmp/aft-storage/opencode/filters")
610 );
611
612 let project = Path::new("/repo");
613 assert_eq!(project_filter_dir(project), Path::new("/repo/.aft/filters"));
614 }
615
616 #[test]
617 fn repair_legacy_user_filter_dir_moves_root_filters_without_overwrite() {
618 let temp = tempfile::tempdir().unwrap();
619 let storage = temp.path();
620 fs::create_dir_all(storage.join("filters")).unwrap();
621 fs::create_dir_all(storage.join("opencode/filters")).unwrap();
622 fs::write(storage.join("filters/root-only.toml"), "root").unwrap();
623 fs::write(storage.join("filters/collides.toml"), "root").unwrap();
624 fs::write(storage.join("opencode/filters/collides.toml"), "harness").unwrap();
625
626 repair_legacy_user_filter_dir(storage, Harness::Opencode);
627
628 assert_eq!(
629 fs::read_to_string(storage.join("opencode/filters/root-only.toml")).unwrap(),
630 "root"
631 );
632 assert_eq!(
633 fs::read_to_string(storage.join("opencode/filters/collides.toml")).unwrap(),
634 "harness"
635 );
636 assert_eq!(
637 fs::read_to_string(storage.join("filters/collides.toml")).unwrap(),
638 "root"
639 );
640 assert!(!storage.join("filters/root-only.toml").exists());
641 }
642}
643
644#[cfg(test)]
645mod dispatch_specificity_tests {
646 use super::*;
647 use crate::compress::toml_filter::FilterRegistry;
648
649 fn empty_registry() -> FilterRegistry {
650 FilterRegistry::default()
651 }
652
653 fn dispatch(cmd: &str, output: &str) -> String {
658 compress_with_registry(cmd, output, &empty_registry())
659 }
660
661 #[test]
662 fn bun_run_vitest_routes_to_vitest_not_generic() {
663 let output = "Test Files 1 passed (1)\n Tests 4 passed (4)\n Start at 10:00:00\n Duration 120ms\n";
668 let compressed = dispatch("bun run vitest", output);
669 assert!(compressed.contains("Tests") || compressed.contains("Test Files"));
671 }
672
673 #[test]
674 fn npm_test_routes_to_vitest_when_output_is_vitest_shaped() {
675 let output = "RERUN src/foo.test.ts x1\nFAIL src/foo.test.ts\nTest Files 1 failed (1)\nDuration 120ms\n";
678 let compressed = dispatch("npm test", output);
679 assert!(compressed.contains("FAIL src/foo.test.ts"));
680 assert!(compressed.contains("Duration 120ms"));
681 assert!(!compressed.contains("RERUN"));
682 }
683
684 #[test]
685 fn bun_run_vitest_token_match_wins_over_bun_head_match() {
686 let output = "PASS src/a.test.ts (1)\n PASS src/b.test.ts (1)\nTest Files 2 passed (2)\n Tests 4 passed (4)\n";
689 let compressed = dispatch("bun run vitest run", output);
690 assert!(compressed.contains("Test Files") || compressed.contains("PASS"));
692 }
693
694 #[test]
695 fn bunx_jest_routes_to_vitest_module() {
696 let output = "PASS src/foo.test.js (1.2s)\nTest Suites: 1 passed, 1 total\nTests: 3 passed, 3 total\n";
697 let compressed = dispatch("bunx jest --json", output);
698 assert!(compressed.contains("Tests:") && compressed.contains("Test Suites"));
699 }
700
701 #[test]
702 fn pnpm_run_vitest_routes_to_vitest() {
703 let output = "Test Files 1 passed (1)\n Tests 10 passed (10)\n";
704 let compressed = dispatch("pnpm run vitest", output);
705 assert!(compressed.contains("Tests") || compressed.contains("Test Files"));
706 }
707
708 #[test]
709 fn npx_eslint_routes_to_eslint_not_generic() {
710 let output = "\n/tmp/a.js\n 1:1 error 'foo' is defined but never used no-unused-vars\n\n✖ 1 problem (1 error, 0 warnings)\n";
711 let compressed = dispatch("npx eslint .", output);
712 assert!(compressed.contains("no-unused-vars") || compressed.contains("✖"));
714 }
715
716 #[test]
717 fn npm_run_lint_without_linter_output_shape_falls_back() {
718 let output = "> my-project@1.0.0 lint\n> eslint .\n\nAll good.\n";
721 let compressed = dispatch("npm run lint", output);
722 assert!(compressed.contains("All good."));
723 }
724
725 #[test]
726 fn bun_test_still_routes_to_bun_test_compressor() {
727 let output = "bun test v1.3.14\n\nsrc/foo.test.ts:\n(pass) my test [0.5ms]\n\n 1 pass\n 0 fail\n 1 expect() calls\nRan 1 tests across 1 files. [1.00ms]\n";
735 let compressed = dispatch("bun test", output);
736 assert!(compressed.contains("(pass)") || compressed.contains("1 pass"));
737 }
738
739 #[test]
740 fn bunx_vitest_routes_to_vitest() {
741 let output = "Test Files 1 passed (1)\n Tests 3 passed (3)\n";
742 let compressed = dispatch("bunx vitest run", output);
743 assert!(compressed.contains("Tests") || compressed.contains("Test Files"));
744 }
745
746 #[test]
747 fn cargo_test_still_routes_to_cargo() {
748 let output = "running 5 tests\ntest foo ... ok\ntest bar ... FAILED\n\nfailures:\n\ntest result: FAILED. 4 passed; 1 failed\n";
751 let compressed = dispatch("cargo test", output);
752 assert!(compressed.contains("failed") || compressed.contains("FAILED"));
754 }
755
756 #[test]
757 fn git_status_still_routes_to_git() {
758 let output =
760 "On branch main\nYour branch is up to date.\n\nnothing to commit, working tree clean\n";
761 let compressed = dispatch("git status", output);
762 assert!(compressed.contains("branch") || compressed.contains("clean"));
763 }
764
765 #[test]
766 fn pnpm_install_still_routes_to_pnpm() {
767 let output = "Progress: resolved 100, downloaded 50, added 50\nAdded 50 packages\n";
769 let compressed = dispatch("pnpm install", output);
770 assert!(compressed.contains("Added") || compressed.contains("Progress"));
772 }
773}
774
775#[cfg(test)]
776mod normalize_command_tests {
777 use super::*;
778
779 #[test]
780 fn passes_bare_commands_unchanged() {
781 assert_eq!(normalize_command_for_dispatch("bun test"), None);
782 assert_eq!(normalize_command_for_dispatch("cargo build"), None);
783 assert_eq!(normalize_command_for_dispatch("git status"), None);
784 }
785
786 #[test]
787 fn strips_cd_and_amp_prefix() {
788 assert_eq!(
789 normalize_command_for_dispatch("cd /repo && bun test").as_deref(),
790 Some("bun test")
791 );
792 assert_eq!(
793 normalize_command_for_dispatch("cd /repo/packages/aft && cargo test --release")
794 .as_deref(),
795 Some("cargo test --release")
796 );
797 }
798
799 #[test]
800 fn strips_cd_and_semicolon_prefix() {
801 assert_eq!(
802 normalize_command_for_dispatch("cd /repo; bun test").as_deref(),
803 Some("bun test")
804 );
805 }
806
807 #[test]
808 fn strips_cd_with_quoted_path() {
809 assert_eq!(
810 normalize_command_for_dispatch("cd \"/path with space\" && npm install").as_deref(),
811 Some("npm install")
812 );
813 }
814
815 #[test]
816 fn strips_env_assignments() {
817 assert_eq!(
818 normalize_command_for_dispatch("env FOO=bar npm install").as_deref(),
819 Some("npm install")
820 );
821 assert_eq!(
822 normalize_command_for_dispatch("env FOO=bar BAZ=qux RUST_LOG=info cargo test")
823 .as_deref(),
824 Some("cargo test")
825 );
826 }
827
828 #[test]
829 fn strips_bare_assignment_prefixes() {
830 assert_eq!(
831 normalize_command_for_dispatch("NODE_ENV=production npm install").as_deref(),
832 Some("npm install")
833 );
834 assert_eq!(
835 normalize_command_for_dispatch("FOO=1 BAR=2 cargo test").as_deref(),
836 Some("cargo test")
837 );
838 assert_eq!(
839 normalize_command_for_dispatch("RUSTFLAGS='-C debug' cargo build").as_deref(),
840 Some("cargo build")
841 );
842 }
843
844 #[test]
845 fn does_not_strip_later_assignment_arguments() {
846 assert_eq!(normalize_command_for_dispatch("npm install foo=bar"), None);
847 }
848
849 #[test]
850 fn env_without_assignments_returns_none() {
851 assert_eq!(
853 normalize_command_for_dispatch("env npm install").as_deref(),
854 None
855 );
856 }
857
858 #[test]
859 fn strips_timeout_prefix() {
860 assert_eq!(
861 normalize_command_for_dispatch("timeout 30 cargo test").as_deref(),
862 Some("cargo test")
863 );
864 assert_eq!(
865 normalize_command_for_dispatch("timeout 5m bun test").as_deref(),
866 Some("bun test")
867 );
868 }
869
870 #[test]
871 fn strips_nohup_prefix() {
872 assert_eq!(
873 normalize_command_for_dispatch("nohup ./long-running-script.sh").as_deref(),
874 Some("./long-running-script.sh")
875 );
876 }
877
878 #[test]
879 fn strips_paren_then_cd_and_amp() {
880 assert_eq!(
881 normalize_command_for_dispatch("(cd /repo && bun test").as_deref(),
882 Some("bun test")
883 );
884 }
885
886 #[test]
887 fn chains_multiple_prefixes() {
888 assert_eq!(
890 normalize_command_for_dispatch("env FOO=bar timeout 30 cargo test").as_deref(),
891 Some("cargo test")
892 );
893 assert_eq!(
895 normalize_command_for_dispatch("cd /repo && env FOO=bar npm install").as_deref(),
896 Some("npm install")
897 );
898 }
899
900 fn empty_registry() -> FilterRegistry {
903 FilterRegistry::default()
904 }
905
906 #[test]
907 fn cd_prefix_bun_test_still_routes_to_bun_test() {
908 let output = "bun test v1.3.14\n\nsrc/a.test.ts:\n(pass) ok [0.1ms]\n\n 1 pass\n 0 fail\n 1 expect() calls\nRan 1 tests across 1 files. [1.00ms]\n";
909 let compressed = compress_with_registry("cd /repo && bun test", output, &empty_registry());
910 assert!(compressed.contains("(pass)") || compressed.contains("1 pass"));
915 }
916
917 #[test]
918 fn cd_prefix_cargo_test_still_routes_to_cargo() {
919 let output = "running 5 tests\ntest foo ... ok\ntest bar ... FAILED\n\nfailures:\n\ntest result: FAILED. 4 passed; 1 failed\n";
920 let compressed =
921 compress_with_registry("cd /repo && cargo test", output, &empty_registry());
922 assert!(compressed.contains("FAILED") || compressed.contains("failed"));
923 }
924
925 #[test]
926 fn env_prefix_npm_install_still_routes_to_npm() {
927 let output = "added 50 packages, and audited 100 packages in 3s\n";
928 let compressed = compress_with_registry(
929 "env NODE_ENV=production npm install",
930 output,
931 &empty_registry(),
932 );
933 assert!(compressed.contains("added") || compressed.contains("audited"));
935 }
936
937 #[test]
938 fn bare_assignment_prefix_npm_install_routes_to_npm() {
939 let output = "npm http fetch GET 200 https://registry.npmjs.org/foo 123ms\nnpm WARN deprecated old-pkg@1.0.0: use new-pkg instead\n\nadded 42 packages in 2s\n\naudited 100 packages in 2s\n\nfound 0 vulnerabilities\n";
940 let compressed =
941 compress_with_registry("NODE_ENV=production npm install", output, &empty_registry());
942 assert!(!compressed.contains("npm http fetch"));
943 assert!(compressed.contains("audited 100 packages"));
944 }
945
946 #[test]
947 fn bare_assignment_prefix_cargo_test_routes_to_cargo() {
948 let output = "running 1 test\ntest foo ... ok\n\ntest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n";
949 let compressed =
950 compress_with_registry("FOO=1 BAR=2 cargo test", output, &empty_registry());
951 assert!(compressed.contains("running 1 test"));
952 assert!(compressed.contains("test result: ok"));
953 assert!(!compressed.contains("test foo ... ok"));
954 }
955
956 #[test]
957 fn quoted_assignment_prefix_cargo_build_routes_to_cargo() {
958 let output = " Compiling foo v0.1.0\nwarning: unused variable: `x`\n --> src/lib.rs:1:9\n |\n1 | let x = 1;\n | ^ help: if this is intentional, prefix it with an underscore: `_x`\n\n Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s\n";
959 let compressed = compress_with_registry(
960 "RUSTFLAGS='-C debug' cargo build",
961 output,
962 &empty_registry(),
963 );
964 assert!(!compressed.contains("Compiling foo"));
965 assert!(compressed.contains("warning: unused variable"));
966 assert!(compressed.contains("Finished `dev` profile"));
967 }
968
969 #[test]
970 fn timeout_prefix_cargo_build_still_routes_to_cargo() {
971 let output =
972 " Compiling foo v0.1.0\n Finished `dev` profile [unoptimized] target(s) in 5s\n";
973 let compressed =
974 compress_with_registry("timeout 30 cargo build", output, &empty_registry());
975 assert!(compressed.contains("Compiling") || compressed.contains("Finished"));
977 }
978}