Files
IdeA/crates/infrastructure/tests/mcp_tool_permission_store.rs

178 lines
5.0 KiB
Rust

//! L2 integration tests for [`FsMcpToolPermissionStore`] against a real temp project.
use std::path::PathBuf;
use std::sync::Arc;
use domain::ids::{AgentId, ProjectId};
use domain::ports::{FileSystem, McpToolPermissionStore, StoreError};
use domain::project::{Project, ProjectPath};
use domain::remote::RemoteRef;
use domain::{
AgentMcpToolPolicyOverride, McpToolPolicy, ProjectMcpToolPermissions,
MCP_TOOL_PERMISSIONS_VERSION,
};
use infrastructure::orchestrator::mcp::tools::{classified_tool_names, READ_ONLY_TOOLS};
use infrastructure::{FsMcpToolPermissionStore, LocalFileSystem};
use uuid::Uuid;
/// A unique scratch project directory under the OS temp dir, cleaned up on drop.
struct TempDir(PathBuf);
impl TempDir {
fn new() -> Self {
let p =
std::env::temp_dir().join(format!("idea-l2-mcp-tool-permissions-{}", Uuid::new_v4()));
std::fs::create_dir_all(&p).unwrap();
Self(p)
}
fn project_root(&self) -> String {
self.0.to_string_lossy().into_owned()
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn store() -> FsMcpToolPermissionStore {
let fs: Arc<dyn FileSystem> = Arc::new(LocalFileSystem::new());
FsMcpToolPermissionStore::new(fs)
}
fn project(tmp: &TempDir) -> Project {
Project::new(
ProjectId::new_random(),
"mcp-tool-permissions",
ProjectPath::new(tmp.project_root()).unwrap(),
RemoteRef::local(),
1_700_000_000_000,
)
.unwrap()
}
fn policy(tools: &[&str]) -> McpToolPolicy {
McpToolPolicy::new(
tools.iter().map(|tool| (*tool).to_owned()).collect(),
&classified_tool_names(),
)
.unwrap()
}
#[tokio::test]
async fn missing_mcp_tool_permissions_file_returns_default_document() {
let tmp = TempDir::new();
let project = project(&tmp);
let loaded = store().load_mcp_tool_permissions(&project).await.unwrap();
assert_eq!(loaded, ProjectMcpToolPermissions::default());
assert_eq!(loaded.version, MCP_TOOL_PERMISSIONS_VERSION);
let effective = loaded
.effective_policy(
AgentId::new_random(),
READ_ONLY_TOOLS,
&classified_tool_names(),
)
.unwrap();
assert!(effective.permits("idea_context_read"));
assert!(!effective.permits("idea_memory_write"));
}
#[tokio::test]
async fn save_then_load_roundtrips_project_default_and_agent_override() {
let tmp = TempDir::new();
let project = project(&tmp);
let agent = AgentId::new_random();
let doc = ProjectMcpToolPermissions::new(
Some(policy(&["idea_context_read", "idea_memory_read"])),
vec![AgentMcpToolPolicyOverride::new(
agent,
policy(&["idea_ticket_update"]),
)],
&classified_tool_names(),
)
.unwrap();
let store = store();
store
.save_mcp_tool_permissions(&project, &doc)
.await
.unwrap();
let loaded = store.load_mcp_tool_permissions(&project).await.unwrap();
assert_eq!(loaded, doc);
let effective = loaded
.effective_policy(agent, READ_ONLY_TOOLS, &classified_tool_names())
.unwrap();
assert!(effective.permits("idea_ticket_update"));
assert!(effective.permits("idea_context_read"));
assert!(effective.permits("idea_memory_read"));
let path = tmp.0.join(".ideai").join("mcp-tool-permissions.json");
assert!(
path.exists(),
"store writes under .ideai/mcp-tool-permissions.json"
);
}
#[tokio::test]
async fn load_rejects_unknown_tool_permissions() {
let tmp = TempDir::new();
let project = project(&tmp);
let path = tmp.0.join(".ideai").join("mcp-tool-permissions.json");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(
path,
r#"{
"version": 1,
"projectDefault": { "allowedTools": ["idea_unknown"] },
"agents": []
}
"#,
)
.unwrap();
let err = store()
.load_mcp_tool_permissions(&project)
.await
.unwrap_err();
assert!(
matches!(err, StoreError::Serialization(ref message) if message.contains("unknown MCP tool: idea_unknown")),
"unknown tools must not become latent permissions, got {err:?}"
);
}
#[tokio::test]
async fn save_rejects_invalid_allowlist_before_writing() {
let tmp = TempDir::new();
let project = project(&tmp);
let invalid = ProjectMcpToolPermissions {
version: MCP_TOOL_PERMISSIONS_VERSION,
project_default: Some(McpToolPolicy {
allowed_tools: vec!["idea_unknown".to_owned()],
}),
agents: Vec::new(),
};
let err = store()
.save_mcp_tool_permissions(&project, &invalid)
.await
.unwrap_err();
assert!(
matches!(err, StoreError::Serialization(ref message) if message.contains("unknown MCP tool: idea_unknown")),
"invalid allowlists must be rejected before persistence, got {err:?}"
);
assert!(!tmp
.0
.join(".ideai")
.join("mcp-tool-permissions.json")
.exists());
}