//! Integration tests for repository and binding CRUD, plus working directory resolution. use dirigent_projects::{ AddRepositoryParams, BindParams, CreateProjectParams, FileBasedProjectStore, ProjectStore, }; use std::path::PathBuf; use uuid::Uuid; async fn make_store() -> FileBasedProjectStore { let dir = tempfile::tempdir().unwrap(); FileBasedProjectStore::new(dir.into_path()).await.unwrap() } fn default_params() -> CreateProjectParams { CreateProjectParams { name: "Test Project".to_string(), description: String::new(), icon: None, owner: Uuid::now_v7(), tags: vec![], languages: vec![], metadata: serde_json::json!({}), } } // ============================================================================ // Repository Tests // ============================================================================ #[tokio::test] async fn test_add_and_list_repositories() { let store = make_store().await; let project = store.create_project(default_params()).await.unwrap(); let repo = store .add_repository(AddRepositoryParams { project_id: project.id, path: PathBuf::from("/home/user/project"), is_primary: false, label: Some("main".to_string()), }) .await .unwrap(); assert_eq!(repo.project_id, project.id); assert_eq!(repo.path, PathBuf::from("/home/user/project")); assert!(!repo.is_primary); assert_eq!(repo.label, Some("main".to_string())); let repos = store.list_repositories(&project.id).await.unwrap(); assert_eq!(repos.len(), 1); assert_eq!(repos[0].id, repo.id); } #[tokio::test] async fn test_add_primary_repository_unsets_others() { let store = make_store().await; let project = store.create_project(default_params()).await.unwrap(); let repo1 = store .add_repository(AddRepositoryParams { project_id: project.id, path: PathBuf::from("/repo1"), is_primary: true, label: None, }) .await .unwrap(); assert!(repo1.is_primary); // Adding a second primary should unset the first let repo2 = store .add_repository(AddRepositoryParams { project_id: project.id, path: PathBuf::from("/repo2"), is_primary: true, label: None, }) .await .unwrap(); assert!(repo2.is_primary); let repos = store.list_repositories(&project.id).await.unwrap(); assert_eq!(repos.len(), 2); let first = repos.iter().find(|r| r.id == repo1.id).unwrap(); let second = repos.iter().find(|r| r.id == repo2.id).unwrap(); assert!(!first.is_primary); assert!(second.is_primary); } #[tokio::test] async fn test_remove_repository() { let store = make_store().await; let project = store.create_project(default_params()).await.unwrap(); let repo = store .add_repository(AddRepositoryParams { project_id: project.id, path: PathBuf::from("/repo"), is_primary: false, label: None, }) .await .unwrap(); store.remove_repository(&repo.id).await.unwrap(); let repos = store.list_repositories(&project.id).await.unwrap(); assert!(repos.is_empty()); } #[tokio::test] async fn test_remove_nonexistent_repository() { let store = make_store().await; let err = store.remove_repository(&Uuid::now_v7()).await.unwrap_err(); assert!(matches!( err, dirigent_projects::ProjectError::RepositoryNotFound(_) )); } #[tokio::test] async fn test_set_primary_repository() { let store = make_store().await; let project = store.create_project(default_params()).await.unwrap(); let repo1 = store .add_repository(AddRepositoryParams { project_id: project.id, path: PathBuf::from("/repo1"), is_primary: true, label: None, }) .await .unwrap(); let repo2 = store .add_repository(AddRepositoryParams { project_id: project.id, path: PathBuf::from("/repo2"), is_primary: false, label: None, }) .await .unwrap(); // Switch primary to repo2 store .set_primary_repository(&project.id, &repo2.id) .await .unwrap(); let repos = store.list_repositories(&project.id).await.unwrap(); let first = repos.iter().find(|r| r.id == repo1.id).unwrap(); let second = repos.iter().find(|r| r.id == repo2.id).unwrap(); assert!(!first.is_primary); assert!(second.is_primary); } #[tokio::test] async fn test_set_primary_nonexistent_repo() { let store = make_store().await; let project = store.create_project(default_params()).await.unwrap(); let err = store .set_primary_repository(&project.id, &Uuid::now_v7()) .await .unwrap_err(); assert!(matches!( err, dirigent_projects::ProjectError::RepositoryNotFound(_) )); } #[tokio::test] async fn test_add_repo_to_nonexistent_project() { let store = make_store().await; let err = store .add_repository(AddRepositoryParams { project_id: Uuid::now_v7(), path: PathBuf::from("/repo"), is_primary: false, label: None, }) .await .unwrap_err(); assert!(matches!(err, dirigent_projects::ProjectError::NotFound(_))); } // ============================================================================ // Working Directory Resolution Tests // ============================================================================ #[tokio::test] async fn test_resolve_working_dir_specific_repo() { let store = make_store().await; let project = store.create_project(default_params()).await.unwrap(); let repo = store .add_repository(AddRepositoryParams { project_id: project.id, path: PathBuf::from("/specific/repo"), is_primary: false, label: None, }) .await .unwrap(); let resolved = store .resolve_working_dir(&project.id, Some(&repo.id)) .await .unwrap(); assert_eq!(resolved, PathBuf::from("/specific/repo")); } #[tokio::test] async fn test_resolve_working_dir_primary_repo() { let store = make_store().await; let project = store.create_project(default_params()).await.unwrap(); store .add_repository(AddRepositoryParams { project_id: project.id, path: PathBuf::from("/secondary"), is_primary: false, label: None, }) .await .unwrap(); store .add_repository(AddRepositoryParams { project_id: project.id, path: PathBuf::from("/primary"), is_primary: true, label: None, }) .await .unwrap(); let resolved = store.resolve_working_dir(&project.id, None).await.unwrap(); assert_eq!(resolved, PathBuf::from("/primary")); } #[tokio::test] async fn test_resolve_working_dir_first_repo_fallback() { let store = make_store().await; let project = store.create_project(default_params()).await.unwrap(); store .add_repository(AddRepositoryParams { project_id: project.id, path: PathBuf::from("/only-repo"), is_primary: false, label: None, }) .await .unwrap(); let resolved = store.resolve_working_dir(&project.id, None).await.unwrap(); assert_eq!(resolved, PathBuf::from("/only-repo")); } #[tokio::test] async fn test_resolve_working_dir_no_repos_errors() { let store = make_store().await; let project = store.create_project(default_params()).await.unwrap(); let err = store .resolve_working_dir(&project.id, None) .await .unwrap_err(); assert!(matches!( err, dirigent_projects::ProjectError::Validation(_) )); } #[tokio::test] async fn test_resolve_working_dir_nonexistent_repo_id() { let store = make_store().await; let project = store.create_project(default_params()).await.unwrap(); store .add_repository(AddRepositoryParams { project_id: project.id, path: PathBuf::from("/repo"), is_primary: true, label: None, }) .await .unwrap(); let err = store .resolve_working_dir(&project.id, Some(&Uuid::now_v7())) .await .unwrap_err(); assert!(matches!( err, dirigent_projects::ProjectError::RepositoryNotFound(_) )); } // ============================================================================ // Binding Tests // ============================================================================ #[tokio::test] async fn test_bind_and_list_bindings() { let store = make_store().await; let project = store.create_project(default_params()).await.unwrap(); let binding = store .bind(BindParams { project_id: project.id, connector_id: Some("opencode-1".to_string()), session_id: None, working_dir: Some(PathBuf::from("/custom/dir")), }) .await .unwrap(); assert_eq!(binding.project_id, project.id); assert_eq!(binding.connector_id, Some("opencode-1".to_string())); assert!(binding.session_id.is_none()); assert_eq!(binding.working_dir, Some(PathBuf::from("/custom/dir"))); let bindings = store.list_bindings(&project.id).await.unwrap(); assert_eq!(bindings.len(), 1); assert_eq!(bindings[0].id, binding.id); } #[tokio::test] async fn test_unbind() { let store = make_store().await; let project = store.create_project(default_params()).await.unwrap(); let binding = store .bind(BindParams { project_id: project.id, connector_id: Some("conn-1".to_string()), session_id: None, working_dir: None, }) .await .unwrap(); store.unbind(&binding.id).await.unwrap(); let bindings = store.list_bindings(&project.id).await.unwrap(); assert!(bindings.is_empty()); } #[tokio::test] async fn test_unbind_nonexistent() { let store = make_store().await; let err = store.unbind(&Uuid::now_v7()).await.unwrap_err(); assert!(matches!( err, dirigent_projects::ProjectError::BindingNotFound(_) )); } #[tokio::test] async fn test_bind_to_nonexistent_project() { let store = make_store().await; let err = store .bind(BindParams { project_id: Uuid::now_v7(), connector_id: Some("conn".to_string()), session_id: None, working_dir: None, }) .await .unwrap_err(); assert!(matches!(err, dirigent_projects::ProjectError::NotFound(_))); } #[tokio::test] async fn test_bind_with_session_id() { let store = make_store().await; let project = store.create_project(default_params()).await.unwrap(); let session_id = Uuid::now_v7(); let binding = store .bind(BindParams { project_id: project.id, connector_id: Some("conn-1".to_string()), session_id: Some(session_id), working_dir: None, }) .await .unwrap(); assert_eq!(binding.session_id, Some(session_id)); } #[tokio::test] async fn test_multiple_bindings_per_project() { let store = make_store().await; let project = store.create_project(default_params()).await.unwrap(); store .bind(BindParams { project_id: project.id, connector_id: Some("conn-1".to_string()), session_id: None, working_dir: None, }) .await .unwrap(); store .bind(BindParams { project_id: project.id, connector_id: Some("conn-2".to_string()), session_id: None, working_dir: None, }) .await .unwrap(); let bindings = store.list_bindings(&project.id).await.unwrap(); assert_eq!(bindings.len(), 2); }