425 lines
12 KiB
Rust
425 lines
12 KiB
Rust
//! 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);
|
|
}
|