//! Adversarial compression tests based on TheDecipherist/rtk-test methodology. //! Each test verifies that safety-critical information survives compression. use lean_ctx::core::patterns::compress_output; #[test] fn adversarial_git_diff_preserves_code_content() { let diff = "diff --git a/src/auth.rs b/src/auth.rs\n\ index abc123..def456 100644\n\ --- a/src/auth.rs\n\ +++ b/src/auth.rs\n\ @@ -10,6 +10,8 @@ fn verify_token(token: &str) -> bool {\n\ let decoded = decode(token);\n\ if decoded.is_err() {\n\ return false;\n\ + }\n\ + if decoded.unwrap().exp < now() {\n\ + return false; // expired token\n\ }\n\ true\n\ }"; let compressed = compress_output("git diff", diff).unwrap(); assert!( compressed.contains("expired token"), "diff must preserve code content: {compressed}" ); assert!( compressed.contains('+'), "diff must preserve +/- markers: {compressed}" ); } #[test] fn adversarial_git_diff_preserves_security_bug() { let diff = "diff --git a/src/api.rs b/src/api.rs\n\ --- a/src/api.rs\n\ +++ b/src/api.rs\n\ @@ -5,3 +5,5 @@\n\ - verify_csrf_token(&request);\n\ + // TODO: re-enable CSRF check\n\ + // verify_csrf_token(&request);\n\ process_request(&request);\n"; let compressed = compress_output("git diff", diff).unwrap_or_else(|| diff.to_string()); assert!( compressed.contains("CSRF") || compressed.contains("csrf"), "diff must preserve security-relevant changes: {compressed}" ); assert!( compressed.contains("verify_csrf_token"), "diff must preserve removed function calls: {compressed}" ); } #[test] fn adversarial_docker_ps_preserves_unhealthy() { let ps_output = "CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\n\ abc123def456 nginx:latest \"nginx -g…\" 2 hours ago Up 2 hours (unhealthy) 80/tcp web-prod\n\ 789ghi012jkl redis:7 \"redis-se…\" 3 hours ago Up 3 hours (healthy) 6379/tcp cache-prod\n\ 345mno678pqr postgres:16 \"docker-e…\" 5 hours ago Exited (1) 30 minutes ago db-prod"; // docker ps is Verbatim via OutputPolicy — compress_output returns None, // meaning output is preserved unmodified. This is the safest outcome. assert!( compress_output("docker ps", ps_output).is_none(), "docker ps must be Verbatim (no compression)" ); } #[test] fn adversarial_df_preserves_root_filesystem() { let mut lines = vec!["Filesystem 1K-blocks Used Available Use% Mounted on".to_string()]; lines.push("/dev/sda1 100000000 95000000 5000000 95% /".to_string()); for i in 0..15 { lines.push(format!( "tmpfs 1000 100 900 10% /snap/core/{i}" )); } let df_output = lines.join("\n"); let result = compress_output("df -h", &df_output).unwrap_or_else(|| df_output.clone()); assert!( result.contains("/dev/sda1") || result.contains("95%"), "df must preserve root filesystem info: {result}" ); assert!( result.contains("/ ") || result.contains("Mounted on"), "df must preserve mount points: {result}" ); } #[test] fn adversarial_pytest_preserves_xfail_xpass() { let output = "============================= test session starts ==============================\n\ collected 20 items\n\ \n\ tests/test_auth.py ....x.X... [100%]\n\ \n\ ================== 15 passed, 2 xfailed, 1 xpassed, 2 warnings in 3.5s =================="; let compressed = compress_output("pytest", output).unwrap(); assert!( compressed.contains("xfailed") || compressed.contains("xfail"), "pytest must preserve xfailed counter: {compressed}" ); assert!( compressed.contains("xpassed") || compressed.contains("xpass"), "pytest must preserve xpassed counter: {compressed}" ); assert!( compressed.contains("warning"), "pytest must preserve warnings counter: {compressed}" ); } #[test] fn adversarial_git_log_preserves_full_history() { let mut log_lines: Vec = Vec::new(); for i in 0..60 { log_lines.push(format!("abc{i:04x} fix: commit message number {i}")); } let output = log_lines.join("\n"); let compressed_unlimited = compress_output("git log -n 60 --oneline", &output).unwrap(); assert!( compressed_unlimited.contains("commit message number 59"), "git log with explicit -n must preserve all entries: {compressed_unlimited}" ); let compressed_default = compress_output("git log --oneline", &output).unwrap(); assert!( compressed_default.contains("commit message number 59"), "git log default should show all 60 entries (under 100 cap): {compressed_default}" ); } #[test] fn adversarial_grep_preserves_context() { let mut grep_lines: Vec = Vec::new(); for i in 0..80 { grep_lines.push(format!("src/auth.rs:{i}: let user = get_user(id);")); } let output = grep_lines.join("\n"); let result = compress_output("grep -rn 'get_user'", &output); if let Some(ref compressed) = result { assert!( compressed.contains("get_user"), "grep compressed output must preserve key content: {compressed}" ); assert!( compressed.contains("auth.rs"), "grep compressed output must preserve file reference: {compressed}" ); assert!( compressed.len() <= output.len(), "grep compression must not inflate: {} vs {}", compressed.len(), output.len() ); } } #[test] fn adversarial_log_preserves_critical_severity() { let mut log = Vec::new(); for i in 0..40 { log.push(format!("2024-01-01 10:00:{i:02} INFO request processed")); } log.insert( 20, "2024-01-01 10:00:20 CRITICAL database connection lost".to_string(), ); log.insert( 25, "2024-01-01 10:00:25 ERROR OOMKilled: container exceeded memory".to_string(), ); let output = log.join("\n"); let compressed = compress_output("cat /var/log/app.log", &output); let text = compressed.unwrap_or_else(|| output.clone()); assert!( text.contains("CRITICAL") || text.contains("database connection lost"), "log output must preserve CRITICAL lines: {text}" ); } #[test] fn adversarial_npm_audit_preserves_cve_ids() { let audit = "# npm audit report\n\ \n\ lodash <=4.17.20\n\ Severity: critical\n\ Prototype Pollution - https://github.com/advisories/GHSA-xxxx\n\ fix available via `npm audit fix --force`\n\ depends on vulnerable versions of lodash\n\ node_modules/lodash\n\ \n\ express <4.17.3\n\ Severity: high\n\ CVE-2024-12345 - Open redirect vulnerability\n\ fix available via `npm audit fix`\n\ node_modules/express\n\ \n\ 2 vulnerabilities (1 high, 1 critical)\n"; // npm audit is Verbatim via OutputPolicy (is_package_manager_info), // so compress_output returns None — output preserved fully. assert!( compress_output("npm audit", audit).is_none(), "npm audit must be Verbatim (no compression)" ); } #[test] fn adversarial_docker_logs_preserves_critical() { let mut log = Vec::new(); for i in 0..50 { log.push(format!( "2024-01-01T10:00:{i:02}Z INFO healthy check passed" )); } log.insert( 15, "2024-01-01T10:00:15Z FATAL out of memory, container killed".to_string(), ); log.insert( 30, "2024-01-01T10:00:30Z ERROR panic: runtime error".to_string(), ); let output = log.join("\n"); // docker logs is Verbatim via OutputPolicy (is_log_viewer), // so output is preserved in full — no compression at all. assert!( compress_output("docker logs mycontainer", &output).is_none(), "docker logs must be Verbatim (no compression)" ); } #[test] fn adversarial_pip_uninstall_preserves_package_names() { let output = "Found existing installation: requests 2.28.0\n\ Uninstalling requests-2.28.0:\n\ Successfully uninstalled requests-2.28.0\n\ Found existing installation: flask 2.3.0\n\ Uninstalling flask-2.3.0:\n\ Successfully uninstalled flask-2.3.0\n\ Found existing installation: numpy 1.24.0\n\ Uninstalling numpy-1.24.0:\n\ Successfully uninstalled numpy-1.24.0\n"; let compressed = compress_output("pip uninstall requests flask numpy -y", output).unwrap(); assert!( compressed.contains("requests"), "pip uninstall must list package names: {compressed}" ); assert!( compressed.contains("flask"), "pip uninstall must list package names: {compressed}" ); assert!( compressed.contains("numpy"), "pip uninstall must list package names: {compressed}" ); } #[test] fn adversarial_middle_truncation_preserves_errors() { let mut lines: Vec = Vec::new(); for i in 0..60 { lines.push(format!("line {i}: normal output")); } lines[30] = "ERROR: critical failure in module X".to_string(); lines[35] = "WARNING: disk space low".to_string(); let output = lines.join("\n"); let compressed = lean_ctx::shell::compress_if_beneficial_pub("unknown-command", &output); if compressed.contains('[') && compressed.contains("omitted") { assert!( compressed.contains("ERROR") || compressed.contains("critical failure"), "truncation must preserve error lines: {compressed}" ); } } // ===== Regression tests: Scenarios that were SAFE in TheDecipherist/rtk-test v3.2.5 ===== // These must stay SAFE after the adversarial hardening changes. #[test] fn regression_git_status_detached_head() { let output = "HEAD detached at 48a7098\nnothing to commit, working tree clean"; let result = compress_output("git status", output).unwrap_or_else(|| output.to_string()); assert!( result.contains("detached") || result.contains("HEAD detached"), "git status must preserve DETACHED HEAD warning: {result}" ); } #[test] fn regression_log_critical_severity() { let output = "[INFO] health check ok\n\ [INFO] health check ok\n\ [CRITICAL] database connection lost\n\ [INFO] health check ok\n\ [ERROR] retry failed"; let compressed = compress_output("cat /var/log/app.log", output); let text = compressed.unwrap_or_else(|| output.to_string()); assert!( text.contains("CRITICAL"), "cat log must preserve CRITICAL lines: {text}" ); assert!( text.contains("ERROR"), "cat log must preserve ERROR lines: {text}" ); } #[test] fn regression_ls_shows_dotenv() { let output = ".env\n.gitignore\nREADME.md\nsrc\npackage.json"; let compressed = compress_output("ls -a", output); let text = compressed.unwrap_or_else(|| output.to_string()); assert!(text.contains(".env"), "ls must show .env file: {text}"); } #[test] fn regression_pip_list_all_packages() { let mut lines = vec![ "Package Version".to_string(), "---------- -------".to_string(), ]; for i in 0..50 { lines.push(format!("package-{i} 1.0.{i}")); } let output = lines.join("\n"); let compressed = compress_output("pip list", &output); let text = compressed.unwrap_or_else(|| output.clone()); assert!( text.contains("package-0") && text.contains("package-49"), "pip list must show all packages: first and last must be present" ); } #[test] fn regression_git_stash_verbatim() { let output = "No local changes to save"; let compressed = compress_output("git stash", output); let text = compressed.unwrap_or_else(|| output.to_string()); assert!( text.contains("No local changes"), "git stash must pass through verbatim: {text}" ); let output2 = "Saved working directory and index state WIP on main: abc1234 fix: typo"; let compressed2 = compress_output("git stash", output2); let text2 = compressed2.unwrap_or_else(|| output2.to_string()); assert!( text2.contains("Saved") || text2.contains("WIP on main"), "git stash save must pass through: {text2}" ); } #[test] fn regression_ruff_preserves_file_line_col() { let output = "src/api.py:42:10: E501 Line too long (120 > 79)\n\ src/api.py:88:1: F401 'os' imported but unused\n\ Found 2 errors."; let compressed = compress_output("ruff check", output); let text = compressed.unwrap_or_else(|| output.to_string()); assert!( text.contains("src/api.py:42:10"), "ruff must preserve file:line:col references: {text}" ); assert!( text.contains("src/api.py:88:1"), "ruff must preserve all references: {text}" ); } #[test] fn regression_find_preserves_full_paths() { let output = "/home/user/project/src/api/file.ts\n\ /home/user/project/src/utils/helper.ts\n\ /home/user/project/tests/test_api.ts"; let compressed = compress_output("find . -name '*.ts'", output); let text = compressed.unwrap_or_else(|| output.to_string()); assert!( text.contains("/home/user/project/src/api/file.ts"), "find must preserve full absolute paths: {text}" ); } #[test] fn regression_ls_recursive_preserves_tree() { let output = "./src:\napi.ts\nutils.ts\n\n./src/components:\nButton.tsx\nHeader.tsx\n\n./tests:\ntest_api.ts"; let compressed = compress_output("ls -R", output); let text = compressed.unwrap_or_else(|| output.to_string()); assert!( text.contains("./src:") && text.contains("./tests:"), "ls -R must preserve directory headers: {text}" ); } #[test] fn regression_wc_pipe_correct() { let output = "42"; let compressed = compress_output("wc -l", output); let text = compressed.unwrap_or_else(|| output.to_string()); assert!(text.contains("42"), "wc output must be preserved: {text}"); } // ===== New adversarial tests: comprehensive coverage across ecosystems ===== #[test] fn adversarial_npm_install_preserves_packages() { let output = "\ npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported npm warn deprecated inflight@1.0.6: This module is not supported added 547 packages, and audited 548 packages in 12s 75 packages are looking for funding run `npm fund` for details found 0 vulnerabilities"; let compressed = compress_output("npm install", output).unwrap(); assert!( compressed.contains("547"), "npm install must preserve package count: {compressed}" ); } #[test] fn adversarial_npm_install_with_explicit_packages() { let output = "\ + express@4.18.2 + lodash@4.17.21 + axios@1.6.2 added 58 packages, and audited 59 packages in 3s found 0 vulnerabilities"; let compressed = compress_output("npm install express lodash axios", output).unwrap(); assert!( compressed.contains("express"), "npm install must preserve installed package names: {compressed}" ); assert!( compressed.contains("lodash"), "npm install must preserve installed package names: {compressed}" ); assert!( compressed.contains("axios"), "npm install must preserve installed package names: {compressed}" ); } #[test] fn adversarial_cargo_build_preserves_errors() { let output = "\ Compiling myapp v0.1.0 (/home/user/myapp) error[E0308]: mismatched types --> src/main.rs:42:10 | 42| foo(x) | ^ expected `&str`, found `String` | error[E0599]: no method named `bar` found for struct `Config` --> src/config.rs:15:10 | 15 | cfg.bar() | ^^^ method not found in `Config` error: could not compile `myapp` (bin \"myapp\") due to 2 previous errors"; let compressed = compress_output("cargo build", output).unwrap(); assert!( compressed.contains("E0308"), "cargo build must preserve error codes: {compressed}" ); assert!( compressed.contains("E0599"), "cargo build must preserve all error codes: {compressed}" ); assert!( compressed.contains("mismatched types") || compressed.contains("expected"), "cargo build must preserve error messages: {compressed}" ); } #[test] fn adversarial_eslint_preserves_file_line_and_rules() { let output = "\ /home/user/project/src/App.tsx 5:10 error 'useState' is defined but never used no-unused-vars 12:15 warning Unexpected any @typescript-eslint/no-explicit-any 33:1 error Missing return type @typescript-eslint/explicit-function-return-type /home/user/project/src/utils.ts 8:5 error Unexpected var no-var 4 problems (3 errors, 1 warning)"; let compressed = compress_output("eslint .", output).unwrap(); assert!( compressed.contains("no-unused-vars") || compressed.contains("unused"), "eslint must preserve rule names: {compressed}" ); assert!( compressed.contains("3 error") || compressed.contains("error"), "eslint must preserve error counts: {compressed}" ); } #[test] fn adversarial_go_build_preserves_errors() { let output = "\ ./main.go:15:2: undefined: Config ./main.go:23:10: cannot use x (variable of type string) as int value in argument to process ./handlers/auth.go:42:5: too many arguments in call to validateToken ./handlers/auth.go:55:12: impossible type assertion: *http.Request does not implement CustomRequest"; let result = compress_output("go build ./...", output).unwrap_or_else(|| output.to_string()); assert!( result.contains("main.go") || result.contains("undefined"), "go build must preserve file references: {result}" ); assert!( result.contains("auth.go") || result.contains("validateToken"), "go build must preserve all error locations: {result}" ); } #[test] fn adversarial_docker_build_preserves_step_errors() { let output = "\ #1 [internal] load build definition from Dockerfile #1 DONE 0.0s #5 [2/5] RUN apt-get update && apt-get install -y curl #5 DONE 15.2s #6 [3/5] COPY requirements.txt . #6 DONE 0.1s #7 [4/5] RUN pip install -r requirements.txt #7 4.521 ERROR: Could not find a version that satisfies the requirement nonexistent-package==99.0 #7 4.521 ERROR: No matching distribution found for nonexistent-package==99.0 #7 ERROR: process \"/bin/sh -c pip install -r requirements.txt\" did not complete successfully: exit code: 1"; let compressed = compress_output("docker build .", output).unwrap(); assert!( compressed.contains("ERROR") || compressed.contains("error"), "docker build must preserve error lines: {compressed}" ); assert!( compressed.contains("nonexistent-package") || compressed.contains("not complete"), "docker build must preserve error details: {compressed}" ); } #[test] fn adversarial_tsc_preserves_type_errors() { let output = "\ src/api/routes.ts(15,10): error TS2304: Cannot find name 'Request'. src/api/routes.ts(22,5): error TS2339: Property 'userId' does not exist on type 'Session'. src/utils/auth.ts(8,3): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. src/utils/auth.ts(42,20): error TS7006: Parameter 'req' implicitly has an 'any' type. Found 4 errors in 2 files."; let compressed = compress_output("tsc --noEmit", output).unwrap(); assert!( compressed.contains("TS2304"), "tsc must preserve error code TS2304: {compressed}" ); assert!( compressed.contains("TS2339"), "tsc must preserve error code TS2339: {compressed}" ); assert!( compressed.contains("routes.ts") || compressed.contains("auth.ts"), "tsc must preserve file references: {compressed}" ); assert!( compressed.contains("4 error"), "tsc must preserve error count: {compressed}" ); } #[test] fn adversarial_dotnet_build_preserves_errors() { let output = "\ Microsoft (R) Build Engine version 17.8.3+195e7f5a3 for .NET Copyright (C) Microsoft Corporation. All rights reserved. Determining projects to restore... All projects are up-to-date for restore. Controllers/UserController.cs(15,25): error CS0246: The type or namespace name 'UserService' could not be found Models/User.cs(8,12): error CS0246: The type or namespace name 'JsonProperty' could not be found Build FAILED. Controllers/UserController.cs(15,25): error CS0246: The type or namespace name 'UserService' could not be found Models/User.cs(8,12): error CS0246: The type or namespace name 'JsonProperty' could not be found 2 Error(s) 0 Warning(s) Time Elapsed 00:00:01.82"; let compressed = compress_output("dotnet build", output).unwrap(); assert!( compressed.contains("CS0246") || compressed.contains("error"), "dotnet build must preserve error codes: {compressed}" ); assert!( compressed.contains("FAILED") || compressed.contains('2'), "dotnet build must preserve build result: {compressed}" ); } #[test] fn adversarial_composer_install_preserves_packages() { let output = "\ Loading composer repositories with package information Updating dependencies Lock file operations: 5 installs, 0 updates, 0 removals - Installing psr/log (3.0.0): Extracting archive - Installing monolog/monolog (3.5.0): Extracting archive - Installing symfony/console (7.0.3): Extracting archive - Installing laravel/framework (11.0.0): Extracting archive - Installing phpunit/phpunit (10.5.0): Extracting archive Writing lock file Generating optimized autoload files Package operations: 5 installs, 0 updates, 0 removals"; let compressed = compress_output("composer install", output).unwrap(); assert!( compressed.contains('5') || compressed.contains("install"), "composer install must preserve package count: {compressed}" ); } #[test] fn adversarial_cargo_test_preserves_failures() { let output = "\ Compiling myapp v0.1.0 (/home/user/myapp) Finished `test` profile [unoptimized + debuginfo] target(s) in 2.34s Running unittests src/lib.rs (target/debug/deps/myapp-abc123) running 25 tests test auth::tests::login_works ... ok test auth::tests::logout_works ... ok test auth::tests::token_expired ... FAILED test db::tests::connection_pool ... ok failures: ---- auth::tests::token_expired stdout ---- thread 'auth::tests::token_expired' panicked at 'assertion failed: `(left == right)` left: `true`, right: `false`', src/auth.rs:142:9 failures: auth::tests::token_expired test result: FAILED. 24 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.52s"; let compressed = compress_output("cargo test", output).unwrap(); assert!( compressed.contains("1 failed") || compressed.contains("FAILED"), "cargo test must preserve failure count: {compressed}" ); assert!( compressed.contains("24 passed") || compressed.contains("24"), "cargo test must preserve passed count: {compressed}" ); } #[test] fn adversarial_kubectl_get_preserves_pod_status() { let output = "\ NAME READY STATUS RESTARTS AGE api-deploy-abc123-xyz 1/1 Running 0 5d web-deploy-def456-uvw 0/1 CrashLoopBackOff 15 (2m ago) 1h worker-deploy-ghi789-rst 1/1 Running 0 3d db-migrate-job-abc 0/1 Error 0 30m"; // kubectl get pods is Compressible (not Verbatim), but the kubectl // pattern engine must preserve all critical status information. match compress_output("kubectl get pods", output) { None => {} // Verbatim passthrough is also fine Some(compressed) => { assert!( compressed.contains("CrashLoopBackOff"), "must preserve CrashLoopBackOff status: {compressed}" ); assert!( compressed.contains("Error"), "must preserve Error status: {compressed}" ); assert!( compressed.contains("web-deploy") || compressed.contains("def456"), "must preserve failing pod identifier: {compressed}" ); } } } #[test] fn adversarial_terraform_plan_preserves_changes() { let output = "\ Terraform will perform the following actions: # aws_instance.web will be destroyed - resource \"aws_instance\" \"web\" { - ami = \"ami-abc123\" -> null - instance_type = \"t3.large\" -> null } # aws_security_group.allow_all will be created + resource \"aws_security_group\" \"allow_all\" { + name = \"allow_all\" + ingress { + from_port = 0 + to_port = 65535 + protocol = \"-1\" + cidr_blocks = [\"0.0.0.0/0\"] } } Plan: 1 to add, 0 to change, 1 to destroy."; let compressed = compress_output("terraform plan", output).unwrap(); assert!( compressed.contains("destroy") || compressed.contains("1 to destroy"), "terraform plan must preserve destructive actions: {compressed}" ); assert!( compressed.contains("add") || compressed.contains("1 to add"), "terraform plan must preserve additions: {compressed}" ); } // ===== Issue #149: TheDecipherist security review — additional adversarial tests ===== #[test] fn adversarial_git_show_preserves_diff_content() { let output = "\ commit abc1234def5678901234567890abcdef12345678 Author: Dev Date: Mon Jan 1 10:00:00 2024 +0000 fix: remove fee from charge calculation diff --git a/src/billing.rs b/src/billing.rs --- a/src/billing.rs +++ b/src/billing.rs @@ -10,3 +10,3 @@ - return charge(amount + fee); + return charge(amount); // BUG: fee not applied log_transaction(amount);"; let compressed = compress_output("git show abc1234", output).unwrap(); assert!( compressed.contains("charge(amount)") || compressed.contains("fee not applied"), "git show must preserve diff code content: {compressed}" ); assert!( compressed.contains('+') || compressed.contains('-'), "git show must preserve diff +/- markers: {compressed}" ); } #[test] fn adversarial_git_show_preserves_security_change() { let output = "\ commit deadbeef12345678901234567890abcdef12345678 Author: Dev Date: Mon Jan 1 10:00:00 2024 +0000 chore: disable auth temporarily diff --git a/src/auth.rs b/src/auth.rs --- a/src/auth.rs +++ b/src/auth.rs @@ -5,3 +5,5 @@ - verify_csrf_token(&request); + // HACK: skip CSRF for now + // verify_csrf_token(&request); process_request(&request);"; let compressed = compress_output("git show deadbeef", output).unwrap(); assert!( compressed.contains("CSRF") || compressed.contains("csrf"), "git show must preserve security-relevant changes: {compressed}" ); assert!( compressed.contains("verify_csrf_token"), "git show must preserve removed function calls: {compressed}" ); } #[test] fn adversarial_docker_ps_unhealthy_narrow_columns() { let output = "\ CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES abc123def456 nginx:latest \"nginx\" 2 hours ago Up 2 hours (unhealthy) 80/tcp web 789ghi012jkl redis:7 \"redis\" 3 hours ago Up 3 hours 6379/tcp cache"; // docker ps is Verbatim — output preserved unmodified. assert!( compress_output("docker ps", output).is_none(), "docker ps must be Verbatim (no compression)" ); } #[test] fn adversarial_docker_ps_exited_containers() { let output = "\ CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES abc123def456 nginx:latest \"nginx\" 2 hours ago Exited (1) 30 minutes ago web-crashed 789ghi012jkl redis:7 \"redis\" 3 hours ago Up 3 hours (healthy) 6379/tcp cache"; // docker ps -a is Verbatim — output preserved unmodified. assert!( compress_output("docker ps -a", output).is_none(), "docker ps -a must be Verbatim (no compression)" ); } #[test] fn adversarial_git_log_100_plus_commits() { let mut log_lines: Vec = Vec::new(); for i in 0..120 { log_lines.push(format!("abc{i:04x} fix: commit message number {i}")); } let output = log_lines.join("\n"); let compressed = compress_output("git log --oneline", &output).unwrap(); assert!( compressed.contains("commit message number 99"), "git log default should show at least 100 entries: {compressed}" ); assert!( compressed.contains("20 more commits"), "git log should indicate truncated count: {compressed}" ); } #[test] fn adversarial_git_log_explicit_limit_unlimited() { let mut log_lines: Vec = Vec::new(); for i in 0..120 { log_lines.push(format!("abc{i:04x} fix: commit message number {i}")); } let output = log_lines.join("\n"); let compressed = compress_output("git log -n 120 --oneline", &output).unwrap(); assert!( compressed.contains("commit message number 119"), "git log with explicit -n must preserve all entries: {compressed}" ); assert!( !compressed.contains("more commits"), "git log with explicit -n must not truncate: {compressed}" ); } #[test] fn adversarial_safeguard_ratio_prevents_over_compression() { use lean_ctx::core::compressor::safeguard_ratio; let original = "a]b ".repeat(200); let over_compressed = "x"; let result = safeguard_ratio(&original, over_compressed); assert_eq!( result, original, "safeguard_ratio must return original when ratio < 0.05 and output is small" ); let mild_compressed = "a]b ".repeat(80); let result2 = safeguard_ratio(&original, &mild_compressed); assert_eq!( result2, mild_compressed, "safeguard_ratio must allow mild compression" ); } #[test] fn adversarial_shell_hook_preserves_errors_in_truncation() { let mut lines: Vec = Vec::new(); for i in 0..100 { lines.push(format!("normal output line {i}")); } lines[50] = "CRITICAL: database corruption detected in row 4821".to_string(); lines[75] = "ERROR: payment processing service unreachable".to_string(); let output = lines.join("\n"); let compressed = lean_ctx::shell::compress_if_beneficial_pub("cat /var/log/app.log", &output); assert!( compressed.contains("CRITICAL") || compressed.contains("database corruption"), "shell hook must preserve CRITICAL lines during truncation: {compressed}" ); assert!( compressed.contains("ERROR") || compressed.contains("payment processing"), "shell hook must preserve ERROR lines during truncation: {compressed}" ); } // --- gh CLI security regression tests (issue #149 follow-up) --- #[test] fn adversarial_gh_pr_diff_passes_through_verbatim() { let diff = r#"diff --git a/src/auth.rs b/src/auth.rs --- a/src/auth.rs +++ b/src/auth.rs @@ -42,7 +42,6 @@ fn validate_csrf_token(req: &Request) -> bool { let token = req.header("X-CSRF-Token"); - if !csrf::verify(token, &session.secret) { - return false; - } + // SECURITY: CSRF check removed for performance true } "#; let result = compress_output("gh pr diff 185", diff).unwrap_or_else(|| diff.to_string()); assert!( result.contains("csrf::verify"), "gh pr diff must preserve all diff content: {result}" ); assert!( result.contains("SECURITY: CSRF check removed"), "gh pr diff must preserve security-critical additions: {result}" ); } #[test] fn adversarial_gh_api_passes_through_verbatim() { let api_output = r#"[{"filename":"src/auth.rs","patch":"@@ -42,7 +42,6 @@\n- csrf::verify(token)\n+ // removed"}]"#; let result = compress_output("gh api repos/org/repo/pulls/42/files", api_output) .unwrap_or_else(|| api_output.to_string()); assert!( result.len() <= api_output.len(), "gh api output must not inflate: {} vs {}", result.len(), api_output.len() ); } #[test] fn adversarial_gh_search_passes_through_verbatim() { let search_output = (0..50) .map(|i| format!("repo/file{i}.rs:42: fn vulnerable_handler() {{")) .collect::>() .join("\n"); let result = compress_output("gh search code 'sql injection'", &search_output) .unwrap_or_else(|| search_output.clone()); assert!( result.contains("vulnerable_handler"), "gh search output must preserve security-critical content: {result}" ); } #[test] fn adversarial_gh_workflow_passes_through_verbatim() { let workflow_output = "ID STATUS CONCLUSION NAME\n123456 completed failure CI\n123457 completed success Deploy\n123458 in_progress Security Scan"; let result = compress_output("gh workflow view ci.yml", workflow_output) .unwrap_or_else(|| workflow_output.to_string()); assert!( result.contains("failure") && result.contains("CI"), "gh workflow output must preserve status info: {result}" ); } #[test] fn adversarial_gh_pr_list_still_compresses() { let pr_list = "#185\tSupport configurable proxy\tzsefvlol:feat/proxy\tDRAFT\n#184\tFix auth bypass\tsecurity-team:fix/auth\tOPEN\n"; let result = compress_output("gh pr list", pr_list).unwrap_or_else(|| pr_list.to_string()); assert!( result.contains("auth bypass") || result.contains("Fix auth"), "gh pr list output must preserve PR info: {result}" ); } #[test] fn adversarial_gh_diff_preserves_security_change() { let diff = format!( "diff --git a/middleware/auth.js b/middleware/auth.js\n{}\n{}", "- const isAdmin = await checkAdminRole(user);", "+ const isAdmin = true; // HACK: bypassed for demo" ); let shell_result = lean_ctx::shell::compress_if_beneficial_pub("gh pr diff 200", &diff); assert!( shell_result.contains("isAdmin = true") || shell_result.contains("HACK"), "gh pr diff through shell hook must preserve security-critical diff content: {shell_result}" ); } #[test] fn rg_large_output_compresses_through_pipeline() { let mut lines = Vec::new(); for i in 0..500 { for f in &[ "src/core/mod.rs", "src/cli/dispatch.rs", "src/tools/ctx_shell.rs", "src/server/mod.rs", "src/lib.rs", ] { lines.push(format!("{f}:{}: fn func_{i}() {{", i + 1)); } } let output = lines.join("\n"); let input_len = output.len(); let result = lean_ctx::shell::compress_if_beneficial_pub("rg 'fn ' --no-heading -n src/", &output); assert!( result.len() < input_len, "rg pipeline must compress: result={} vs original={}", result.len(), input_len ); assert!( !result.contains("[lean-ctx:"), "savings footer must be silent by default" ); }