use std::path::PathBuf;
use opi_ai::test_support::{self, MockProvider};
use opi_coding_agent::config::OpiConfig;
use opi_coding_agent::policy::{RunMode, ToolRuntimeConfig, ToolSelection};
use opi_coding_agent::runner::{ExitCode, NonInteractiveRunner};
fn temp_workspace() -> PathBuf {
let dir = std::env::temp_dir().join(format!("opi-policy-test-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
dir
}
#[tokio::test]
async fn policy_write_blocked_by_default() {
let first = test_support::tool_call_response(
"tc-1",
"write",
r#"{"path":"test.txt","content":"hello"}"#,
);
let second = test_support::text_response("Write was denied.");
let provider = MockProvider::new("mock", vec![first, second]);
let mut runner = NonInteractiveRunner::new(
Box::new(provider),
"mock-model".into(),
OpiConfig::default(),
temp_workspace(),
false, None,
Vec::new(),
);
let result = runner.run("Write a file").await;
assert_eq!(result.exit_code, ExitCode::Success as i32);
assert!(
result.stdout.contains("unknown tool: write")
|| result.stderr.contains("unknown tool: write")
|| result.stdout.contains("Write was denied"),
"should indicate write was not available, stdout: {:?}, stderr: {:?}",
result.stdout,
result.stderr
);
}
#[tokio::test]
async fn policy_edit_blocked_by_default() {
let first = test_support::tool_call_response(
"tc-1",
"edit",
r#"{"path":"test.txt","old_string":"foo","new_string":"bar"}"#,
);
let second = test_support::text_response("Edit was denied.");
let provider = MockProvider::new("mock", vec![first, second]);
let mut runner = NonInteractiveRunner::new(
Box::new(provider),
"mock-model".into(),
OpiConfig::default(),
temp_workspace(),
false,
None,
Vec::new(),
);
let result = runner.run("Edit a file").await;
assert_eq!(result.exit_code, ExitCode::Success as i32);
assert!(
result.stdout.contains("unknown tool: edit")
|| result.stderr.contains("unknown tool: edit")
|| result.stdout.contains("Edit was denied"),
"should indicate edit was not available, stdout: {:?}, stderr: {:?}",
result.stdout,
result.stderr
);
}
#[tokio::test]
async fn policy_bash_blocked_by_default() {
let first = test_support::tool_call_response("tc-1", "bash", r#"{"command":"ls -la"}"#);
let second = test_support::text_response("Bash was denied.");
let provider = MockProvider::new("mock", vec![first, second]);
let mut runner = NonInteractiveRunner::new(
Box::new(provider),
"mock-model".into(),
OpiConfig::default(),
temp_workspace(),
false,
None,
Vec::new(),
);
let result = runner.run("Run a command").await;
assert_eq!(result.exit_code, ExitCode::Success as i32);
assert!(
result.stdout.contains("unknown tool: bash")
|| result.stderr.contains("unknown tool: bash")
|| result.stdout.contains("Bash was denied"),
"should indicate bash was not available, stdout: {:?}, stderr: {:?}",
result.stdout,
result.stderr
);
}
#[tokio::test]
async fn policy_read_allowed_by_default() {
let first = test_support::tool_call_response("tc-1", "read", r#"{"path":"Cargo.toml"}"#);
let second = test_support::text_response("File read successful.");
let provider = MockProvider::new("mock", vec![first, second]);
let mut runner = NonInteractiveRunner::new(
Box::new(provider),
"mock-model".into(),
OpiConfig::default(),
temp_workspace(),
false,
None,
Vec::new(),
);
let result = runner.run("Read a file").await;
assert_eq!(result.exit_code, ExitCode::Success as i32);
}
#[tokio::test]
async fn policy_all_tools_allowed_when_opted_in() {
let first = test_support::tool_call_response(
"tc-1",
"write",
r#"{"path":"test.txt","content":"hello"}"#,
);
let second = test_support::text_response("Write succeeded.");
let provider = MockProvider::new("mock", vec![first, second]);
let workspace = temp_workspace();
let mut runner = NonInteractiveRunner::new(
Box::new(provider),
"mock-model".into(),
OpiConfig::default(),
workspace.clone(),
true, None,
Vec::new(),
);
let result = runner.run("Write a file").await;
assert_eq!(result.exit_code, ExitCode::Success as i32);
assert!(
!result.stdout.contains("not allowed"),
"should not contain denial message when opted in, got: {:?}",
result.stdout
);
let _ = std::fs::remove_file(workspace.join("test.txt"));
}
#[test]
fn policy_readonly_tools_always_allowed() {
let config = ToolRuntimeConfig::resolve(RunMode::NonInteractive, false, ToolSelection::Default)
.expect("tool config");
assert_eq!(
config.active_tool_names,
vec!["read", "grep", "find", "ls", "glob"]
);
}
#[test]
fn non_interactive_tools_bash_without_allow_mutating_is_policy_error() {
let error = ToolRuntimeConfig::resolve(
RunMode::NonInteractive,
false,
ToolSelection::Allowlist(vec!["bash".into()]),
)
.expect_err("bash should require opt-in");
assert!(
error
.to_string()
.contains("mutating tool 'bash' requires --allow-mutating")
);
}