use std::fs;
use opi_ai::test_support::{MockProvider, text_response};
use opi_coding_agent::cli::Cli;
use opi_coding_agent::config::OpiConfig;
use opi_coding_agent::harness::CodingHarness;
use opi_coding_agent::policy::{
RunMode, ToolFlags, ToolRuntimeConfig, ToolSelection, filter_tool_names, resolve_tool_selection,
};
use clap::Parser;
fn create_temp_workspace() -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("failed to create temp dir");
fs::create_dir_all(dir.path().join(".git")).expect("failed to create .git");
dir
}
#[test]
fn resolve_default_when_no_flags() {
let flags = ToolFlags {
tools: None,
no_tools: false,
no_builtin_tools: false,
};
assert_eq!(resolve_tool_selection(flags), ToolSelection::Default);
}
#[test]
fn resolve_disabled_when_no_tools() {
let flags = ToolFlags {
tools: None,
no_tools: true,
no_builtin_tools: false,
};
assert_eq!(resolve_tool_selection(flags), ToolSelection::Disabled);
}
#[test]
fn resolve_no_builtin_when_flag() {
let flags = ToolFlags {
tools: None,
no_tools: false,
no_builtin_tools: true,
};
assert_eq!(resolve_tool_selection(flags), ToolSelection::NoBuiltin);
}
#[test]
fn resolve_allowlist_when_tools_specified() {
let flags = ToolFlags {
tools: Some(vec!["read".into(), "glob".into()]),
no_tools: false,
no_builtin_tools: false,
};
assert_eq!(
resolve_tool_selection(flags),
ToolSelection::Allowlist(vec!["read".into(), "glob".into()])
);
}
#[test]
fn no_tools_takes_precedence_over_tools() {
let flags = ToolFlags {
tools: Some(vec!["read".into()]),
no_tools: true,
no_builtin_tools: false,
};
assert_eq!(resolve_tool_selection(flags), ToolSelection::Disabled);
}
#[test]
fn no_tools_takes_precedence_over_no_builtin() {
let flags = ToolFlags {
tools: None,
no_tools: true,
no_builtin_tools: true,
};
assert_eq!(resolve_tool_selection(flags), ToolSelection::Disabled);
}
#[test]
fn tools_takes_precedence_over_no_builtin() {
let flags = ToolFlags {
tools: Some(vec!["read".into()]),
no_tools: false,
no_builtin_tools: true,
};
assert_eq!(
resolve_tool_selection(flags),
ToolSelection::Allowlist(vec!["read".into()])
);
}
#[test]
fn interactive_default_active_tools_match_pi() {
let config = ToolRuntimeConfig::resolve(RunMode::Interactive, false, ToolSelection::Default)
.expect("interactive default should be valid");
assert_eq!(
config.active_tool_names,
vec!["read", "write", "edit", "bash"]
);
}
#[test]
fn non_interactive_default_active_tools_are_read_only() {
let config = ToolRuntimeConfig::resolve(RunMode::NonInteractive, false, ToolSelection::Default)
.expect("non-interactive read-only default should be valid");
assert_eq!(
config.active_tool_names,
vec!["read", "grep", "find", "ls", "glob"]
);
}
#[test]
fn non_interactive_mutating_opt_in_uses_coding_tools() {
let config = ToolRuntimeConfig::resolve(RunMode::NonInteractive, true, ToolSelection::Default)
.expect("non-interactive mutating default should be valid");
assert_eq!(
config.active_tool_names,
vec!["read", "write", "edit", "bash"]
);
}
#[test]
fn non_interactive_allowlisted_mutating_tool_requires_opt_in() {
let error = ToolRuntimeConfig::resolve(
RunMode::NonInteractive,
false,
ToolSelection::Allowlist(vec!["read".into(), "bash".into()]),
)
.expect_err("bash should require mutating opt-in");
assert!(
error
.to_string()
.contains("mutating tool 'bash' requires --allow-mutating")
);
}
#[test]
fn non_interactive_allowlisted_mutating_tool_allowed_with_opt_in() {
let config = ToolRuntimeConfig::resolve(
RunMode::NonInteractive,
true,
ToolSelection::Allowlist(vec!["read".into(), "bash".into()]),
)
.expect("bash should be valid with mutating opt-in");
assert_eq!(config.active_tool_names, vec!["read", "bash"]);
}
#[test]
fn filter_disabled_excludes_all() {
let all = vec!["read", "write", "edit", "bash", "glob", "grep"];
let result = filter_tool_names(&all, &ToolSelection::Disabled);
assert!(result.is_empty(), "Disabled should filter out all tools");
}
#[test]
fn filter_no_builtin_excludes_all_phase3() {
let all = vec!["read", "write", "edit", "bash", "glob", "grep"];
let result = filter_tool_names(&all, &ToolSelection::NoBuiltin);
assert!(result.is_empty(), "NoBuiltin should exclude built-in tools");
}
#[test]
fn filter_allowlist_keeps_only_named() {
let all = vec!["read", "write", "edit", "bash", "glob", "grep"];
let result = filter_tool_names(
&all,
&ToolSelection::Allowlist(vec!["read".into(), "glob".into()]),
);
assert_eq!(result, vec!["read", "glob"]);
}
#[test]
fn filter_allowlist_unknown_names_excluded() {
let all = vec!["read", "write"];
let result = filter_tool_names(
&all,
&ToolSelection::Allowlist(vec!["read".into(), "nonexistent".into()]),
);
assert_eq!(result, vec!["read"]);
}
#[test]
fn filter_allowlist_preserves_builtin_order() {
let all = vec![
"read", "write", "edit", "bash", "grep", "find", "ls", "glob",
];
let result = filter_tool_names(
&all,
&ToolSelection::Allowlist(vec!["grep".into(), "read".into(), "bash".into()]),
);
assert_eq!(result, vec!["read", "bash", "grep"]);
}
#[test]
fn filter_allowlist_empty_excludes_all() {
let all = vec!["read", "write"];
let result = filter_tool_names(&all, &ToolSelection::Allowlist(vec![]));
assert!(result.is_empty());
}
#[test]
fn cli_parse_tools_flag() {
let cli = Cli::try_parse_from(["opi", "--tools", "read,glob"]).unwrap();
assert_eq!(cli.tools, Some(vec!["read".into(), "glob".into()]));
}
#[test]
fn cli_parse_no_tools_flag() {
let cli = Cli::try_parse_from(["opi", "--no-tools"]).unwrap();
assert!(cli.no_tools);
}
#[test]
fn cli_parse_no_builtin_tools_flag() {
let cli = Cli::try_parse_from(["opi", "--no-builtin-tools"]).unwrap();
assert!(cli.no_builtin_tools);
}
#[test]
fn cli_parse_tools_single_tool() {
let cli = Cli::try_parse_from(["opi", "--tools", "read"]).unwrap();
assert_eq!(cli.tools, Some(vec!["read".into()]));
}
#[test]
fn cli_parse_no_flags_defaults() {
let cli = Cli::try_parse_from(["opi"]).unwrap();
assert!(cli.tools.is_none());
assert!(!cli.no_tools);
assert!(!cli.no_builtin_tools);
}
#[tokio::test]
async fn harness_default_includes_interactive_coding_tools() {
let workspace = create_temp_workspace();
let mock = MockProvider::new("mock", vec![text_response("done")]);
let harness = CodingHarness::new(
Box::new(mock),
"mock:mock-model".into(),
OpiConfig::default(),
workspace.path().to_path_buf(),
);
let system = harness.system_prompt();
for tool in &["read", "write", "edit", "bash"] {
assert!(
system.contains(&format!("- {tool}:")),
"Default interactive selection should include tool '{tool}'"
);
}
for tool in &["grep", "find", "ls", "glob"] {
assert!(
!system.contains(&format!("- {tool}:")),
"Default interactive selection should not include tool '{tool}'"
);
}
}
#[tokio::test]
async fn harness_non_interactive_default_includes_read_only_tools() {
let workspace = create_temp_workspace();
let mock = MockProvider::new("mock", vec![text_response("done")]);
let tool_config =
ToolRuntimeConfig::resolve(RunMode::NonInteractive, false, ToolSelection::Default)
.expect("tool config");
let harness = CodingHarness::new_with_tool_config(
Box::new(mock),
"mock:mock-model".into(),
OpiConfig::default(),
workspace.path().to_path_buf(),
tool_config,
);
let system = harness.system_prompt();
for tool in &["read", "grep", "find", "ls", "glob"] {
assert!(
system.contains(&format!("- {tool}:")),
"Non-interactive default should include tool '{tool}'"
);
}
for tool in &["bash", "edit", "write"] {
assert!(
!system.contains(&format!("- {tool}:")),
"Non-interactive default should not include tool '{tool}'"
);
}
}
#[tokio::test]
async fn harness_disabled_removes_all_tools() {
let workspace = create_temp_workspace();
let mock = MockProvider::new("mock", vec![text_response("done")]);
let harness = CodingHarness::new_with_selection(
Box::new(mock),
"mock:mock-model".into(),
OpiConfig::default(),
workspace.path().to_path_buf(),
ToolSelection::Disabled,
);
let system = harness.system_prompt();
assert!(
!system.contains("Available tools:"),
"Disabled selection should not include any tools"
);
}
#[tokio::test]
async fn harness_no_builtin_removes_all_tools_phase3() {
let workspace = create_temp_workspace();
let mock = MockProvider::new("mock", vec![text_response("done")]);
let harness = CodingHarness::new_with_selection(
Box::new(mock),
"mock:mock-model".into(),
OpiConfig::default(),
workspace.path().to_path_buf(),
ToolSelection::NoBuiltin,
);
let system = harness.system_prompt();
assert!(
!system.contains("Available tools:"),
"NoBuiltin should not include built-in tools in system prompt"
);
}
#[tokio::test]
async fn harness_allowlist_filters_tools() {
let workspace = create_temp_workspace();
let mock = MockProvider::new("mock", vec![text_response("done")]);
let harness = CodingHarness::new_with_selection(
Box::new(mock),
"mock:mock-model".into(),
OpiConfig::default(),
workspace.path().to_path_buf(),
ToolSelection::Allowlist(vec!["read".into(), "glob".into()]),
);
let system = harness.system_prompt();
assert!(
system.contains("- read:"),
"Allowlist should include 'read'"
);
assert!(
system.contains("- glob:"),
"Allowlist should include 'glob'"
);
assert!(
!system.contains("- write:"),
"Allowlist should exclude 'write'"
);
assert!(
!system.contains("- edit:"),
"Allowlist should exclude 'edit'"
);
assert!(
!system.contains("- bash:"),
"Allowlist should exclude 'bash'"
);
assert!(
!system.contains("- grep:"),
"Allowlist should exclude 'grep'"
);
}