From ca90f4220032058fddc6dbe4c10f7cd13706234a Mon Sep 17 00:00:00 2001 From: Alex Yankov Date: Fri, 22 May 2026 23:59:14 -0400 Subject: [PATCH 1/4] feat: add non-interactive blocking for allow and deny commands --- README.md | 6 +++++ cargo_crap_baseline.json | 20 +++++++++++++++-- src/commands/allow.rs | 11 +++++++++ src/commands/deny.rs | 11 +++++++++ src/commands/mod.rs | 33 +++++++++++++++++++++++++++ tests/docker_noinit/test_noinit.sh | 36 ++++++++++++++++++++++++++++++ 6 files changed, 115 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 58d5e28..c03bd42 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,16 @@ Or use `dela run` for subshell execution: $ dela run build ``` + +### Allowlist.toml +The allowlist is a TOML file located at `~/.config/dela/allowlist.toml`. It stores allow and deny rules at folder, file, and task level. It gets updated when you either run a task in a new folder for the first time, or when you run `dela allow ` and `dela deny ` commands explicitly. + ## MCP Server Dela includes an [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that allows AI assistants and editors to discover and execute tasks programmatically. +The mcp executed tasks need to be already on the allowlist. The mcp server respects the allowlist, but does not give agents tools to modify it. + ### Setting Up MCP in Your Editor ```sh diff --git a/cargo_crap_baseline.json b/cargo_crap_baseline.json index c559e38..7f8f534 100644 --- a/cargo_crap_baseline.json +++ b/cargo_crap_baseline.json @@ -207,8 +207,8 @@ "function": "evaluate_task_against_allowlist", "line": 75, "cyclomatic": 17.0, - "coverage": 100.0, - "crap": 17.0 + "coverage": 96.42857142857143, + "crap": 17.013165087463555 }, { "file": "./src/commands/allow_command.rs", @@ -770,6 +770,14 @@ "coverage": 100.0, "crap": 5.0 }, + { + "file": "./src/commands/mod.rs", + "function": "gate_non_interactive", + "line": 18, + "cyclomatic": 4.0, + "coverage": 70.0, + "crap": 4.432 + }, { "file": "./src/task_discovery/turbo.rs", "function": "build_turbo_package_config_index", @@ -1106,6 +1114,14 @@ "coverage": 100.0, "crap": 3.0 }, + { + "file": "./src/commands/mod.rs", + "function": "should_block", + "line": 14, + "cyclomatic": 3.0, + "coverage": 100.0, + "crap": 3.0 + }, { "file": "./src/commands/mcp.rs", "function": "Editor::dela_marker", diff --git a/src/commands/allow.rs b/src/commands/allow.rs index ed5d904..ac1826a 100644 --- a/src/commands/allow.rs +++ b/src/commands/allow.rs @@ -5,6 +5,8 @@ use std::env; /// Executes the 'dela allow' command to add a specific task to the allowlist. pub fn execute(task_name: &str) -> anyhow::Result<()> { + super::gate_non_interactive("dela allow"); + let current_dir = env::current_dir() .map_err(|e| anyhow::anyhow!("Failed to get current directory: {}", e))?; let discovered = task_discovery::discover_tasks(¤t_dir); @@ -57,6 +59,9 @@ mod tests { impl TestEnvGuard { fn new() -> Self { + unsafe { + std::env::set_var("DELA_FORCE_INTERACTIVE", "1"); + } let original_cwd = env::current_dir().unwrap_or_else(|_| std::env::temp_dir()); let (project_dir, home_dir) = setup_test_env(); Self { @@ -67,6 +72,9 @@ mod tests { } fn new_uninitialized() -> Self { + unsafe { + std::env::set_var("DELA_FORCE_INTERACTIVE", "1"); + } let original_cwd = env::current_dir().unwrap_or_else(|_| std::env::temp_dir()); // Create a temp dir for HOME but don't create the dela config directory let home_dir = TempDir::new().expect("Failed to create temp HOME directory"); @@ -85,6 +93,9 @@ mod tests { impl Drop for TestEnvGuard { fn drop(&mut self) { + unsafe { + std::env::remove_var("DELA_FORCE_INTERACTIVE"); + } let _ = env::set_current_dir(&self.original_cwd); reset_to_real_environment(); } diff --git a/src/commands/deny.rs b/src/commands/deny.rs index f47eb37..b3f66e8 100644 --- a/src/commands/deny.rs +++ b/src/commands/deny.rs @@ -5,6 +5,8 @@ use std::env; /// Executes the 'dela deny' command to add a specific task definition file to the denylist. pub fn execute(task_name: &str) -> anyhow::Result<()> { + super::gate_non_interactive("dela deny"); + let current_dir = env::current_dir() .map_err(|e| anyhow::anyhow!("Failed to get current directory: {}", e))?; let discovered = task_discovery::discover_tasks(¤t_dir); @@ -57,6 +59,9 @@ mod tests { impl TestEnvGuard { fn new() -> Self { + unsafe { + std::env::set_var("DELA_FORCE_INTERACTIVE", "1"); + } let original_cwd = env::current_dir().unwrap_or_else(|_| std::env::temp_dir()); let (project_dir, home_dir) = setup_test_env(); Self { @@ -67,6 +72,9 @@ mod tests { } fn new_uninitialized() -> Self { + unsafe { + std::env::set_var("DELA_FORCE_INTERACTIVE", "1"); + } let original_cwd = env::current_dir().unwrap_or_else(|_| std::env::temp_dir()); // Create a temp dir for HOME but don't create the dela config directory let home_dir = TempDir::new().expect("Failed to create temp HOME directory"); @@ -85,6 +93,9 @@ mod tests { impl Drop for TestEnvGuard { fn drop(&mut self) { + unsafe { + std::env::remove_var("DELA_FORCE_INTERACTIVE"); + } let _ = env::set_current_dir(&self.original_cwd); reset_to_real_environment(); } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 3803a98..de0ec9d 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -8,3 +8,36 @@ pub mod list; pub mod mcp; pub mod run; pub mod run_command; + +use std::io::IsTerminal; + +pub(crate) fn should_block(is_terminal: bool, is_test: bool, force_interactive: bool) -> bool { + !is_terminal && !is_test && !force_interactive +} + +pub(crate) fn gate_non_interactive(command_name: &str) { + let is_terminal = std::io::stdout().is_terminal() && std::io::stdin().is_terminal(); + let is_test = std::env::var("RUST_TEST_THREADS").is_ok() || std::env::var("CARGO_TEST").is_ok(); + let force_interactive = std::env::var("DELA_FORCE_INTERACTIVE").is_ok(); + + if should_block(is_terminal, is_test, force_interactive) { + eprintln!( + "'{}' should only be run by human users directly, and not by scripts or agents.", + command_name + ); + std::process::exit(1); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_should_block_non_interactive() { + assert!(!should_block(true, false, false)); // Interactive session + assert!(should_block(false, false, false)); // Non-interactive session + assert!(!should_block(false, true, false)); // In tests + assert!(!should_block(false, false, true)); // Force interactive override + } +} diff --git a/tests/docker_noinit/test_noinit.sh b/tests/docker_noinit/test_noinit.sh index 14d12ea..f659ef8 100755 --- a/tests/docker_noinit/test_noinit.sh +++ b/tests/docker_noinit/test_noinit.sh @@ -840,6 +840,7 @@ fi # Test 31: Test 'dela allow' command functionality echo "\nTest 31: Testing 'dela allow' command functionality" +export DELA_FORCE_INTERACTIVE=1 echo "Initial allowlist contents:" cat /home/testuser/.config/dela/allowlist.toml @@ -961,7 +962,42 @@ else echo "${GREEN}✓ dela deny test (ambiguous) failed as expected${NC}" fi +# Test 33: Test non-interactive blocking of 'dela allow' and 'dela deny' +echo "\nTest 33: Testing non-interactive blocking of 'dela allow' and 'dela deny'" +unset DELA_FORCE_INTERACTIVE + +# Running dela allow should get blocked and print the error to stderr +exit_code=0 +output=$(dela allow print-args 2>&1) || exit_code=$? +if [ $exit_code -eq 0 ]; then + echo "${RED}✗ dela allow print-args did not fail in non-interactive session${NC}" + exit 1 +fi +if echo "$output" | grep -q "'dela allow' should only be run by human users directly, and not by scripts or agents."; then + echo "${GREEN}✓ dela allow was blocked in non-interactive session as expected${NC}" +else + echo "${RED}✗ dela allow was not blocked with the expected message${NC}" + echo "Output: $output" + exit 1 +fi + +# Running dela deny should get blocked and print the error to stderr +exit_code=0 +output=$(dela deny print-args 2>&1) || exit_code=$? +if [ $exit_code -eq 0 ]; then + echo "${RED}✗ dela deny print-args did not fail in non-interactive session${NC}" + exit 1 +fi +if echo "$output" | grep -q "'dela deny' should only be run by human users directly, and not by scripts or agents."; then + echo "${GREEN}✓ dela deny was blocked in non-interactive session as expected${NC}" +else + echo "${RED}✗ dela deny was not blocked with the expected message${NC}" + echo "Output: $output" + exit 1 +fi + # Clean up test files rm -f duplicate_test.json duplicate_test.mk list_output.txt list_output_long.txt echo "\n${GREEN}All non-init tests completed successfully!${NC}" + From 3deaf12c4b33e116b7cb52f76f8a916af897fb6c Mon Sep 17 00:00:00 2001 From: Alex Yankov Date: Sat, 23 May 2026 11:38:33 -0400 Subject: [PATCH 2/4] Use script instead of DELA_FORCE_INTERACTIVE --- src/commands/allow.rs | 24 +++++++++--------------- src/commands/deny.rs | 24 +++++++++--------------- src/commands/mod.rs | 30 ++++++------------------------ src/main.rs | 10 ++++------ tests/docker_noinit/Dockerfile | 3 ++- tests/docker_noinit/test_noinit.sh | 16 +++++++--------- 6 files changed, 37 insertions(+), 70 deletions(-) diff --git a/src/commands/allow.rs b/src/commands/allow.rs index ac1826a..10ad864 100644 --- a/src/commands/allow.rs +++ b/src/commands/allow.rs @@ -5,8 +5,11 @@ use std::env; /// Executes the 'dela allow' command to add a specific task to the allowlist. pub fn execute(task_name: &str) -> anyhow::Result<()> { - super::gate_non_interactive("dela allow"); + super::gate_non_interactive("dela allow")?; + execute_inner(task_name) +} +fn execute_inner(task_name: &str) -> anyhow::Result<()> { let current_dir = env::current_dir() .map_err(|e| anyhow::anyhow!("Failed to get current directory: {}", e))?; let discovered = task_discovery::discover_tasks(¤t_dir); @@ -59,9 +62,6 @@ mod tests { impl TestEnvGuard { fn new() -> Self { - unsafe { - std::env::set_var("DELA_FORCE_INTERACTIVE", "1"); - } let original_cwd = env::current_dir().unwrap_or_else(|_| std::env::temp_dir()); let (project_dir, home_dir) = setup_test_env(); Self { @@ -72,9 +72,6 @@ mod tests { } fn new_uninitialized() -> Self { - unsafe { - std::env::set_var("DELA_FORCE_INTERACTIVE", "1"); - } let original_cwd = env::current_dir().unwrap_or_else(|_| std::env::temp_dir()); // Create a temp dir for HOME but don't create the dela config directory let home_dir = TempDir::new().expect("Failed to create temp HOME directory"); @@ -93,9 +90,6 @@ mod tests { impl Drop for TestEnvGuard { fn drop(&mut self) { - unsafe { - std::env::remove_var("DELA_FORCE_INTERACTIVE"); - } let _ = env::set_current_dir(&self.original_cwd); reset_to_real_environment(); } @@ -139,7 +133,7 @@ test: ## Running tests let guard = TestEnvGuard::new(); env::set_current_dir(&guard.project_dir).expect("Failed to change directory"); - let result = execute("test"); + let result = execute_inner("test"); assert!(result.is_ok(), "Should succeed for a single task"); // Verify it was added to the allowlist @@ -155,7 +149,7 @@ test: ## Running tests let guard = TestEnvGuard::new(); env::set_current_dir(&guard.project_dir).expect("Failed to change directory"); - let result = execute("nonexistent"); + let result = execute_inner("nonexistent"); assert!(result.is_err(), "Should fail for nonexistent task"); assert_eq!( result.unwrap_err().to_string(), @@ -180,7 +174,7 @@ test: ## Running tests .write_all(makefile_content.as_bytes()) .expect("Failed to write Makefile"); - let result = execute("test"); + let result = execute_inner("test"); assert!(result.is_err(), "Should fail when dela is not initialized"); assert_eq!( result.unwrap_err().to_string(), @@ -214,7 +208,7 @@ test: ## Running tests .unwrap(); // Ambiguous task name 'test' should fail - let result = execute("test"); + let result = execute_inner("test"); assert!(result.is_err(), "Should fail for ambiguous task name"); assert!( result @@ -224,7 +218,7 @@ test: ## Running tests ); // Suffixed/disambiguated task name should succeed - let result = execute("test-m"); + let result = execute_inner("test-m"); assert!(result.is_ok(), "Should succeed with disambiguated name"); let allowlist_content = diff --git a/src/commands/deny.rs b/src/commands/deny.rs index b3f66e8..2ed6beb 100644 --- a/src/commands/deny.rs +++ b/src/commands/deny.rs @@ -5,8 +5,11 @@ use std::env; /// Executes the 'dela deny' command to add a specific task definition file to the denylist. pub fn execute(task_name: &str) -> anyhow::Result<()> { - super::gate_non_interactive("dela deny"); + super::gate_non_interactive("dela deny")?; + execute_inner(task_name) +} +fn execute_inner(task_name: &str) -> anyhow::Result<()> { let current_dir = env::current_dir() .map_err(|e| anyhow::anyhow!("Failed to get current directory: {}", e))?; let discovered = task_discovery::discover_tasks(¤t_dir); @@ -59,9 +62,6 @@ mod tests { impl TestEnvGuard { fn new() -> Self { - unsafe { - std::env::set_var("DELA_FORCE_INTERACTIVE", "1"); - } let original_cwd = env::current_dir().unwrap_or_else(|_| std::env::temp_dir()); let (project_dir, home_dir) = setup_test_env(); Self { @@ -72,9 +72,6 @@ mod tests { } fn new_uninitialized() -> Self { - unsafe { - std::env::set_var("DELA_FORCE_INTERACTIVE", "1"); - } let original_cwd = env::current_dir().unwrap_or_else(|_| std::env::temp_dir()); // Create a temp dir for HOME but don't create the dela config directory let home_dir = TempDir::new().expect("Failed to create temp HOME directory"); @@ -93,9 +90,6 @@ mod tests { impl Drop for TestEnvGuard { fn drop(&mut self) { - unsafe { - std::env::remove_var("DELA_FORCE_INTERACTIVE"); - } let _ = env::set_current_dir(&self.original_cwd); reset_to_real_environment(); } @@ -139,7 +133,7 @@ test: ## Running tests let guard = TestEnvGuard::new(); env::set_current_dir(&guard.project_dir).expect("Failed to change directory"); - let result = execute("test"); + let result = execute_inner("test"); assert!(result.is_ok(), "Should succeed for a single task"); // Verify it was added to the allowlist with Deny scope and task name @@ -155,7 +149,7 @@ test: ## Running tests let guard = TestEnvGuard::new(); env::set_current_dir(&guard.project_dir).expect("Failed to change directory"); - let result = execute("nonexistent"); + let result = execute_inner("nonexistent"); assert!(result.is_err(), "Should fail for nonexistent task"); assert_eq!( result.unwrap_err().to_string(), @@ -180,7 +174,7 @@ test: ## Running tests .write_all(makefile_content.as_bytes()) .expect("Failed to write Makefile"); - let result = execute("test"); + let result = execute_inner("test"); assert!(result.is_err(), "Should fail when dela is not initialized"); assert_eq!( result.unwrap_err().to_string(), @@ -214,7 +208,7 @@ test: ## Running tests .unwrap(); // Ambiguous task name 'test' should fail - let result = execute("test"); + let result = execute_inner("test"); assert!(result.is_err(), "Should fail for ambiguous task name"); assert!( result @@ -224,7 +218,7 @@ test: ## Running tests ); // Suffixed/disambiguated task name should succeed - let result = execute("test-m"); + let result = execute_inner("test-m"); assert!(result.is_ok(), "Should succeed with disambiguated name"); let allowlist_content = diff --git a/src/commands/mod.rs b/src/commands/mod.rs index de0ec9d..8108e30 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -11,33 +11,15 @@ pub mod run_command; use std::io::IsTerminal; -pub(crate) fn should_block(is_terminal: bool, is_test: bool, force_interactive: bool) -> bool { - !is_terminal && !is_test && !force_interactive -} - -pub(crate) fn gate_non_interactive(command_name: &str) { +/// Returns an error if the current session is non-interactive (no TTY). +/// This prevents scripts and agents from running `dela allow` / `dela deny`. +pub(crate) fn gate_non_interactive(command_name: &str) -> anyhow::Result<()> { let is_terminal = std::io::stdout().is_terminal() && std::io::stdin().is_terminal(); - let is_test = std::env::var("RUST_TEST_THREADS").is_ok() || std::env::var("CARGO_TEST").is_ok(); - let force_interactive = std::env::var("DELA_FORCE_INTERACTIVE").is_ok(); - - if should_block(is_terminal, is_test, force_interactive) { - eprintln!( + if !is_terminal { + anyhow::bail!( "'{}' should only be run by human users directly, and not by scripts or agents.", command_name ); - std::process::exit(1); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_should_block_non_interactive() { - assert!(!should_block(true, false, false)); // Interactive session - assert!(should_block(false, false, false)); // Non-interactive session - assert!(!should_block(false, true, false)); // In tests - assert!(!should_block(false, false, true)); // Force interactive override } + Ok(()) } diff --git a/src/main.rs b/src/main.rs index d9df0c2..a816697 100644 --- a/src/main.rs +++ b/src/main.rs @@ -223,13 +223,11 @@ async fn main() { let result = run_command(cli.command).await; if let Err(err) = result { - if err - .to_string() - .starts_with("dela: command or task not found") - { - eprintln!("{}", err); + let msg = err.to_string(); + if msg.starts_with("dela: command or task not found") || msg.starts_with("'dela ") { + eprintln!("{}", msg); } else { - eprintln!("Error: {}", err); + eprintln!("Error: {}", msg); } std::process::exit(1); } diff --git a/tests/docker_noinit/Dockerfile b/tests/docker_noinit/Dockerfile index 89bc142..b0e3700 100644 --- a/tests/docker_noinit/Dockerfile +++ b/tests/docker_noinit/Dockerfile @@ -16,7 +16,8 @@ RUN apk add --no-cache \ npm \ maven \ gradle \ - docker-cli && \ + docker-cli \ + util-linux && \ apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/v3.21/community go-task # symlink go-task to task diff --git a/tests/docker_noinit/test_noinit.sh b/tests/docker_noinit/test_noinit.sh index f659ef8..eef42ee 100755 --- a/tests/docker_noinit/test_noinit.sh +++ b/tests/docker_noinit/test_noinit.sh @@ -840,12 +840,11 @@ fi # Test 31: Test 'dela allow' command functionality echo "\nTest 31: Testing 'dela allow' command functionality" -export DELA_FORCE_INTERACTIVE=1 echo "Initial allowlist contents:" cat /home/testuser/.config/dela/allowlist.toml # Test dela allow on a single valid task -dela allow print-args >/dev/null 2>&1 || { +script -qc "dela allow print-args" /dev/null >/dev/null 2>&1 || { echo "${RED}✗ dela allow print-args failed${NC}" exit 1 } @@ -866,7 +865,7 @@ else fi # Verify dela allow fails for nonexistent task -if dela allow nonexistent >/dev/null 2>&1; then +if script -qc "dela allow nonexistent" /dev/null >/dev/null 2>&1; then echo "${RED}✗ dela allow nonexistent succeeded but should have failed${NC}" exit 1 else @@ -875,7 +874,7 @@ fi # We have duplicate_test.json and duplicate_test.mk still present in the directory. # So 'test' is ambiguous. -if dela allow test >/dev/null 2>&1; then +if script -qc "dela allow test" /dev/null >/dev/null 2>&1; then echo "${RED}✗ dela allow test (ambiguous) succeeded but should have failed${NC}" exit 1 else @@ -883,7 +882,7 @@ else fi # But dela allow test-m (disambiguated name) should succeed! -dela allow test-m >/dev/null 2>&1 || { +script -qc "dela allow test-m" /dev/null >/dev/null 2>&1 || { echo "${RED}✗ dela allow test-m (disambiguated name) failed${NC}" exit 1 } @@ -893,7 +892,7 @@ echo "${GREEN}✓ dela allow test-m (disambiguated name) succeeded as expected${ echo "\nTest 32: Testing 'dela deny' command functionality" # Deny a single valid task -dela deny another-task >/dev/null 2>&1 || { +script -qc "dela deny another-task" /dev/null >/dev/null 2>&1 || { echo "${RED}✗ dela deny another-task failed${NC}" exit 1 } @@ -945,7 +944,7 @@ else fi # Verify dela deny fails for nonexistent task -if dela deny nonexistent >/dev/null 2>&1; then +if script -qc "dela deny nonexistent" /dev/null >/dev/null 2>&1; then echo "${RED}✗ dela deny nonexistent succeeded but should have failed${NC}" exit 1 else @@ -955,7 +954,7 @@ fi # Verify dela deny fails for ambiguous tasks # We have duplicate_test.json and duplicate_test.mk still present in the directory. # So 'test' is ambiguous. -if dela deny test >/dev/null 2>&1; then +if script -qc "dela deny test" /dev/null >/dev/null 2>&1; then echo "${RED}✗ dela deny test (ambiguous) succeeded but should have failed${NC}" exit 1 else @@ -964,7 +963,6 @@ fi # Test 33: Test non-interactive blocking of 'dela allow' and 'dela deny' echo "\nTest 33: Testing non-interactive blocking of 'dela allow' and 'dela deny'" -unset DELA_FORCE_INTERACTIVE # Running dela allow should get blocked and print the error to stderr exit_code=0 From 0e76dbd8bd6135b5982bedd7ed43eba29d56c7c6 Mon Sep 17 00:00:00 2001 From: Alex Yankov Date: Sat, 23 May 2026 12:07:47 -0400 Subject: [PATCH 3/4] fixes --- cargo_crap_baseline.json | 60 +++++++++++++++++------------- src/commands/mod.rs | 16 ++++++++ tests/docker_noinit/test_noinit.sh | 14 +++---- 3 files changed, 57 insertions(+), 33 deletions(-) diff --git a/cargo_crap_baseline.json b/cargo_crap_baseline.json index 7f8f534..905ac6f 100644 --- a/cargo_crap_baseline.json +++ b/cargo_crap_baseline.json @@ -146,6 +146,14 @@ "coverage": 100.0, "crap": 20.0 }, + { + "file": "./src/main.rs", + "function": "main", + "line": 220, + "cyclomatic": 4.0, + "coverage": 0.0, + "crap": 20.0 + }, { "file": "./src/task_discovery/github_actions.rs", "function": "discover_github_actions_tasks", @@ -207,8 +215,8 @@ "function": "evaluate_task_against_allowlist", "line": 75, "cyclomatic": 17.0, - "coverage": 96.42857142857143, - "crap": 17.013165087463555 + "coverage": 100.0, + "crap": 17.0 }, { "file": "./src/commands/allow_command.rs", @@ -290,14 +298,6 @@ "coverage": 95.0, "crap": 12.018 }, - { - "file": "./src/main.rs", - "function": "main", - "line": 220, - "cyclomatic": 3.0, - "coverage": 0.0, - "crap": 12.0 - }, { "file": "./src/parsers/parse_makefile.rs", "function": "parse", @@ -678,6 +678,14 @@ "file": "./src/commands/allow.rs", "function": "execute", "line": 7, + "cyclomatic": 2.0, + "coverage": 0.0, + "crap": 6.0 + }, + { + "file": "./src/commands/allow.rs", + "function": "execute_inner", + "line": 12, "cyclomatic": 6.0, "coverage": 100.0, "crap": 6.0 @@ -686,6 +694,14 @@ "file": "./src/commands/deny.rs", "function": "execute", "line": 7, + "cyclomatic": 2.0, + "coverage": 0.0, + "crap": 6.0 + }, + { + "file": "./src/commands/deny.rs", + "function": "execute_inner", + "line": 12, "cyclomatic": 6.0, "coverage": 100.0, "crap": 6.0 @@ -770,14 +786,6 @@ "coverage": 100.0, "crap": 5.0 }, - { - "file": "./src/commands/mod.rs", - "function": "gate_non_interactive", - "line": 18, - "cyclomatic": 4.0, - "coverage": 70.0, - "crap": 4.432 - }, { "file": "./src/task_discovery/turbo.rs", "function": "build_turbo_package_config_index", @@ -930,6 +938,14 @@ "coverage": 100.0, "crap": 4.0 }, + { + "file": "./src/commands/mod.rs", + "function": "gate_non_interactive", + "line": 16, + "cyclomatic": 3.0, + "coverage": 75.0, + "crap": 3.140625 + }, { "file": "./src/mcp/server.rs", "function": "DelaMcpServer::send_log", @@ -1114,14 +1130,6 @@ "coverage": 100.0, "crap": 3.0 }, - { - "file": "./src/commands/mod.rs", - "function": "should_block", - "line": 14, - "cyclomatic": 3.0, - "coverage": 100.0, - "crap": 3.0 - }, { "file": "./src/commands/mcp.rs", "function": "Editor::dela_marker", diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 8108e30..f26bcd0 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -23,3 +23,19 @@ pub(crate) fn gate_non_interactive(command_name: &str) -> anyhow::Result<()> { } Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_gate_non_interactive_in_test_env() { + let result = gate_non_interactive("dela allow"); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "'dela allow' should only be run by human users directly, and not by scripts or agents." + ); + } +} + diff --git a/tests/docker_noinit/test_noinit.sh b/tests/docker_noinit/test_noinit.sh index eef42ee..02d3953 100755 --- a/tests/docker_noinit/test_noinit.sh +++ b/tests/docker_noinit/test_noinit.sh @@ -844,7 +844,7 @@ echo "Initial allowlist contents:" cat /home/testuser/.config/dela/allowlist.toml # Test dela allow on a single valid task -script -qc "dela allow print-args" /dev/null >/dev/null 2>&1 || { +script -eqc "dela allow print-args" /dev/null >/dev/null 2>&1 || { echo "${RED}✗ dela allow print-args failed${NC}" exit 1 } @@ -865,7 +865,7 @@ else fi # Verify dela allow fails for nonexistent task -if script -qc "dela allow nonexistent" /dev/null >/dev/null 2>&1; then +if script -eqc "dela allow nonexistent" /dev/null >/dev/null 2>&1; then echo "${RED}✗ dela allow nonexistent succeeded but should have failed${NC}" exit 1 else @@ -874,7 +874,7 @@ fi # We have duplicate_test.json and duplicate_test.mk still present in the directory. # So 'test' is ambiguous. -if script -qc "dela allow test" /dev/null >/dev/null 2>&1; then +if script -eqc "dela allow test" /dev/null >/dev/null 2>&1; then echo "${RED}✗ dela allow test (ambiguous) succeeded but should have failed${NC}" exit 1 else @@ -882,7 +882,7 @@ else fi # But dela allow test-m (disambiguated name) should succeed! -script -qc "dela allow test-m" /dev/null >/dev/null 2>&1 || { +script -eqc "dela allow test-m" /dev/null >/dev/null 2>&1 || { echo "${RED}✗ dela allow test-m (disambiguated name) failed${NC}" exit 1 } @@ -892,7 +892,7 @@ echo "${GREEN}✓ dela allow test-m (disambiguated name) succeeded as expected${ echo "\nTest 32: Testing 'dela deny' command functionality" # Deny a single valid task -script -qc "dela deny another-task" /dev/null >/dev/null 2>&1 || { +script -eqc "dela deny another-task" /dev/null >/dev/null 2>&1 || { echo "${RED}✗ dela deny another-task failed${NC}" exit 1 } @@ -944,7 +944,7 @@ else fi # Verify dela deny fails for nonexistent task -if script -qc "dela deny nonexistent" /dev/null >/dev/null 2>&1; then +if script -eqc "dela deny nonexistent" /dev/null >/dev/null 2>&1; then echo "${RED}✗ dela deny nonexistent succeeded but should have failed${NC}" exit 1 else @@ -954,7 +954,7 @@ fi # Verify dela deny fails for ambiguous tasks # We have duplicate_test.json and duplicate_test.mk still present in the directory. # So 'test' is ambiguous. -if script -qc "dela deny test" /dev/null >/dev/null 2>&1; then +if script -eqc "dela deny test" /dev/null >/dev/null 2>&1; then echo "${RED}✗ dela deny test (ambiguous) succeeded but should have failed${NC}" exit 1 else From 234d75fe98fd5c4e4ad375faa390122c8973eb17 Mon Sep 17 00:00:00 2001 From: Alex Yankov Date: Sat, 23 May 2026 12:09:25 -0400 Subject: [PATCH 4/4] format --- src/commands/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index f26bcd0..06fd15b 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -38,4 +38,3 @@ mod tests { ); } } -