use std::path::Path;
use serde_json::Value;
use super::readonly::{
command_words_are_readonly, static_shell_command_words, static_shell_command_words_with_output_plumbing,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ShellActivity {
Inspection,
Verification,
Mutation,
}
fn make_like_verification_targets(words: &[String]) -> bool {
let mut saw_target = false;
for word in words.iter().skip(1) {
let lower = word.to_ascii_lowercase();
if lower.starts_with('-') || lower.contains('=') {
continue;
}
if !matches!(lower.as_str(), "test" | "tests" | "check" | "checks" | "lint" | "verify" | "validate") {
return false;
}
saw_target = true;
}
saw_target
}
fn is_verification_invocation(words: &[String]) -> bool {
let command_words = crate::tools::command_args::command_words_after_environment_prefix(words);
let first = command_words.first().map(String::as_str).unwrap_or_default();
let second = command_words.get(1).map(|word| word.to_ascii_lowercase());
let third = command_words.get(2).map(|word| word.to_ascii_lowercase());
let program = Path::new(first)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(first)
.to_ascii_lowercase();
match program.as_str() {
"cargo" => {
if second.as_deref() == Some("fmt") {
return words.iter().any(|word| word == "--check");
}
matches!(second.as_deref(), Some("check" | "build" | "clippy" | "test"))
|| (second.as_deref() == Some("nextest") && third.as_deref() == Some("run"))
}
"go" => matches!(second.as_deref(), Some("test" | "build")),
"npm" | "pnpm" | "yarn" => {
matches!(second.as_deref(), Some("test" | "build" | "lint" | "check"))
|| (second.as_deref() == Some("run")
&& matches!(third.as_deref(), Some("test" | "build" | "lint" | "check")))
}
"bun" | "bunx" => {
matches!(second.as_deref(), Some("test"))
|| (second.as_deref() == Some("run")
&& matches!(third.as_deref(), Some("test" | "build" | "lint" | "check")))
}
"deno" => matches!(second.as_deref(), Some("test" | "lint" | "check")),
"make" | "gmake" | "just" => make_like_verification_targets(command_words),
"uv" => {
command_words.iter().any(|word| word == "pytest")
|| (command_words.iter().any(|word| word == "ruff") && command_words.iter().any(|word| word == "check"))
}
"ruff" => {
if second.as_deref() == Some("format") {
return words.iter().any(|word| word == "--check");
}
matches!(second.as_deref(), Some("check" | "lint"))
}
"tsc" => {
words.iter().any(|word| word == "--noEmit")
}
"eslint" => {
!words.iter().any(|word| word == "--fix" || word == "--fix-dry-run")
}
"rustc" | "pytest" | "xcodebuild" | "gradle" | "gradlew" => true,
"python" | "python3" => {
command_words.iter().any(|word| word == "-m") && command_words.iter().any(|word| word == "pytest")
}
_ if first.ends_with("/scripts/check.sh") || first.ends_with("/scripts/check-dev.sh") => true,
_ => false,
}
}
fn contains_verification_invocation(command: &str) -> bool {
static_shell_command_words(command)
.is_some_and(|commands| commands.iter().any(|words| is_verification_invocation(words)))
}
pub fn default_verifier_for_workspace(workspace_root: &Path) -> Option<String> {
if workspace_root.join("Cargo.toml").is_file() {
return Some("cargo check --locked".to_string());
}
if workspace_root.join("go.mod").is_file() {
return Some("go test ./...".to_string());
}
if workspace_root.join("deno.json").is_file() || workspace_root.join("deno.jsonc").is_file() {
return Some("deno test".to_string());
}
if workspace_root.join("bun.lockb").is_file() || workspace_root.join("bun.lock").is_file() {
return Some("bun test".to_string());
}
let package_json = workspace_root.join("package.json");
if package_json.is_file() {
if let Ok(content) = std::fs::read_to_string(&package_json)
&& let Ok(parsed) = serde_json::from_str::<Value>(&content)
&& let Some(scripts) = parsed.get("scripts").and_then(Value::as_object)
{
if scripts.contains_key("test") {
return Some("npm test".to_string());
}
if scripts.contains_key("check") {
return Some("npm run check".to_string());
}
if scripts.contains_key("lint") {
return Some("npm run lint".to_string());
}
if scripts.contains_key("build") {
return Some("npm run build".to_string());
}
}
return Some("npm test".to_string());
}
for marker in ["pytest.ini", "pyproject.toml", "setup.cfg", "tox.ini"] {
if workspace_root.join(marker).is_file() {
return Some("pytest -q".to_string());
}
}
if workspace_root.join("Makefile").is_file() || workspace_root.join("makefile").is_file() {
return Some("make test".to_string());
}
if workspace_root.join("justfile").is_file() || workspace_root.join("Justfile").is_file() {
return Some("just test".to_string());
}
None
}
pub fn verification_recovery_directive(default_verifier: Option<&str>, attempt: u8, max_attempts: u8) -> String {
let command = default_verifier.unwrap_or("cargo check --locked");
format!(
"AUTONOMOUS VERIFICATION RECOVERY ({attempt}/{max_attempts}): verification is still pending and the turn will block without it. \
Stop editing and run one verifier NOW with `exec_command` — `{command}` — standalone or as a pure `&&` chain of verifiers \
(no `|`, `;`, or `||`; cap output with `max_output_tokens` instead of piping). Pure `| head`/`| tail` truncators are elided at execution. \
Let it exit 0 before another mutation. \
A failed verifier grants bounded fix-up edits before re-verify is required; filtering pipes (`| grep`), `;`, and `||` joins never clear the gate."
)
}
pub fn shell_command_is_admitted_verification_attempt(args: &Value) -> bool {
let Some(command) = crate::tools::command_args::raw_command_text(args) else {
return false;
};
if crate::tools::command_args::contains_dynamic_shell_syntax(&command) {
return false;
}
let segments =
static_shell_command_words(&command).or_else(|| static_shell_command_words_with_output_plumbing(&command));
let Some(segments) = segments else {
return false;
};
if segments.is_empty() {
return false;
}
let mut saw_verification = false;
for words in &segments {
if is_verification_invocation(words) {
saw_verification = true;
} else if !command_words_are_readonly(words) {
return false;
}
}
saw_verification
}
fn has_logical_sequencing(words: &[String]) -> bool {
words.iter().any(|word| matches!(word.as_str(), "&&" | "||" | ";"))
}
fn split_top_level_pipes(command: &str) -> Option<Vec<&str>> {
if !command.contains('|') {
return None;
}
let mut stages = Vec::new();
let mut start = 0;
let mut saw_pipe = false;
let mut in_single_quote = false;
let mut in_double_quote = false;
let mut chars = command.char_indices().peekable();
while let Some((index, character)) = chars.next() {
if character == '\\' && !in_single_quote {
chars.next();
continue;
}
if character == '\'' && !in_double_quote {
in_single_quote = !in_single_quote;
continue;
}
if character == '"' && !in_single_quote {
in_double_quote = !in_double_quote;
continue;
}
if in_single_quote || in_double_quote {
continue;
}
if character == '|' {
saw_pipe = true;
stages.push(&command[start..index]);
start = index + character.len_utf8();
}
}
if !saw_pipe {
return None;
}
stages.push(&command[start..]);
Some(stages)
}
fn is_pure_truncation_stage(stage: &str) -> bool {
let trimmed = stage.trim();
if trimmed.is_empty() {
return false;
}
let mut in_single_quote = false;
let mut in_double_quote = false;
let mut chars = trimmed.chars().peekable();
while let Some(character) = chars.next() {
if character == '\\' && !in_single_quote {
chars.next();
continue;
}
if character == '\'' && !in_double_quote {
in_single_quote = !in_single_quote;
continue;
}
if character == '"' && !in_single_quote {
in_double_quote = !in_double_quote;
continue;
}
if in_single_quote || in_double_quote {
continue;
}
if matches!(character, ';' | '&' | '<' | '>' | '\n' | '|') {
return false;
}
}
let words = match shell_words::split(trimmed) {
Ok(words) if !words.is_empty() => words,
_ => return false,
};
let program = Path::new(&words[0])
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(&words[0])
.to_ascii_lowercase();
matches!(program.as_str(), "head" | "tail")
}
pub fn rewrite_truncation_only_verifier(args: &Value) -> Option<String> {
use crate::config::constants::tools as tool_names;
let command = crate::tools::command_args::raw_command_text(args)?;
if !shell_command_is_admitted_verification_attempt(args) {
return None;
}
let stages = split_top_level_pipes(&command)?;
let (head, tails) = stages.split_first()?;
let head = head.trim();
if head.is_empty() || tails.iter().any(|stage| !is_pure_truncation_stage(stage)) {
return None;
}
let head_args = serde_json::json!({"cmd": head});
if !matches!(classify_shell_activity(tool_names::EXEC_COMMAND, &head_args), ShellActivity::Verification) {
return None;
}
Some(head.to_string())
}
fn is_known_inspection(words: &[String]) -> bool {
if words
.iter()
.any(|word| matches!(word.as_str(), ">" | ">>" | "|" | "&&" | ";" | "||"))
{
return false;
}
command_words_are_readonly(words)
}
fn classify_provable_shell_sequence(command: &str) -> Option<ShellActivity> {
let (segments, has_output_plumbing) = if let Some(segments) = static_shell_command_words(command) {
(segments, false)
} else {
(static_shell_command_words_with_output_plumbing(command)?, true)
};
if has_output_plumbing && segments.len() != 1 {
return None;
}
let has_multiple_segments = segments.len() > 1;
let mut saw_verification = false;
for words in segments {
if is_verification_invocation(&words) {
saw_verification = true;
} else if !command_words_are_readonly(&words) {
return None;
}
}
if has_output_plumbing && !saw_verification {
return None;
}
if saw_verification && has_multiple_segments {
if shell_uses_only_and_chaining(command) {
return Some(ShellActivity::Verification);
}
return Some(ShellActivity::Mutation);
}
Some(if saw_verification {
ShellActivity::Verification
} else {
ShellActivity::Inspection
})
}
fn has_shell_sequence(command: &str) -> bool {
static_shell_command_words(command).is_none_or(|segments| segments.len() > 1)
}
fn shell_uses_only_and_chaining(command: &str) -> bool {
let chars: Vec<char> = command.chars().collect();
let mut index = 0;
let mut in_single_quote = false;
let mut in_double_quote = false;
while index < chars.len() {
let character = chars[index];
if character == '\\' && !in_single_quote && index + 1 < chars.len() {
index += 2;
continue;
}
if character == '\'' && !in_double_quote {
in_single_quote = !in_single_quote;
index += 1;
continue;
}
if character == '"' && !in_single_quote {
in_double_quote = !in_double_quote;
index += 1;
continue;
}
if in_single_quote || in_double_quote {
index += 1;
continue;
}
match character {
';' | '\n' | '|' => return false,
'&' => {
let next_is_and = chars.get(index + 1) == Some(&'&');
if !next_is_and {
return false;
}
let third = chars.get(index + 2);
if third == Some(&'&') || third == Some(&'|') {
return false;
}
index += 2;
continue;
}
_ => {}
}
index += 1;
}
true
}
#[must_use]
pub fn classify_shell_activity(tool_name: &str, args: &Value) -> ShellActivity {
let command = crate::tools::command_args::raw_command_text(args);
let words = crate::tools::command_args::command_words(args).ok().flatten();
let has_unclassified_shell_sequence = command.as_deref().is_some_and(has_shell_sequence);
if let Some(activity) = command.as_deref().and_then(classify_provable_shell_sequence) {
return activity;
}
let intent = super::classify_tool_intent(tool_name, args);
if !has_unclassified_shell_sequence && words.as_deref().is_some_and(is_known_inspection) {
return ShellActivity::Inspection;
}
let starts_with_verification = words.as_deref().is_some_and(is_verification_invocation);
let contains_verification =
starts_with_verification || command.as_deref().is_some_and(contains_verification_invocation);
if !intent.mutating {
return if contains_verification {
ShellActivity::Verification
} else {
ShellActivity::Inspection
};
}
if starts_with_verification
&& !has_unclassified_shell_sequence
&& !words.as_deref().is_some_and(has_logical_sequencing)
{
ShellActivity::Verification
} else {
ShellActivity::Mutation
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::config::constants::tools;
use crate::tools::tool_intent::is_readonly_command_session_command;
fn exec_command(command: &str) -> Value {
json!({"cmd": command})
}
#[test]
fn admitted_verification_attempt_allows_truncation_but_blocks_smuggled_mutations() {
for command in [
"cargo check --locked 2>&1 | head -c 4000",
"cargo check --locked",
"cargo nextest run 2>&1 | head -c 4000",
] {
assert!(
shell_command_is_admitted_verification_attempt(&exec_command(command)),
"expected admission: {command}"
);
}
for command in [
"cargo check && rm -rf target",
"cargo check; rm foo.txt",
"cargo check || rm foo.txt",
"cargo check && cargo test && rm foo.txt",
"sed -i '' 's/old/new/' README.md",
"echo $(date)",
"cargo check > build.log && rm foo.txt",
] {
assert!(
!shell_command_is_admitted_verification_attempt(&exec_command(command)),
"expected block: {command}"
);
}
assert!(!shell_command_is_admitted_verification_attempt(&json!({})));
}
#[test]
fn logged_compound_inspection_commands_are_not_mutations() {
for command in [
"cat README.md && printf '\\n--- git status ---\\n' && git status --short",
"wc -l README.md; rg -n '^#' README.md",
"git diff --stat; find docs -maxdepth 2 -type f | sort | head -40",
] {
assert_eq!(
classify_shell_activity(tools::EXEC_COMMAND, &exec_command(command)),
ShellActivity::Inspection,
"{command}"
);
}
}
#[cfg(unix)]
#[test]
fn captured_read_commands_with_output_suppression_are_inspection() {
for command in [
r#"sed -n '1,180p' README.md; sed -n '280,350p' README.md; sed -n '389,411p' README.md; printf '\n--- repo metadata ---\n'; git log -1 --format='%h %s'; sed -n '1,100p' Cargo.toml; rg -n '^version\s*=|rust-version|workspace\.package' Cargo.toml crates -g Cargo.toml | head -40"#,
r#"sed -n '1,120p' crates/codegen/vtcode-core/src/tools/tool_intent/activity.rs; printf '\n--- readonly policy ---\n'; rg -n 'READONLY_UNIFIED_EXEC_COMMANDS|command_words_are_readonly' crates/codegen/vtcode-core/src/tools/tool_intent/readonly.rs; printf '\n--- recent commits ---\n'; git log -5 --oneline; printf '\n--- command arguments ---\n'; sed -n '1,180p' crates/codegen/vtcode-core/src/tools/command_args.rs"#,
r###"git diff --stat; find docs -maxdepth 2 -type f | sort | head -40; rg -n "vtcode init|vtcode models|full-auto|run-debug|cargo install" docs/user-guide docs/installation docs/development 2>/dev/null | head -50"###,
] {
assert_eq!(
classify_shell_activity(tools::EXEC_COMMAND, &exec_command(command)),
ShellActivity::Inspection,
"{command}"
);
}
}
#[cfg(unix)]
#[test]
fn printf_output_safety_guards_remain_mutations() {
for command in [
"printf 'captured output\\n' > output.txt",
"printf '%s\\n' \"$(git status --short)\"",
"printf '%s\\n' `git status --short`",
"printf '\\n--- inspection ---\\n' && rm output.txt",
] {
let args = exec_command(command);
assert_eq!(classify_shell_activity(tools::EXEC_COMMAND, &args), ShellActivity::Mutation, "{command}");
assert!(!is_readonly_command_session_command(&args), "unexpected readonly command: {command}");
}
}
#[test]
fn git_diff_check_chain_remains_inspection() {
assert_eq!(
classify_shell_activity(
tools::EXEC_COMMAND,
&exec_command("git diff --check && git status --short && git diff --stat"),
),
ShellActivity::Inspection
);
}
#[test]
fn verification_detection_skips_environment_prefixes() {
for command in [
"env RUSTFLAGS=-Dwarnings cargo check",
"RUSTFLAGS=-Dwarnings env cargo check",
"env -u PATH cargo check",
"env -C /tmp cargo check",
] {
assert_eq!(
classify_shell_activity(tools::EXEC_COMMAND, &exec_command(command)),
ShellActivity::Verification,
"{command}"
);
}
}
#[test]
fn ambiguous_or_mutating_compounds_remain_mutations() {
for command in [
"git diff --stat; python3 -c 'open(\"out\", \"w\").write(\"x\")'",
"cat README.md; sed -i '' 's/a/b/' README.md",
"sed --in-place= README.md",
"git diff --output=out",
"git diff '--output=out'",
"git diff -o out",
"git diff -oout",
"git log --output=out",
"git show --textconv",
"git -C /external/repo=alt status",
"find . -fprint output.txt",
"find . -fprintf output.txt '%p'",
"rg --hostname-bin sh pattern",
"rg --search-zip pattern",
"rg -z pattern",
"sort -o generated.txt README.md",
"sort --compress-program=sh README.md",
"date -s now",
"awk -i inplace '{print}' README.md",
"sed -n 's/a/b/e' README.md",
"fd --exec sh -c 'touch output'",
"tree -o output.txt",
"ast-grep -r 'README.md'",
"sed -n -fmalicious.sed -e '1p' src/main.rs",
"sed -I '' 's/a/b/' src/main.rs",
"sed -n '1p\nw leaked.txt' src/main.rs",
"cargo check & rm output",
"cargo check > build.log | rm output",
"cargo check | head -40 > build.log",
"cargo check | echo x > output.log",
"cargo check | head -40",
"cargo check > build.log &",
"env -S 'cargo check'",
"echo x > output.log && cargo check",
"cargo check < build-input.log",
"cat README.md > copied.txt",
"git diff --check; rm output",
"cat README.md\nrm output",
] {
assert_eq!(
classify_shell_activity(tools::EXEC_COMMAND, &exec_command(command)),
ShellActivity::Mutation,
"{command}"
);
}
}
#[test]
fn quoted_output_text_does_not_change_inspection_classification() {
for command in ["echo 'git diff --output=out'", "printf 'sort -o out input'"] {
assert_eq!(
classify_shell_activity(tools::EXEC_COMMAND, &exec_command(command)),
ShellActivity::Inspection,
"{command}"
);
}
}
#[test]
fn cargo_fmt_check_is_verification_but_plain_fmt_is_not() {
for command in [
"cargo fmt --check",
"cargo fmt --all -- --check",
"cargo fmt -- --check",
] {
assert_eq!(
classify_shell_activity(tools::EXEC_COMMAND, &exec_command(command)),
ShellActivity::Verification,
"{command}"
);
assert!(
shell_command_is_admitted_verification_attempt(&exec_command(command)),
"expected admission: {command}"
);
}
assert_eq!(classify_shell_activity(tools::EXEC_COMMAND, &exec_command("cargo fmt")), ShellActivity::Mutation);
assert!(!shell_command_is_admitted_verification_attempt(&exec_command("cargo fmt")));
}
#[test]
fn pure_and_chained_verifiers_are_verification() {
for command in [
"cargo fmt --all -- --check && cargo check --locked",
"cargo check --locked && cargo nextest run --locked -p vtcode-ui",
"cargo check --locked && cargo clippy --locked -p vtcode-ui -- -D warnings",
"git diff --check && cargo check --locked",
] {
assert_eq!(
classify_shell_activity(tools::EXEC_COMMAND, &exec_command(command)),
ShellActivity::Verification,
"{command}"
);
assert!(
shell_command_is_admitted_verification_attempt(&exec_command(command)),
"expected admission: {command}"
);
}
}
#[test]
fn non_and_chained_verifiers_remain_mutations() {
for command in [
"cargo check --locked; cargo nextest run --locked -p vtcode-ui",
"cargo check --locked || cargo nextest run --locked -p vtcode-ui",
"cargo check --locked | head -40",
"cargo check --locked && cargo nextest run --locked -p vtcode-ui | head -40",
"cargo check --locked &",
] {
assert_eq!(
classify_shell_activity(tools::EXEC_COMMAND, &exec_command(command)),
ShellActivity::Mutation,
"{command}"
);
}
}
#[test]
fn expanded_verifiers_classify_as_verification() {
for command in [
"bun test",
"bun run test",
"deno lint",
"deno check mod.ts",
"make test",
"make lint",
"just verify",
"ruff check src/",
"ruff format --check",
"tsc --noEmit",
"eslint src/",
"python3 -m pytest",
"uv run pytest",
"npm run lint",
] {
assert_eq!(
classify_shell_activity(tools::EXEC_COMMAND, &exec_command(command)),
ShellActivity::Verification,
"{command}"
);
}
for command in [
"make clean",
"make test clean",
"just fmt",
"tsc",
"eslint --fix src/",
"ruff format src/",
"bun install",
"uv sync",
] {
assert_eq!(
classify_shell_activity(tools::EXEC_COMMAND, &exec_command(command)),
ShellActivity::Mutation,
"{command}"
);
}
}
#[test]
fn escaped_quotes_fail_closed_instead_of_hiding_operators() {
assert!(!shell_uses_only_and_chaining("cargo check --locked \\\"; echo ok"));
assert!(!shell_uses_only_and_chaining("echo \\\" ; cargo check --locked && echo done"));
assert!(shell_uses_only_and_chaining("echo \"a\\\"b\" && cargo check --locked"));
assert!(shell_uses_only_and_chaining("echo \"path\" && cargo fmt --check"));
assert!(shell_uses_only_and_chaining("echo 'a\\b' && cargo check --locked"));
}
#[test]
fn default_verifier_prefers_manifest_priority_order() {
let dir = tempfile::tempdir().expect("tempdir");
assert_eq!(default_verifier_for_workspace(dir.path()), None);
std::fs::write(dir.path().join("justfile"), "test:\n\techo ok\n").expect("justfile");
assert_eq!(default_verifier_for_workspace(dir.path()).as_deref(), Some("just test"));
std::fs::write(dir.path().join("pyproject.toml"), "[tool.pytest]\n").expect("pyproject");
assert_eq!(default_verifier_for_workspace(dir.path()).as_deref(), Some("pytest -q"));
std::fs::write(dir.path().join("package.json"), r#"{"scripts":{"test":"vitest"}}"#).expect("package.json");
assert_eq!(default_verifier_for_workspace(dir.path()).as_deref(), Some("npm test"));
std::fs::write(dir.path().join("go.mod"), "module example\n").expect("go.mod");
assert_eq!(default_verifier_for_workspace(dir.path()).as_deref(), Some("go test ./..."));
std::fs::write(dir.path().join("Cargo.toml"), "[package]\nname=\"x\"\n").expect("Cargo.toml");
assert_eq!(default_verifier_for_workspace(dir.path()).as_deref(), Some("cargo check --locked"));
}
#[test]
fn default_verifier_reads_npm_script_fallbacks() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("package.json"), r#"{"scripts":{"lint":"eslint ."}}"#).expect("package.json");
assert_eq!(default_verifier_for_workspace(dir.path()).as_deref(), Some("npm run lint"));
}
#[test]
fn verification_recovery_directive_names_concrete_command() {
let directive = verification_recovery_directive(Some("go test ./..."), 1, 2);
assert!(directive.contains("go test ./..."));
assert!(directive.contains("1/2"));
assert!(directive.contains("max_output_tokens"));
assert!(directive.contains("elided at execution"));
let fallback = verification_recovery_directive(None, 2, 2);
assert!(fallback.contains("cargo check --locked"));
assert!(fallback.contains("2/2"));
}
#[test]
fn truncation_only_verifier_rewrites_to_standalone_prefix() {
for (command, expected) in [
("cargo check --locked 2>&1 | head -c 4000", "cargo check --locked 2>&1"),
("cargo check --locked -p vtcode 2>&1 | tail -20", "cargo check --locked -p vtcode 2>&1"),
("cargo nextest run 2>&1 | tail -15", "cargo nextest run 2>&1"),
(
"cargo check --locked && cargo nextest run --locked -p vtcode | tail -5",
"cargo check --locked && cargo nextest run --locked -p vtcode",
),
("cargo check | tail -5 | head -20", "cargo check"),
("RUSTFLAGS=-Dwarnings cargo check | head -20", "RUSTFLAGS=-Dwarnings cargo check"),
("cargo check > build.log | tail -5", "cargo check > build.log"),
] {
assert_eq!(
rewrite_truncation_only_verifier(&exec_command(command)).as_deref(),
Some(expected),
"expected rewrite: {command}"
);
}
}
#[test]
fn non_truncation_pipelines_and_mutations_are_never_rewritten() {
for command in [
"cargo check | grep error",
"cargo check | wc -l",
"cargo check | sort | uniq",
"cargo check && rm -rf target | tail -5",
"cargo check; git status | tail -5",
"cargo check || cargo test | tail -5",
"cargo check | tail -5; rm foo.txt",
"cargo check | tail &",
"cargo check",
"echo done | tail -5",
"rg -n 'pattern' src | head -20",
"echo $(date) | tail -5",
] {
assert_eq!(rewrite_truncation_only_verifier(&exec_command(command)), None, "must not rewrite: {command}");
}
assert_eq!(
rewrite_truncation_only_verifier(&serde_json::json!({"command": ["cargo", "check", "|", "tail"]})),
None,
"array-form commands keep today's behavior"
);
assert_eq!(rewrite_truncation_only_verifier(&serde_json::json!({})), None);
}
}