Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions cargo_crap_baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@
{
"file": "./src/main.rs",
"function": "run_command",
"line": 172,
"cyclomatic": 10.0,
"coverage": 21.428571428571427,
"crap": 58.505830903790084
"line": 182,
"cyclomatic": 11.0,
"coverage": 20.689655172413794,
"crap": 71.36356554184265
},
{
"file": "./src/prompt.rs",
Expand Down Expand Up @@ -239,8 +239,8 @@
"function": "evaluate_task_against_allowlist",
"line": 75,
"cyclomatic": 14.0,
"coverage": 95.65217391304348,
"crap": 14.016109147694584
"coverage": 100.0,
"crap": 14.0
},
{
"file": "./src/mcp/server.rs",
Expand Down Expand Up @@ -293,7 +293,7 @@
{
"file": "./src/main.rs",
"function": "main",
"line": 209,
"line": 220,
"cyclomatic": 3.0,
"coverage": 0.0,
"crap": 12.0
Expand Down Expand Up @@ -682,6 +682,14 @@
"coverage": 100.0,
"crap": 6.0
},
{
"file": "./src/commands/deny.rs",
"function": "execute",
"line": 7,
"cyclomatic": 6.0,
"coverage": 100.0,
"crap": 6.0
},
{
"file": "./src/commands/mcp.rs",
"function": "Editor::config_path",
Expand Down
2 changes: 1 addition & 1 deletion dev_docs/project_plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ This plan outlines the major development phases and tasks for building `dela`, a
- [x] [DTKT-32] Persist decisions in the allowlist.
- [ ] [DTKT-33] Have `dela run` take an optional `--allow` flag to allow a task without prompting.
- [x] [DTKT-109] Implement `dela allow` command to add allowlist entries.
- [ ] [DTKT-110] Implement `dela deny` command to add denylist entries.
- [x] [DTKT-110] Implement `dela deny` command to add denylist entries.
- [ ] [DTKT-111] Implement `dela run --allow-once` command to run a command once.

- [x] **Runtime Checks**
Expand Down
6 changes: 6 additions & 0 deletions src/commands/allow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,22 @@ mod tests {
struct TestEnvGuard {
project_dir: TempDir,
home_dir: TempDir,
original_cwd: std::path::PathBuf,
}

impl TestEnvGuard {
fn new() -> Self {
let original_cwd = env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
let (project_dir, home_dir) = setup_test_env();
Self {
project_dir,
home_dir,
original_cwd,
}
}

fn new_uninitialized() -> Self {
let original_cwd = env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
// 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");

Expand All @@ -74,12 +78,14 @@ mod tests {
Self {
project_dir: TempDir::new().expect("Failed to create temp directory"),
home_dir,
original_cwd,
}
}
}

impl Drop for TestEnvGuard {
fn drop(&mut self) {
let _ = env::set_current_dir(&self.original_cwd);
reset_to_real_environment();
}
}
Expand Down
222 changes: 222 additions & 0 deletions src/commands/deny.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
use crate::allowlist;
use crate::task_discovery;
use crate::types::AllowScope;
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<()> {
let current_dir = env::current_dir()
.map_err(|e| anyhow::anyhow!("Failed to get current directory: {}", e))?;
let discovered = task_discovery::discover_tasks(&current_dir);

// Find all tasks with the given name (both original and disambiguated)
let matching_tasks = task_discovery::get_matching_tasks(&discovered, task_name);

match matching_tasks.len() {
0 => Err(anyhow::anyhow!(
"dela: command or task not found: {}",
task_name
)),
1 => {
let task = matching_tasks[0];
allowlist::check_task_allowed_with_scope(task, AllowScope::Deny)?;
println!(
"Denied task '{}' ({}) in allowlist.",
task.name,
task.runner.short_name()
);
Ok(())
}
_ => {
let error_msg = task_discovery::format_ambiguous_task_error(task_name, &matching_tasks);
eprintln!("{}", error_msg);
Err(anyhow::anyhow!(
"Multiple tasks named '{}' found",
task_name
))
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::config::{preferred_allowlist_path_for, preferred_config_dir_path_for};
use crate::environment::{TestEnvironment, reset_to_real_environment, set_test_environment};
use serial_test::serial;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use tempfile::TempDir;

struct TestEnvGuard {
project_dir: TempDir,
home_dir: TempDir,
original_cwd: std::path::PathBuf,
}

impl TestEnvGuard {
fn new() -> Self {
let original_cwd = env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
let (project_dir, home_dir) = setup_test_env();
Self {
project_dir,
home_dir,
original_cwd,
}
}

fn new_uninitialized() -> Self {
let original_cwd = env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
// 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");

// Set up test environment with the temp directory as HOME
let test_env = TestEnvironment::new().with_home(home_dir.path().to_string_lossy());
set_test_environment(test_env);

Self {
project_dir: TempDir::new().expect("Failed to create temp directory"),
home_dir,
original_cwd,
}
}
}

impl Drop for TestEnvGuard {
fn drop(&mut self) {
let _ = env::set_current_dir(&self.original_cwd);
reset_to_real_environment();
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

fn setup_test_env() -> (TempDir, TempDir) {
// Create a temp dir for the project
let project_dir = TempDir::new().expect("Failed to create temp directory");

// Create a test Makefile
let makefile_content = "
build: ## Building the project
\t@echo Building...

test: ## Running tests
\t@echo Testing...
";
let mut makefile =
File::create(project_dir.path().join("Makefile")).expect("Failed to create Makefile");
makefile
.write_all(makefile_content.as_bytes())
.expect("Failed to write Makefile");

// Create a temp dir for HOME and set it up
let home_dir = TempDir::new().expect("Failed to create temp HOME directory");

// Set up test environment with the temp directory as HOME
let test_env = TestEnvironment::new().with_home(home_dir.path().to_string_lossy());
set_test_environment(test_env);

// Create ~/.config/dela directory
fs::create_dir_all(preferred_config_dir_path_for(home_dir.path()))
.expect("Failed to create dela config directory");

(project_dir, home_dir)
}

#[test]
#[serial]
fn test_execute_deny_single_task() {
let guard = TestEnvGuard::new();
env::set_current_dir(&guard.project_dir).expect("Failed to change directory");

let result = execute("test");
assert!(result.is_ok(), "Should succeed for a single task");

// Verify it was added to the allowlist with Deny scope
let allowlist_content =
fs::read_to_string(preferred_allowlist_path_for(guard.home_dir.path())).unwrap();
assert!(allowlist_content.contains("scope = \"Deny\""));
}

#[test]
#[serial]
fn test_execute_deny_no_task() {
let guard = TestEnvGuard::new();
env::set_current_dir(&guard.project_dir).expect("Failed to change directory");

let result = execute("nonexistent");
assert!(result.is_err(), "Should fail for nonexistent task");
assert_eq!(
result.unwrap_err().to_string(),
"dela: command or task not found: nonexistent"
);
}

#[test]
#[serial]
fn test_execute_deny_uninitialized() {
let guard = TestEnvGuard::new_uninitialized();
env::set_current_dir(&guard.project_dir).expect("Failed to change directory");

// Create a test Makefile
let makefile_content = "
test: ## Running tests
\t@echo Testing...
";
let mut makefile = File::create(guard.project_dir.path().join("Makefile"))
.expect("Failed to create Makefile");
makefile
.write_all(makefile_content.as_bytes())
.expect("Failed to write Makefile");

let result = execute("test");
assert!(result.is_err(), "Should fail when dela is not initialized");
assert_eq!(
result.unwrap_err().to_string(),
"Dela is not initialized. Please run 'dela init' first."
);
}

#[test]
#[serial]
fn test_execute_deny_disambiguated() {
let guard = TestEnvGuard::new();
env::set_current_dir(&guard.project_dir).expect("Failed to change directory");

// Create a package.json with the same task name
let package_json_content = r#"{
"name": "test-package",
"scripts": {
"test": "jest"
}
}"#;

File::create(guard.project_dir.path().join("package.json"))
.unwrap()
.write_all(package_json_content.as_bytes())
.unwrap();

// Create package-lock.json to ensure npm is detected
File::create(guard.project_dir.path().join("package-lock.json"))
.unwrap()
.write_all(b"{}")
.unwrap();

// Ambiguous task name 'test' should fail
let result = execute("test");
assert!(result.is_err(), "Should fail for ambiguous task name");
assert!(
result
.unwrap_err()
.to_string()
.contains("Multiple tasks named 'test' found")
);

// Suffixed/disambiguated task name should succeed
let result = execute("test-m");
assert!(result.is_ok(), "Should succeed with disambiguated name");

let allowlist_content =
fs::read_to_string(preferred_allowlist_path_for(guard.home_dir.path())).unwrap();
assert!(allowlist_content.contains("scope = \"Deny\""));
}
}
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod allow;
pub mod allow_command;
pub mod configure_shell;
pub mod deny;
pub mod get_command;
pub mod init;
pub mod list;
Expand Down
11 changes: 11 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,16 @@ enum Commands {
task: String,
},

/// Deny a specific task from running
///
/// This adds the task's definition file to the allowlist at the Deny scope.
///
/// Example: dela deny build
Deny {
/// Name of the task to deny
task: String,
},

// Internal commands (hidden from help by default)
#[command(name = "configure-shell", hide = true)]
ConfigureShell,
Expand Down Expand Up @@ -194,6 +204,7 @@ async fn run_command(command: Commands) -> anyhow::Result<()> {
Commands::List { verbose, color } => commands::list::execute(verbose, &color),
Commands::Run { task } => commands::run::execute(&task),
Commands::Allow { task } => commands::allow::execute(&task),
Commands::Deny { task } => commands::deny::execute(&task),
Commands::GetCommand { args } => {
if args.is_empty() {
Err(anyhow::anyhow!("No task name provided"))
Expand Down
Loading