//! 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 = 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()); }