use crate::parser::{Arg, SimpleCommand, Statement};
use super::config::{AllowlistEntry, RulesConfig};
use super::matching::normalize_command_name;
use std::borrow::Cow;
pub fn is_allowlisted(config: &RulesConfig, leaf: &Statement) -> bool {
match leaf {
Statement::SimpleCommand(cmd) => {
if cmd.name.is_none() && !cmd.assignments.is_empty() && cmd.argv.is_empty() {
return cmd.embedded_substitutions.iter().all(|sub| {
crate::parser::flatten(sub)
.iter()
.all(|inner| is_allowlisted(config, inner))
});
}
find_allowlist_match(config, cmd).is_some() || is_version_check(cmd)
}
Statement::Empty => true, _ => false,
}
}
pub(super) fn is_version_check(cmd: &SimpleCommand) -> bool {
cmd.argv.len() == 1 && (cmd.argv[0].text == "--version" || cmd.argv[0].text == "-V")
}
fn strip_wrapper_value_flag_pairs(argv: &[Arg], value_flags: &[&str]) -> Vec<Arg> {
if value_flags.is_empty() {
return argv.to_vec();
}
let mut out = Vec::with_capacity(argv.len());
let mut i = 0;
while i < argv.len() {
let token = argv[i].text.as_str();
if value_flags.contains(&token) && i + 1 < argv.len() {
i += 2;
continue;
}
out.push(argv[i].clone());
i += 1;
}
out
}
fn strip_git_global_c_flag(argv: &[Arg]) -> Vec<Arg> {
let mut out = Vec::with_capacity(argv.len());
let mut i = 0;
let mut in_global_opts = true;
while i < argv.len() {
let arg = &argv[i];
if in_global_opts {
if arg.text == "--" {
in_global_opts = false;
out.push(arg.clone());
i += 1;
continue;
}
if arg.text == "-C" {
i += 1;
if i < argv.len() {
i += 1;
}
continue;
}
if !arg.text.starts_with('-') {
in_global_opts = false;
}
}
out.push(arg.clone());
i += 1;
}
out
}
fn strip_codex_global_flags(argv: &[Arg]) -> Vec<Arg> {
const VALUE_FLAGS: &[&str] = &["--profile", "--model", "-m", "--config", "-c"];
let mut out = Vec::with_capacity(argv.len());
let mut i = 0;
let mut in_global_opts = true;
while i < argv.len() {
let arg = &argv[i];
if in_global_opts {
if arg.text == "--" {
in_global_opts = false;
out.push(arg.clone());
i += 1;
continue;
}
if VALUE_FLAGS.contains(&arg.text.as_str()) {
i += 1;
if i < argv.len() {
i += 1;
}
continue;
}
if !arg.text.starts_with('-') {
in_global_opts = false;
}
}
out.push(arg.clone());
i += 1;
}
out
}
fn normalize_arg(arg: &str) -> &str {
if !arg.contains('/') {
return arg;
}
if arg.starts_with('/') {
return arg;
}
if arg.split('/').any(|component| component == "..") {
return arg;
}
arg.rsplit('/').next().unwrap_or(arg)
}
fn args_match_prefix(required_args: &[&str], argv: &[Arg]) -> bool {
let mut argv_idx = 0;
for req in required_args {
if !req.starts_with('-') {
while argv_idx < argv.len() && argv[argv_idx].text.starts_with('-') {
argv_idx += 1;
}
}
if argv_idx >= argv.len() {
return false;
}
let normalized = normalize_arg(&argv[argv_idx].text);
if *req != normalized {
return false;
}
argv_idx += 1;
}
true
}
fn find_matching_entry<'a>(
config: &'a RulesConfig,
cmd: &SimpleCommand,
check_trust: bool,
) -> Option<&'a AllowlistEntry> {
let cmd_name = match &cmd.name {
Some(n) => n.as_str(),
None => return None,
};
let argv: Cow<[Arg]> = if cmd_name == "git" && cmd.argv.iter().any(|a| a.text == "-C") {
Cow::Owned(strip_git_global_c_flag(&cmd.argv))
} else if cmd_name == "codex"
&& cmd.argv.iter().any(|a| {
matches!(
a.text.as_str(),
"--profile" | "--model" | "-m" | "--config" | "-c"
)
})
{
Cow::Owned(strip_codex_global_flags(&cmd.argv))
} else {
let first_arg = cmd.argv.first().map(|a| a.text.as_str());
let value_flags = crate::parser::wrappers::value_flags_for(cmd_name, first_arg);
if !value_flags.is_empty()
&& cmd
.argv
.iter()
.any(|a| value_flags.contains(&a.text.as_str()))
{
Cow::Owned(strip_wrapper_value_flag_pairs(&cmd.argv, value_flags))
} else {
Cow::Borrowed(&cmd.argv)
}
};
for entry in &config.allowlists.commands {
if check_trust && entry.trust > config.trust_level {
continue;
}
let parts: Vec<&str> = entry.command.split_whitespace().collect();
if parts.is_empty() {
continue;
}
if parts[0] != normalize_command_name(cmd_name) {
continue;
}
if parts.len() == 1 {
return Some(entry);
}
let required_args = &parts[1..];
if args_match_prefix(required_args, argv.as_ref()) {
return Some(entry);
}
}
None
}
pub fn find_allowlist_match<'a>(config: &'a RulesConfig, cmd: &SimpleCommand) -> Option<&'a str> {
find_matching_entry(config, cmd, true).map(|entry| entry.command.as_str())
}
pub fn find_allowlist_reason(config: &RulesConfig, cmd: &SimpleCommand) -> Option<String> {
find_matching_entry(config, cmd, false).and_then(|entry| entry.reason.clone())
}
pub fn is_covered_by_wrapper_entry(
config: &RulesConfig,
original_leaves: &[&Statement],
extra_leaf: &Statement,
) -> bool {
let extra_cmd_name = match extra_leaf {
Statement::SimpleCommand(cmd) => cmd.name.as_deref(),
_ => return false,
};
let extra_cmd_name = match extra_cmd_name {
Some(name) => name,
None => return false,
};
for leaf in original_leaves {
if let Statement::SimpleCommand(cmd) = leaf {
if let Some(entry_str) = find_allowlist_match(config, cmd) {
let parts: Vec<&str> = entry_str.split_whitespace().collect();
if parts.len() > 1 && parts[1..].contains(&extra_cmd_name) {
return true;
}
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
fn make_argv(args: &[&str]) -> Vec<Arg> {
args.iter().map(|s| Arg::plain(*s)).collect()
}
fn test_git_cmd(argv: Vec<&str>) -> SimpleCommand {
SimpleCommand {
name: Some("git".to_string()),
argv: argv.into_iter().map(Arg::plain).collect(),
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
}
}
#[test]
fn test_normalize_arg_no_slash_unchanged() {
assert_eq!(normalize_arg("manage.py"), "manage.py");
assert_eq!(normalize_arg("check"), "check");
assert_eq!(normalize_arg("myapp.tests"), "myapp.tests");
}
#[test]
fn test_normalize_arg_relative_path_returns_basename() {
assert_eq!(normalize_arg("./manage.py"), "manage.py");
assert_eq!(normalize_arg("server/manage.py"), "manage.py");
assert_eq!(normalize_arg("apps/core/manage.py"), "manage.py");
}
#[test]
fn test_normalize_arg_absolute_path_unchanged() {
assert_eq!(normalize_arg("/tmp/manage.py"), "/tmp/manage.py");
assert_eq!(
normalize_arg("/home/user/project/manage.py"),
"/home/user/project/manage.py"
);
}
#[test]
fn test_normalize_arg_traversal_unchanged() {
assert_eq!(normalize_arg("../manage.py"), "../manage.py");
assert_eq!(normalize_arg("../../manage.py"), "../../manage.py");
assert_eq!(normalize_arg("foo/../manage.py"), "foo/../manage.py");
}
#[test]
fn test_normalize_arg_dot_only_returns_empty() {
let result = normalize_arg("./");
assert!(result.is_empty() || result == "./");
}
#[test]
fn test_args_match_prefix_exact_match() {
let required = &["manage.py", "check"];
let argv = make_argv(&["manage.py", "check"]);
assert!(args_match_prefix(required, &argv));
}
#[test]
fn test_args_match_prefix_with_extra_trailing_args() {
let required = &["manage.py", "check"];
let argv = make_argv(&["manage.py", "check", "--deploy"]);
assert!(args_match_prefix(required, &argv));
}
#[test]
fn test_args_match_prefix_wrong_first_arg_fails() {
let required = &["manage.py", "check"];
let argv = make_argv(&["evil.py", "manage.py", "check"]);
assert!(!args_match_prefix(required, &argv));
}
#[test]
fn test_args_match_prefix_out_of_order_fails() {
let required = &["manage.py", "check"];
let argv = make_argv(&["check", "manage.py"]);
assert!(!args_match_prefix(required, &argv));
}
#[test]
fn test_args_match_prefix_with_path_normalization() {
let required = &["manage.py", "check"];
let argv = make_argv(&["./manage.py", "check"]);
assert!(args_match_prefix(required, &argv));
}
#[test]
fn test_args_match_prefix_subdir_path_normalization() {
let required = &["manage.py", "check"];
let argv = make_argv(&["server/manage.py", "check"]);
assert!(args_match_prefix(required, &argv));
}
#[test]
fn test_args_match_prefix_absolute_path_no_normalization() {
let required = &["manage.py", "check"];
let argv = make_argv(&["/tmp/manage.py", "check"]);
assert!(!args_match_prefix(required, &argv));
}
#[test]
fn test_args_match_prefix_traversal_no_normalization() {
let required = &["manage.py", "check"];
let argv = make_argv(&["../manage.py", "check"]);
assert!(!args_match_prefix(required, &argv));
}
#[test]
fn test_args_match_prefix_not_enough_args() {
let required = &["manage.py", "check"];
let argv = make_argv(&["manage.py"]);
assert!(!args_match_prefix(required, &argv));
}
#[test]
fn test_args_match_prefix_empty_required() {
let required: &[&str] = &[];
let argv = make_argv(&["anything"]);
assert!(args_match_prefix(required, &argv));
}
#[test]
fn test_strip_git_global_c_flag_basic() {
let argv = make_argv(&["-C", "/tmp", "status"]);
assert_eq!(strip_git_global_c_flag(&argv), make_argv(&["status"]));
}
#[test]
fn test_strip_git_global_c_flag_multiple() {
let argv = make_argv(&["-C", "/tmp", "-C", "/other", "status"]);
assert_eq!(strip_git_global_c_flag(&argv), make_argv(&["status"]));
}
#[test]
fn test_find_allowlist_match_git_c_status_matches_git_status() {
let config = RulesConfig {
version: 1,
default_decision: crate::domain::Decision::Ask,
safety_level: crate::policy::SafetyLevel::High,
trust_level: crate::policy::TrustLevel::default(),
allowlists: crate::policy::Allowlists {
commands: vec![crate::policy::AllowlistEntry {
command: "git status".to_string(),
trust: crate::policy::TrustLevel::Standard,
reason: None,
source: crate::policy::RuleSource::default(),
}],
paths: vec![],
},
rules: vec![],
};
let cmd = test_git_cmd(vec!["-C", "/tmp", "status"]);
assert_eq!(find_allowlist_match(&config, &cmd), Some("git status"));
}
#[test]
fn test_find_allowlist_match_respects_trust_level() {
let config_minimal = RulesConfig {
version: 1,
default_decision: crate::domain::Decision::Ask,
safety_level: crate::policy::SafetyLevel::High,
trust_level: crate::policy::TrustLevel::Minimal,
allowlists: crate::policy::Allowlists {
commands: vec![
crate::policy::AllowlistEntry {
command: "ls".to_string(),
trust: crate::policy::TrustLevel::Minimal,
reason: None,
source: crate::policy::RuleSource::default(),
},
crate::policy::AllowlistEntry {
command: "go build".to_string(),
trust: crate::policy::TrustLevel::Standard,
reason: None,
source: crate::policy::RuleSource::default(),
},
crate::policy::AllowlistEntry {
command: "docker run".to_string(),
trust: crate::policy::TrustLevel::Full,
reason: None,
source: crate::policy::RuleSource::default(),
},
],
paths: vec![],
},
rules: vec![],
};
let ls_cmd = SimpleCommand {
name: Some("ls".to_string()),
argv: vec![],
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
};
assert_eq!(
find_allowlist_match(&config_minimal, &ls_cmd),
Some("ls"),
"Minimal-trust entry should match at Minimal trust level"
);
let go_cmd = SimpleCommand {
name: Some("go".to_string()),
argv: make_argv(&["build"]),
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
};
assert_eq!(
find_allowlist_match(&config_minimal, &go_cmd),
None,
"Standard-trust entry should be skipped at Minimal trust level"
);
let docker_cmd = SimpleCommand {
name: Some("docker".to_string()),
argv: make_argv(&["run"]),
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
};
assert_eq!(
find_allowlist_match(&config_minimal, &docker_cmd),
None,
"Full-trust entry should be skipped at Minimal trust level"
);
let config_standard = RulesConfig {
trust_level: crate::policy::TrustLevel::Standard,
..config_minimal
};
assert_eq!(
find_allowlist_match(&config_standard, &go_cmd),
Some("go build"),
"Standard-trust entry should match at Standard trust level"
);
assert_eq!(
find_allowlist_match(&config_standard, &docker_cmd),
None,
"Full-trust entry should be skipped at Standard trust level"
);
}
#[test]
fn test_find_allowlist_reason_returns_reason_for_trust_filtered_entry() {
let config = RulesConfig {
version: 1,
default_decision: crate::domain::Decision::Ask,
safety_level: crate::policy::SafetyLevel::High,
trust_level: crate::policy::TrustLevel::Standard,
allowlists: crate::policy::Allowlists {
commands: vec![crate::policy::AllowlistEntry {
command: "git push".to_string(),
trust: crate::policy::TrustLevel::Full,
reason: Some("Pushes local commits to a remote repository".to_string()),
source: crate::policy::RuleSource::default(),
}],
paths: vec![],
},
rules: vec![],
};
let cmd = test_git_cmd(vec!["push", "origin", "main"]);
assert_eq!(find_allowlist_match(&config, &cmd), None);
assert_eq!(
find_allowlist_reason(&config, &cmd),
Some("Pushes local commits to a remote repository".to_string()),
);
}
#[test]
fn test_find_allowlist_reason_returns_none_for_unrecognized_command() {
let config = RulesConfig {
version: 1,
default_decision: crate::domain::Decision::Ask,
safety_level: crate::policy::SafetyLevel::High,
trust_level: crate::policy::TrustLevel::Standard,
allowlists: crate::policy::Allowlists {
commands: vec![crate::policy::AllowlistEntry {
command: "git push".to_string(),
trust: crate::policy::TrustLevel::Full,
reason: Some("Pushes local commits to a remote repository".to_string()),
source: crate::policy::RuleSource::default(),
}],
paths: vec![],
},
rules: vec![],
};
let cmd = SimpleCommand {
name: Some("unknown-tool".to_string()),
argv: vec![],
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
};
assert_eq!(find_allowlist_reason(&config, &cmd), None);
}
#[test]
fn test_find_allowlist_reason_returns_none_when_no_reason_field() {
let config = RulesConfig {
version: 1,
default_decision: crate::domain::Decision::Ask,
safety_level: crate::policy::SafetyLevel::High,
trust_level: crate::policy::TrustLevel::Standard,
allowlists: crate::policy::Allowlists {
commands: vec![crate::policy::AllowlistEntry {
command: "git push".to_string(),
trust: crate::policy::TrustLevel::Full,
reason: None,
source: crate::policy::RuleSource::default(),
}],
paths: vec![],
},
rules: vec![],
};
let cmd = test_git_cmd(vec!["push"]);
assert_eq!(find_allowlist_reason(&config, &cmd), None);
}
#[test]
fn test_find_allowlist_match_git_c_clean_does_not_match_git_clean_allowlist() {
let config = RulesConfig {
version: 1,
default_decision: crate::domain::Decision::Ask,
safety_level: crate::policy::SafetyLevel::High,
trust_level: crate::policy::TrustLevel::default(),
allowlists: crate::policy::Allowlists {
commands: vec![crate::policy::AllowlistEntry {
command: "git status".to_string(),
trust: crate::policy::TrustLevel::Standard,
reason: None,
source: crate::policy::RuleSource::default(),
}],
paths: vec![],
},
rules: vec![],
};
let cmd = test_git_cmd(vec!["-C", "/tmp", "clean", "-f"]);
assert_eq!(find_allowlist_match(&config, &cmd), None);
}
#[test]
fn test_args_match_prefix_skips_flags_before_subcommand() {
let required = &["labels"];
let argv = make_argv(&["--addr=http://foo:3100", "labels", "host"]);
assert!(
args_match_prefix(required, &argv),
"Should skip --addr= flag to match 'labels' subcommand"
);
}
#[test]
fn test_args_match_prefix_skips_multiple_flags() {
let required = &["labels"];
let argv = make_argv(&["--addr=http://foo:3100", "-q", "labels", "host"]);
assert!(
args_match_prefix(required, &argv),
"Should skip multiple flags to match subcommand"
);
}
#[test]
fn test_args_match_prefix_no_flags_still_works() {
let required = &["labels"];
let argv = make_argv(&["labels", "host"]);
assert!(args_match_prefix(required, &argv));
}
#[test]
fn test_args_match_prefix_multi_subcommand_with_flags() {
let required = &["run", "prefect", "config", "view"];
let argv = make_argv(&["--quiet", "run", "prefect", "config", "view"]);
assert!(
args_match_prefix(required, &argv),
"Should skip --quiet to match multi-word subcommand chain"
);
}
#[test]
fn test_args_match_prefix_flag_in_required_still_matches() {
let required = &["push", "--force"];
let argv = make_argv(&["push", "--force"]);
assert!(
args_match_prefix(required, &argv),
"Required flags should match literally"
);
}
#[test]
fn test_args_match_prefix_wrong_subcommand_after_flags() {
let required = &["labels"];
let argv = make_argv(&["--addr=http://foo:3100", "query"]);
assert!(
!args_match_prefix(required, &argv),
"Wrong subcommand should not match even after skipping flags"
);
}
#[test]
fn test_args_match_prefix_only_flags_no_subcommand() {
let required = &["labels"];
let argv = make_argv(&["--addr=http://foo:3100", "-q"]);
assert!(
!args_match_prefix(required, &argv),
"Should not match if no subcommand present after flags"
);
}
#[test]
fn test_is_covered_by_wrapper_entry_compound_entry() {
use crate::parser::Statement;
let config = RulesConfig {
version: 1,
default_decision: crate::domain::Decision::Ask,
safety_level: crate::policy::SafetyLevel::High,
trust_level: crate::policy::TrustLevel::Standard,
allowlists: crate::policy::Allowlists {
commands: vec![crate::policy::AllowlistEntry {
command: "uv run yamllint".to_string(),
trust: crate::policy::TrustLevel::Standard,
reason: None,
source: crate::policy::RuleSource::default(),
}],
paths: vec![],
},
rules: vec![],
};
let original_leaf = Statement::SimpleCommand(SimpleCommand {
name: Some("uv".to_string()),
argv: make_argv(&["run", "yamllint", ".gitlab-ci.yml"]),
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
});
let extra_leaf_covered = Statement::SimpleCommand(SimpleCommand {
name: Some("yamllint".to_string()),
argv: make_argv(&[".gitlab-ci.yml"]),
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
});
let extra_leaf_not_covered = Statement::SimpleCommand(SimpleCommand {
name: Some("dangeroustool".to_string()),
argv: vec![],
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
});
let leaves = vec![&original_leaf];
assert!(
is_covered_by_wrapper_entry(&config, &leaves, &extra_leaf_covered),
"yamllint should be covered by 'uv run yamllint' entry"
);
assert!(
!is_covered_by_wrapper_entry(&config, &leaves, &extra_leaf_not_covered),
"dangeroustool should NOT be covered"
);
}
#[test]
fn test_is_covered_by_wrapper_entry_multi_word_subcommand() {
use crate::parser::Statement;
let config = RulesConfig {
version: 1,
default_decision: crate::domain::Decision::Ask,
safety_level: crate::policy::SafetyLevel::High,
trust_level: crate::policy::TrustLevel::Standard,
allowlists: crate::policy::Allowlists {
commands: vec![crate::policy::AllowlistEntry {
command: "uv run prefect config view".to_string(),
trust: crate::policy::TrustLevel::Standard,
reason: None,
source: crate::policy::RuleSource::default(),
}],
paths: vec![],
},
rules: vec![],
};
let original_leaf = Statement::SimpleCommand(SimpleCommand {
name: Some("uv".to_string()),
argv: make_argv(&["run", "prefect", "config", "view"]),
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
});
let extra_leaf_covered = Statement::SimpleCommand(SimpleCommand {
name: Some("prefect".to_string()),
argv: make_argv(&["config", "view"]),
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
});
let extra_leaf_not_covered = Statement::SimpleCommand(SimpleCommand {
name: Some("dangeroustool".to_string()),
argv: vec![],
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
});
let leaves = vec![&original_leaf];
assert!(
is_covered_by_wrapper_entry(&config, &leaves, &extra_leaf_covered),
"prefect should be covered by 'uv run prefect config view' entry"
);
assert!(
!is_covered_by_wrapper_entry(&config, &leaves, &extra_leaf_not_covered),
"dangeroustool should NOT be covered"
);
}
#[test]
fn test_is_covered_by_wrapper_entry_bare_entry_no_coverage() {
use crate::parser::Statement;
let config = RulesConfig {
version: 1,
default_decision: crate::domain::Decision::Ask,
safety_level: crate::policy::SafetyLevel::High,
trust_level: crate::policy::TrustLevel::Standard,
allowlists: crate::policy::Allowlists {
commands: vec![crate::policy::AllowlistEntry {
command: "timeout".to_string(),
trust: crate::policy::TrustLevel::Standard,
reason: None,
source: crate::policy::RuleSource::default(),
}],
paths: vec![],
},
rules: vec![],
};
let original_leaf = Statement::SimpleCommand(SimpleCommand {
name: Some("timeout".to_string()),
argv: make_argv(&["10", "unknown_command"]),
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
});
let extra_leaf = Statement::SimpleCommand(SimpleCommand {
name: Some("unknown_command".to_string()),
argv: vec![],
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
});
let leaves = vec![&original_leaf];
assert!(
!is_covered_by_wrapper_entry(&config, &leaves, &extra_leaf),
"Bare 'timeout' entry should NOT cover unknown_command"
);
}
fn uv_cmd(argv: &[&str]) -> SimpleCommand {
SimpleCommand {
name: Some("uv".to_string()),
argv: argv.iter().map(|s| Arg::plain(*s)).collect(),
redirects: vec![],
assignments: vec![],
embedded_substitutions: vec![],
}
}
fn uv_run_ruff_config() -> RulesConfig {
RulesConfig {
version: 1,
default_decision: crate::domain::Decision::Ask,
safety_level: crate::policy::SafetyLevel::High,
trust_level: crate::policy::TrustLevel::Standard,
allowlists: crate::policy::Allowlists {
commands: vec![crate::policy::AllowlistEntry {
command: "uv run ruff".to_string(),
trust: crate::policy::TrustLevel::Standard,
reason: None,
source: crate::policy::RuleSource::default(),
}],
paths: vec![],
},
rules: vec![],
}
}
#[test]
fn test_find_match_uv_run_ruff_with_project_space_value() {
let config = uv_run_ruff_config();
let cmd = uv_cmd(&["run", "--project", "/tmp", "ruff", "--version"]);
assert_eq!(find_allowlist_match(&config, &cmd), Some("uv run ruff"));
}
#[test]
fn test_find_match_uv_run_ruff_with_short_flag_p_space_value() {
let config = uv_run_ruff_config();
let cmd = uv_cmd(&["run", "-p", "/tmp", "ruff"]);
assert_eq!(find_allowlist_match(&config, &cmd), Some("uv run ruff"));
}
#[test]
fn test_find_match_uv_run_ruff_with_directory_space_value() {
let config = uv_run_ruff_config();
let cmd = uv_cmd(&["run", "--directory", "/tmp", "ruff"]);
assert_eq!(find_allowlist_match(&config, &cmd), Some("uv run ruff"));
}
#[test]
fn test_find_match_uv_run_ruff_with_python_version_space_value() {
let config = uv_run_ruff_config();
let cmd = uv_cmd(&["run", "--python", "3.12", "ruff"]);
assert_eq!(find_allowlist_match(&config, &cmd), Some("uv run ruff"));
}
#[test]
fn test_find_match_uv_run_ruff_equals_form_still_matches() {
let config = uv_run_ruff_config();
let cmd = uv_cmd(&["run", "--project=/tmp", "ruff"]);
assert_eq!(find_allowlist_match(&config, &cmd), Some("uv run ruff"));
}
#[test]
fn test_find_match_uv_run_ruff_bool_flag_still_matches() {
let config = uv_run_ruff_config();
let cmd = uv_cmd(&["run", "--isolated", "ruff"]);
assert_eq!(find_allowlist_match(&config, &cmd), Some("uv run ruff"));
}
#[test]
fn test_find_match_uv_run_ruff_no_flags_still_matches() {
let config = uv_run_ruff_config();
let cmd = uv_cmd(&["run", "ruff"]);
assert_eq!(find_allowlist_match(&config, &cmd), Some("uv run ruff"));
}
#[test]
fn test_find_match_uv_run_project_does_not_match_other_subcommand() {
let config = uv_run_ruff_config();
let cmd = uv_cmd(&["run", "--project", "/tmp", "dangeroustool"]);
assert_eq!(find_allowlist_match(&config, &cmd), None);
}
#[test]
fn test_find_match_uv_run_project_value_is_not_the_subcommand() {
let config = uv_run_ruff_config();
let cmd = uv_cmd(&["run", "--project", "ruff"]);
assert_eq!(find_allowlist_match(&config, &cmd), None);
}
}