#![allow(clippy::unwrap_used)]
#![allow(clippy::panic)]
#![allow(clippy::expect_used)]
use crate::changeset::ChangesetStorage;
use crate::error::{ChangesetError, ChangesetResult};
use crate::types::{ArchiveResult, ArchivedChangeset, Changeset, ReleaseInfo, VersionBump};
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
fn versions_map(versions: Vec<(String, String)>) -> HashMap<String, String> {
versions.into_iter().collect()
}
struct MockStorage {
pending: Arc<RwLock<HashMap<String, Changeset>>>,
archived: Arc<RwLock<HashMap<String, ArchivedChangeset>>>,
}
impl MockStorage {
fn new() -> Self {
Self {
pending: Arc::new(RwLock::new(HashMap::new())),
archived: Arc::new(RwLock::new(HashMap::new())),
}
}
}
#[async_trait]
impl ChangesetStorage for MockStorage {
async fn save(&self, changeset: &Changeset) -> ChangesetResult<()> {
let mut pending = self.pending.write().await;
pending.insert(changeset.branch.clone(), changeset.clone());
Ok(())
}
async fn load(&self, branch: &str) -> ChangesetResult<Changeset> {
let pending = self.pending.read().await;
pending
.get(branch)
.cloned()
.ok_or_else(|| ChangesetError::NotFound { branch: branch.to_string() })
}
async fn exists(&self, branch: &str) -> ChangesetResult<bool> {
let pending = self.pending.read().await;
Ok(pending.contains_key(branch))
}
async fn delete(&self, branch: &str) -> ChangesetResult<()> {
let mut pending = self.pending.write().await;
pending.remove(branch);
Ok(())
}
async fn list_pending(&self) -> ChangesetResult<Vec<Changeset>> {
let pending = self.pending.read().await;
Ok(pending.values().cloned().collect())
}
async fn archive(
&self,
changeset: &Changeset,
release_info: ReleaseInfo,
) -> ChangesetResult<ArchiveResult> {
let mut pending = self.pending.write().await;
let mut archived = self.archived.write().await;
pending.remove(&changeset.branch);
let archived_changeset = ArchivedChangeset::new(changeset.clone(), release_info);
archived.insert(changeset.branch.clone(), archived_changeset);
Ok(ArchiveResult::new(
std::path::PathBuf::from(format!(".changesets/{}.json", changeset.branch)),
std::path::PathBuf::from(format!(".changesets/history/{}.json", changeset.branch)),
))
}
async fn load_archived(&self, branch: &str) -> ChangesetResult<ArchivedChangeset> {
let archived = self.archived.read().await;
archived
.get(branch)
.cloned()
.ok_or_else(|| ChangesetError::NotFound { branch: branch.to_string() })
}
async fn list_archived(&self) -> ChangesetResult<Vec<ArchivedChangeset>> {
let archived = self.archived.read().await;
Ok(archived.values().cloned().collect())
}
}
#[tokio::test]
async fn test_save_and_load() {
let storage = MockStorage::new();
let changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["production".to_string()]);
let result = storage.save(&changeset).await;
assert!(result.is_ok());
let loaded = storage.load("feature/test").await;
assert!(loaded.is_ok());
let loaded = loaded.unwrap();
assert_eq!(loaded.branch, "feature/test");
assert_eq!(loaded.bump, VersionBump::Minor);
assert_eq!(loaded.environments, vec!["production"]);
}
#[tokio::test]
async fn test_load_nonexistent() {
let storage = MockStorage::new();
let result = storage.load("nonexistent").await;
assert!(result.is_err());
match result {
Err(ChangesetError::NotFound { branch }) => {
assert_eq!(branch, "nonexistent");
}
_ => panic!("Expected NotFound error"),
}
}
#[tokio::test]
async fn test_exists() {
let storage = MockStorage::new();
let changeset =
Changeset::new("feature/exists-test", VersionBump::Patch, vec!["staging".to_string()]);
let exists = storage.exists("feature/exists-test").await.unwrap();
assert!(!exists);
storage.save(&changeset).await.unwrap();
let exists = storage.exists("feature/exists-test").await.unwrap();
assert!(exists);
}
#[tokio::test]
async fn test_delete() {
let storage = MockStorage::new();
let changeset =
Changeset::new("feature/delete-test", VersionBump::Major, vec!["production".to_string()]);
storage.save(&changeset).await.unwrap();
assert!(storage.exists("feature/delete-test").await.unwrap());
let result = storage.delete("feature/delete-test").await;
assert!(result.is_ok());
assert!(!storage.exists("feature/delete-test").await.unwrap());
}
#[tokio::test]
async fn test_delete_nonexistent() {
let storage = MockStorage::new();
let result = storage.delete("nonexistent").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_update_changeset() {
let storage = MockStorage::new();
let mut changeset =
Changeset::new("feature/update-test", VersionBump::Minor, vec!["production".to_string()]);
storage.save(&changeset).await.unwrap();
changeset.add_package("package1");
changeset.set_bump(VersionBump::Major);
storage.save(&changeset).await.unwrap();
let loaded = storage.load("feature/update-test").await.unwrap();
assert_eq!(loaded.packages.len(), 1);
assert_eq!(loaded.bump, VersionBump::Major);
}
#[tokio::test]
async fn test_list_pending_empty() {
let storage = MockStorage::new();
let pending = storage.list_pending().await.unwrap();
assert_eq!(pending.len(), 0);
}
#[tokio::test]
async fn test_list_pending_multiple() {
let storage = MockStorage::new();
let changeset1 =
Changeset::new("feature/one", VersionBump::Minor, vec!["production".to_string()]);
let changeset2 = Changeset::new("feature/two", VersionBump::Patch, vec!["staging".to_string()]);
let changeset3 = Changeset::new(
"feature/three",
VersionBump::Major,
vec!["production".to_string(), "staging".to_string()],
);
storage.save(&changeset1).await.unwrap();
storage.save(&changeset2).await.unwrap();
storage.save(&changeset3).await.unwrap();
let pending = storage.list_pending().await.unwrap();
assert_eq!(pending.len(), 3);
let branches: Vec<String> = pending.iter().map(|c| c.branch.clone()).collect();
assert!(branches.contains(&"feature/one".to_string()));
assert!(branches.contains(&"feature/two".to_string()));
assert!(branches.contains(&"feature/three".to_string()));
}
#[tokio::test]
async fn test_archive() {
let storage = MockStorage::new();
let mut changeset =
Changeset::new("feature/archive-test", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("package1");
changeset.add_package("package2");
storage.save(&changeset).await.unwrap();
assert!(storage.exists("feature/archive-test").await.unwrap());
let release_info = ReleaseInfo::new(
"test-user@example.com".to_string(),
"abc123def456".to_string(),
versions_map(vec![
("package1".to_string(), "1.2.0".to_string()),
("package2".to_string(), "2.0.0".to_string()),
]),
);
let result = storage.archive(&changeset, release_info).await;
assert!(result.is_ok());
assert!(!storage.exists("feature/archive-test").await.unwrap());
let archived = storage.load_archived("feature/archive-test").await;
assert!(archived.is_ok());
let archived = archived.unwrap();
assert_eq!(archived.changeset.branch, "feature/archive-test");
assert_eq!(archived.release_info.applied_by, "test-user@example.com");
assert_eq!(archived.release_info.versions.len(), 2);
}
#[tokio::test]
async fn test_load_archived_nonexistent() {
let storage = MockStorage::new();
let result = storage.load_archived("nonexistent").await;
assert!(result.is_err());
match result {
Err(ChangesetError::NotFound { branch }) => {
assert_eq!(branch, "nonexistent");
}
_ => panic!("Expected NotFound error"),
}
}
#[tokio::test]
async fn test_list_archived_empty() {
let storage = MockStorage::new();
let archived = storage.list_archived().await.unwrap();
assert_eq!(archived.len(), 0);
}
#[tokio::test]
async fn test_list_archived_multiple() {
let storage = MockStorage::new();
for i in 1..=3 {
let mut changeset = Changeset::new(
format!("feature/archived-{}", i),
VersionBump::Minor,
vec!["production".to_string()],
);
changeset.add_package(format!("package{}", i));
storage.save(&changeset).await.unwrap();
let release_info = ReleaseInfo::new(
format!("user{}@example.com", i),
format!("commit{}", i),
versions_map(vec![(format!("package{}", i), format!("1.{}.0", i))]),
);
storage.archive(&changeset, release_info).await.unwrap();
}
let archived = storage.list_archived().await.unwrap();
assert_eq!(archived.len(), 3);
for i in 1..=3 {
let branch = format!("feature/archived-{}", i);
let found = archived.iter().any(|a| a.changeset.branch == branch);
assert!(found, "Expected to find archived changeset for {}", branch);
}
}
#[tokio::test]
async fn test_concurrent_access() {
let storage = Arc::new(MockStorage::new());
let mut handles = vec![];
for i in 0..10 {
let storage_clone = Arc::clone(&storage);
let handle = tokio::spawn(async move {
let changeset = Changeset::new(
format!("feature/concurrent-{}", i),
VersionBump::Patch,
vec!["production".to_string()],
);
storage_clone.save(&changeset).await.unwrap();
});
handles.push(handle);
}
for handle in handles {
handle.await.unwrap();
}
let pending = storage.list_pending().await.unwrap();
assert_eq!(pending.len(), 10);
}
#[tokio::test]
async fn test_save_with_packages_and_commits() {
let storage = MockStorage::new();
let mut changeset = Changeset::new(
"feature/complex",
VersionBump::Minor,
vec!["staging".to_string(), "production".to_string()],
);
changeset.add_package("@myorg/core");
changeset.add_package("@myorg/utils");
changeset.add_commit("abc123");
changeset.add_commit("def456");
storage.save(&changeset).await.unwrap();
let loaded = storage.load("feature/complex").await.unwrap();
assert_eq!(loaded.packages.len(), 2);
assert_eq!(loaded.changes.len(), 2);
assert!(loaded.has_package("@myorg/core"));
assert!(loaded.has_package("@myorg/utils"));
assert!(loaded.has_commit("abc123"));
assert!(loaded.has_commit("def456"));
}
#[tokio::test]
async fn test_archive_preserves_all_data() {
let storage = MockStorage::new();
let mut changeset =
Changeset::new("feature/full-data", VersionBump::Major, vec!["production".to_string()]);
changeset.add_package("package1");
changeset.add_package("package2");
changeset.add_commit("commit1");
changeset.add_commit("commit2");
changeset.set_environments(vec!["production".to_string(), "staging".to_string()]);
storage.save(&changeset).await.unwrap();
let release_info = ReleaseInfo::new(
"release-bot@example.com".to_string(),
"release-commit-123".to_string(),
versions_map(vec![
("package1".to_string(), "2.0.0".to_string()),
("package2".to_string(), "3.0.0".to_string()),
]),
);
storage.archive(&changeset, release_info).await.unwrap();
let archived = storage.load_archived("feature/full-data").await.unwrap();
assert_eq!(archived.changeset.packages.len(), 2);
assert_eq!(archived.changeset.changes.len(), 2);
assert_eq!(archived.changeset.environments.len(), 2);
assert_eq!(archived.changeset.bump, VersionBump::Major);
assert_eq!(archived.release_info.versions.len(), 2);
assert_eq!(archived.release_info.git_commit, "release-commit-123".to_string());
}
#[tokio::test]
async fn test_list_pending_excludes_archived() {
let storage = MockStorage::new();
let changeset1 =
Changeset::new("feature/pending", VersionBump::Minor, vec!["production".to_string()]);
let changeset2 =
Changeset::new("feature/archived", VersionBump::Patch, vec!["production".to_string()]);
storage.save(&changeset1).await.unwrap();
storage.save(&changeset2).await.unwrap();
let release_info = ReleaseInfo::new(
"user@example.com".to_string(),
"commit123".to_string(),
versions_map(vec![("pkg".to_string(), "1.0.0".to_string())]),
);
storage.archive(&changeset2, release_info).await.unwrap();
let pending = storage.list_pending().await.unwrap();
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].branch, "feature/pending");
let archived = storage.list_archived().await.unwrap();
assert_eq!(archived.len(), 1);
assert_eq!(archived[0].changeset.branch, "feature/archived");
}
mod file_based_storage_tests {
use super::*;
use crate::changeset::FileBasedChangesetStorage;
use sublime_standard_tools::filesystem::{AsyncFileSystem, FileSystemManager};
use tempfile::TempDir;
async fn setup_file_storage() -> (TempDir, FileBasedChangesetStorage<FileSystemManager>) {
let temp_dir = tempfile::tempdir().unwrap();
let fs = FileSystemManager::new();
let storage = FileBasedChangesetStorage::new(
temp_dir.path().to_path_buf(),
".changesets".to_string(),
".changesets/history".to_string(),
fs,
);
(temp_dir, storage)
}
#[tokio::test]
async fn test_file_save_and_load() {
let (_temp_dir, storage) = setup_file_storage().await;
let changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["production".to_string()]);
storage.save(&changeset).await.unwrap();
let loaded = storage.load("feature/test").await.unwrap();
assert_eq!(loaded.branch, "feature/test");
assert_eq!(loaded.bump, VersionBump::Minor);
assert_eq!(loaded.environments, vec!["production"]);
}
#[tokio::test]
async fn test_file_load_nonexistent() {
let (_temp_dir, storage) = setup_file_storage().await;
let result = storage.load("nonexistent").await;
assert!(result.is_err());
match result {
Err(ChangesetError::NotFound { branch }) => {
assert_eq!(branch, "nonexistent");
}
_ => panic!("Expected NotFound error"),
}
}
#[tokio::test]
async fn test_file_exists() {
let (_temp_dir, storage) = setup_file_storage().await;
let changeset =
Changeset::new("feature/exists-test", VersionBump::Patch, vec!["staging".to_string()]);
let exists = storage.exists("feature/exists-test").await.unwrap();
assert!(!exists);
storage.save(&changeset).await.unwrap();
let exists = storage.exists("feature/exists-test").await.unwrap();
assert!(exists);
}
#[tokio::test]
async fn test_file_delete() {
let (_temp_dir, storage) = setup_file_storage().await;
let changeset = Changeset::new(
"feature/delete-test",
VersionBump::Major,
vec!["production".to_string()],
);
storage.save(&changeset).await.unwrap();
assert!(storage.exists("feature/delete-test").await.unwrap());
storage.delete("feature/delete-test").await.unwrap();
assert!(!storage.exists("feature/delete-test").await.unwrap());
}
#[tokio::test]
async fn test_file_delete_nonexistent() {
let (_temp_dir, storage) = setup_file_storage().await;
let result = storage.delete("nonexistent").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_file_update_changeset() {
let (_temp_dir, storage) = setup_file_storage().await;
let mut changeset = Changeset::new(
"feature/update-test",
VersionBump::Minor,
vec!["production".to_string()],
);
storage.save(&changeset).await.unwrap();
changeset.add_package("package1");
changeset.set_bump(VersionBump::Major);
storage.save(&changeset).await.unwrap();
let loaded = storage.load("feature/update-test").await.unwrap();
assert_eq!(loaded.packages.len(), 1);
assert_eq!(loaded.bump, VersionBump::Major);
}
#[tokio::test]
async fn test_file_list_pending_empty() {
let (_temp_dir, storage) = setup_file_storage().await;
let pending = storage.list_pending().await.unwrap();
assert_eq!(pending.len(), 0);
}
#[tokio::test]
async fn test_file_list_pending_multiple() {
let (_temp_dir, storage) = setup_file_storage().await;
let changeset1 =
Changeset::new("feature/one", VersionBump::Minor, vec!["production".to_string()]);
let changeset2 =
Changeset::new("feature/two", VersionBump::Patch, vec!["staging".to_string()]);
let changeset3 = Changeset::new(
"feature/three",
VersionBump::Major,
vec!["production".to_string(), "staging".to_string()],
);
storage.save(&changeset1).await.unwrap();
storage.save(&changeset2).await.unwrap();
storage.save(&changeset3).await.unwrap();
let pending = storage.list_pending().await.unwrap();
assert_eq!(pending.len(), 3);
let branches: Vec<String> = pending.iter().map(|c| c.branch.clone()).collect();
assert!(branches.contains(&"feature/one".to_string()));
assert!(branches.contains(&"feature/two".to_string()));
assert!(branches.contains(&"feature/three".to_string()));
}
#[tokio::test]
async fn test_file_archive() {
let (_temp_dir, storage) = setup_file_storage().await;
let mut changeset = Changeset::new(
"feature/archive-test",
VersionBump::Minor,
vec!["production".to_string()],
);
changeset.add_package("package1");
changeset.add_package("package2");
storage.save(&changeset).await.unwrap();
assert!(storage.exists("feature/archive-test").await.unwrap());
let release_info = ReleaseInfo::new(
"test-user@example.com".to_string(),
"abc123def456".to_string(),
versions_map(vec![
("package1".to_string(), "1.2.0".to_string()),
("package2".to_string(), "2.0.0".to_string()),
]),
);
storage.archive(&changeset, release_info).await.unwrap();
assert!(!storage.exists("feature/archive-test").await.unwrap());
let archived = storage.load_archived("feature/archive-test").await.unwrap();
assert_eq!(archived.changeset.branch, "feature/archive-test");
assert_eq!(archived.release_info.applied_by, "test-user@example.com");
assert_eq!(archived.release_info.versions.len(), 2);
}
#[tokio::test]
async fn test_file_archive_already_exists() {
let (_temp_dir, storage) = setup_file_storage().await;
let changeset =
Changeset::new("feature/duplicate", VersionBump::Minor, vec!["production".to_string()]);
storage.save(&changeset).await.unwrap();
let release_info = ReleaseInfo::new(
"user@example.com".to_string(),
"commit1".to_string(),
versions_map(vec![]),
);
storage.archive(&changeset, release_info.clone()).await.unwrap();
let result = storage.archive(&changeset, release_info).await;
assert!(result.is_err());
match result {
Err(ChangesetError::AlreadyExists { branch, .. }) => {
assert_eq!(branch, "feature/duplicate");
}
_ => panic!("Expected AlreadyExists error"),
}
}
#[tokio::test]
async fn test_file_load_archived_nonexistent() {
let (_temp_dir, storage) = setup_file_storage().await;
let result = storage.load_archived("nonexistent").await;
assert!(result.is_err());
match result {
Err(ChangesetError::NotFound { branch }) => {
assert_eq!(branch, "nonexistent");
}
_ => panic!("Expected NotFound error"),
}
}
#[tokio::test]
async fn test_file_list_archived_empty() {
let (_temp_dir, storage) = setup_file_storage().await;
let archived = storage.list_archived().await.unwrap();
assert_eq!(archived.len(), 0);
}
#[tokio::test]
async fn test_file_list_archived_multiple() {
let (_temp_dir, storage) = setup_file_storage().await;
for i in 1..=3 {
let mut changeset = Changeset::new(
format!("feature/archived-{}", i),
VersionBump::Minor,
vec!["production".to_string()],
);
changeset.add_package(format!("package{}", i));
storage.save(&changeset).await.unwrap();
let release_info = ReleaseInfo::new(
format!("user{}@example.com", i),
format!("commit{}", i),
versions_map(vec![(format!("package{}", i), format!("1.{}.0", i))]),
);
storage.archive(&changeset, release_info).await.unwrap();
}
let archived = storage.list_archived().await.unwrap();
assert_eq!(archived.len(), 3);
for i in 1..=3 {
let branch = format!("feature/archived-{}", i);
let found = archived.iter().any(|a| a.changeset.branch == branch);
assert!(found, "Expected to find archived changeset for {}", branch);
}
}
#[tokio::test]
async fn test_file_sanitize_branch_names() {
let (_temp_dir, storage) = setup_file_storage().await;
let branches = vec![
"feature/new-api",
"bugfix\\windows-path",
"hotfix:critical",
"feature*wildcard",
"test?question",
"branch\"quotes",
"branch<greater",
"branch>less",
"branch|pipe",
];
for branch in branches {
let changeset =
Changeset::new(branch, VersionBump::Patch, vec!["production".to_string()]);
storage.save(&changeset).await.unwrap();
let loaded = storage.load(branch).await.unwrap();
assert_eq!(loaded.branch, branch);
}
}
#[tokio::test]
async fn test_file_with_packages_and_commits() {
let (_temp_dir, storage) = setup_file_storage().await;
let mut changeset = Changeset::new(
"feature/complex",
VersionBump::Minor,
vec!["staging".to_string(), "production".to_string()],
);
changeset.add_package("@myorg/core");
changeset.add_package("@myorg/utils");
changeset.add_commit("abc123");
changeset.add_commit("def456");
storage.save(&changeset).await.unwrap();
let loaded = storage.load("feature/complex").await.unwrap();
assert_eq!(loaded.packages.len(), 2);
assert_eq!(loaded.changes.len(), 2);
assert!(loaded.has_package("@myorg/core"));
assert!(loaded.has_package("@myorg/utils"));
assert!(loaded.has_commit("abc123"));
assert!(loaded.has_commit("def456"));
}
#[tokio::test]
async fn test_file_archive_preserves_all_data() {
let (_temp_dir, storage) = setup_file_storage().await;
let mut changeset =
Changeset::new("feature/full-data", VersionBump::Major, vec!["production".to_string()]);
changeset.add_package("package1");
changeset.add_package("package2");
changeset.add_commit("commit1");
changeset.add_commit("commit2");
changeset.set_environments(vec!["production".to_string(), "staging".to_string()]);
storage.save(&changeset).await.unwrap();
let release_info = ReleaseInfo::new(
"release-bot@example.com".to_string(),
"release-commit-123".to_string(),
versions_map(vec![
("package1".to_string(), "2.0.0".to_string()),
("package2".to_string(), "3.0.0".to_string()),
]),
);
storage.archive(&changeset, release_info).await.unwrap();
let archived = storage.load_archived("feature/full-data").await.unwrap();
assert_eq!(archived.changeset.packages.len(), 2);
assert_eq!(archived.changeset.changes.len(), 2);
assert_eq!(archived.changeset.environments.len(), 2);
assert_eq!(archived.changeset.bump, VersionBump::Major);
assert_eq!(archived.release_info.versions.len(), 2);
assert_eq!(archived.release_info.git_commit, "release-commit-123".to_string());
}
#[tokio::test]
async fn test_file_list_pending_excludes_archived() {
let (_temp_dir, storage) = setup_file_storage().await;
let changeset1 =
Changeset::new("feature/pending", VersionBump::Minor, vec!["production".to_string()]);
let changeset2 =
Changeset::new("feature/archived", VersionBump::Patch, vec!["production".to_string()]);
storage.save(&changeset1).await.unwrap();
storage.save(&changeset2).await.unwrap();
let release_info = ReleaseInfo::new(
"user@example.com".to_string(),
"commit123".to_string(),
versions_map(vec![("pkg".to_string(), "1.0.0".to_string())]),
);
storage.archive(&changeset2, release_info).await.unwrap();
let pending = storage.list_pending().await.unwrap();
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].branch, "feature/pending");
let archived = storage.list_archived().await.unwrap();
assert_eq!(archived.len(), 1);
assert_eq!(archived[0].changeset.branch, "feature/archived");
}
#[tokio::test]
async fn test_file_persistence_across_instances() {
let temp_dir = tempfile::tempdir().unwrap();
let fs = FileSystemManager::new();
{
let storage = FileBasedChangesetStorage::new(
temp_dir.path().to_path_buf(),
".changesets".to_string(),
".changesets/history".to_string(),
fs.clone(),
);
let changeset = Changeset::new(
"feature/persist",
VersionBump::Minor,
vec!["production".to_string()],
);
storage.save(&changeset).await.unwrap();
}
{
let storage = FileBasedChangesetStorage::new(
temp_dir.path().to_path_buf(),
".changesets".to_string(),
".changesets/history".to_string(),
fs,
);
let loaded = storage.load("feature/persist").await.unwrap();
assert_eq!(loaded.branch, "feature/persist");
}
}
#[tokio::test]
async fn test_file_json_format_readable() {
let temp_dir = tempfile::tempdir().unwrap();
let fs = FileSystemManager::new();
let storage = FileBasedChangesetStorage::new(
temp_dir.path().to_path_buf(),
".changesets".to_string(),
".changesets/history".to_string(),
fs.clone(),
);
let mut changeset =
Changeset::new("feature/readable", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("test-package");
storage.save(&changeset).await.unwrap();
let path = temp_dir.path().join(".changesets").join("feature-readable.json");
let contents = fs.read_file_string(&path).await.unwrap();
let json: serde_json::Value = serde_json::from_str(&contents).unwrap();
assert_eq!(json["branch"], "feature/readable");
assert_eq!(json["bump"], "minor");
assert!(json["packages"].is_array());
}
#[tokio::test]
async fn test_file_concurrent_saves() {
let temp_dir = tempfile::tempdir().unwrap();
let fs = FileSystemManager::new();
let storage = Arc::new(FileBasedChangesetStorage::new(
temp_dir.path().to_path_buf(),
".changesets".to_string(),
".changesets/history".to_string(),
fs,
));
let mut handles = vec![];
for i in 0..10 {
let storage_clone = Arc::clone(&storage);
let handle = tokio::spawn(async move {
let changeset = Changeset::new(
format!("feature/concurrent-{}", i),
VersionBump::Patch,
vec!["production".to_string()],
);
storage_clone.save(&changeset).await.unwrap();
});
handles.push(handle);
}
for handle in handles {
handle.await.unwrap();
}
let pending = storage.list_pending().await.unwrap();
assert_eq!(pending.len(), 10);
}
#[tokio::test]
async fn test_file_empty_packages_list() {
let (_temp_dir, storage) = setup_file_storage().await;
let changeset = Changeset::new(
"feature/no-packages",
VersionBump::None,
vec!["production".to_string()],
);
storage.save(&changeset).await.unwrap();
let loaded = storage.load("feature/no-packages").await.unwrap();
assert!(loaded.packages.is_empty());
assert_eq!(loaded.bump, VersionBump::None);
}
#[tokio::test]
async fn test_file_timestamps_preserved() {
let (_temp_dir, storage) = setup_file_storage().await;
let changeset = Changeset::new(
"feature/timestamps",
VersionBump::Minor,
vec!["production".to_string()],
);
let created_at = changeset.created_at;
let updated_at = changeset.updated_at;
storage.save(&changeset).await.unwrap();
let loaded = storage.load("feature/timestamps").await.unwrap();
assert_eq!(loaded.created_at, created_at);
assert_eq!(loaded.updated_at, updated_at);
}
#[tokio::test]
async fn test_file_list_ignores_non_json_files() {
let temp_dir = tempfile::tempdir().unwrap();
let fs = FileSystemManager::new();
let storage = FileBasedChangesetStorage::new(
temp_dir.path().to_path_buf(),
".changesets".to_string(),
".changesets/history".to_string(),
fs.clone(),
);
let changeset =
Changeset::new("feature/valid", VersionBump::Minor, vec!["production".to_string()]);
storage.save(&changeset).await.unwrap();
let dir_path = temp_dir.path().join(".changesets");
let non_json_path = dir_path.join("README.md");
fs.write_file_string(&non_json_path, "# Changesets").await.unwrap();
let pending = storage.list_pending().await.unwrap();
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].branch, "feature/valid");
}
}
mod manager_tests {
use super::*;
use crate::changeset::ChangesetManager;
use crate::config::ChangesetConfig;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use sublime_standard_tools::filesystem::FileSystemManager;
#[derive(Debug, Clone)]
pub(super) struct MockManagerStorage {
changesets: Arc<Mutex<HashMap<String, Changeset>>>,
archived: Arc<Mutex<HashMap<String, ArchivedChangeset>>>,
}
impl MockManagerStorage {
fn new() -> Self {
Self {
changesets: Arc::new(Mutex::new(HashMap::new())),
archived: Arc::new(Mutex::new(HashMap::new())),
}
}
fn get_count(&self) -> usize {
self.changesets.lock().unwrap().len()
}
}
#[async_trait]
impl ChangesetStorage for MockManagerStorage {
async fn save(&self, changeset: &Changeset) -> ChangesetResult<()> {
self.changesets.lock().unwrap().insert(changeset.branch.clone(), changeset.clone());
Ok(())
}
async fn load(&self, branch: &str) -> ChangesetResult<Changeset> {
self.changesets
.lock()
.unwrap()
.get(branch)
.cloned()
.ok_or_else(|| ChangesetError::NotFound { branch: branch.to_string() })
}
async fn exists(&self, branch: &str) -> ChangesetResult<bool> {
Ok(self.changesets.lock().unwrap().contains_key(branch))
}
async fn delete(&self, branch: &str) -> ChangesetResult<()> {
self.changesets
.lock()
.unwrap()
.remove(branch)
.ok_or_else(|| ChangesetError::NotFound { branch: branch.to_string() })?;
Ok(())
}
async fn list_pending(&self) -> ChangesetResult<Vec<Changeset>> {
Ok(self.changesets.lock().unwrap().values().cloned().collect())
}
async fn archive(
&self,
changeset: &Changeset,
release_info: ReleaseInfo,
) -> ChangesetResult<ArchiveResult> {
self.changesets.lock().unwrap().remove(&changeset.branch);
let archived_changeset = ArchivedChangeset::new(changeset.clone(), release_info);
self.archived.lock().unwrap().insert(changeset.branch.clone(), archived_changeset);
Ok(ArchiveResult::new(
std::path::PathBuf::from(format!(".changesets/{}.json", changeset.branch)),
std::path::PathBuf::from(format!(".changesets/history/{}.json", changeset.branch)),
))
}
async fn load_archived(&self, branch: &str) -> ChangesetResult<ArchivedChangeset> {
self.archived
.lock()
.unwrap()
.get(branch)
.cloned()
.ok_or_else(|| ChangesetError::NotFound { branch: branch.to_string() })
}
async fn list_archived(&self) -> ChangesetResult<Vec<ArchivedChangeset>> {
Ok(self.archived.lock().unwrap().values().cloned().collect())
}
}
fn create_test_config() -> crate::config::PackageToolsConfig {
crate::config::PackageToolsConfig {
changeset: ChangesetConfig {
path: ".changesets".into(),
history_path: ".changesets/history".into(),
available_environments: vec![
"development".to_string(),
"staging".to_string(),
"production".to_string(),
],
default_environments: vec!["production".to_string()],
},
..Default::default()
}
}
pub(super) fn create_test_manager() -> ChangesetManager<MockManagerStorage> {
let storage = MockManagerStorage::new();
let config = create_test_config();
let workspace_root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
ChangesetManager::with_storage(storage, workspace_root, None, config)
}
#[tokio::test]
async fn test_create_changeset_success() {
let manager = create_test_manager();
let result = manager
.create("feature/test", VersionBump::Minor, vec!["production".to_string()])
.await;
assert!(result.is_ok());
let changeset = result.unwrap();
assert_eq!(changeset.branch, "feature/test");
assert_eq!(changeset.bump, VersionBump::Minor);
assert_eq!(changeset.environments, vec!["production".to_string()]);
}
#[tokio::test]
async fn test_create_changeset_empty_branch() {
let manager = create_test_manager();
let result = manager.create("", VersionBump::Minor, vec!["production".to_string()]).await;
assert!(result.is_err());
match result.unwrap_err() {
ChangesetError::InvalidBranch { branch, reason } => {
assert_eq!(branch, "");
assert!(reason.contains("empty"));
}
_ => panic!("Expected InvalidBranch error"),
}
}
#[tokio::test]
async fn test_create_changeset_already_exists() {
let manager = create_test_manager();
manager
.create("feature/test", VersionBump::Minor, vec!["production".to_string()])
.await
.unwrap();
let result = manager
.create("feature/test", VersionBump::Patch, vec!["production".to_string()])
.await;
assert!(result.is_err());
match result.unwrap_err() {
ChangesetError::AlreadyExists { branch, .. } => {
assert_eq!(branch, "feature/test");
}
_ => panic!("Expected AlreadyExists error"),
}
}
#[tokio::test]
async fn test_create_changeset_invalid_environment() {
let manager = create_test_manager();
let result = manager
.create("feature/test", VersionBump::Minor, vec!["invalid-env".to_string()])
.await;
assert!(result.is_err());
match result.unwrap_err() {
ChangesetError::InvalidEnvironment { environment, available } => {
assert_eq!(environment, "invalid-env");
assert!(available.contains(&"production".to_string()));
}
_ => panic!("Expected InvalidEnvironment error"),
}
}
#[tokio::test]
async fn test_load_changeset_success() {
let manager = create_test_manager();
manager
.create("feature/test", VersionBump::Minor, vec!["production".to_string()])
.await
.unwrap();
let result = manager.load("feature/test").await;
assert!(result.is_ok());
let changeset = result.unwrap();
assert_eq!(changeset.branch, "feature/test");
assert_eq!(changeset.bump, VersionBump::Minor);
}
#[tokio::test]
async fn test_load_changeset_not_found() {
let manager = create_test_manager();
let result = manager.load("nonexistent").await;
assert!(result.is_err());
match result.unwrap_err() {
ChangesetError::NotFound { branch } => {
assert_eq!(branch, "nonexistent");
}
_ => panic!("Expected NotFound error"),
}
}
#[tokio::test]
async fn test_update_changeset_success() {
let manager = create_test_manager();
let changeset = manager
.create("feature/test", VersionBump::Minor, vec!["production".to_string()])
.await
.unwrap();
let original_updated_at = changeset.updated_at;
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
let mut modified = changeset.clone();
modified.add_package("test-package");
let result = manager.update(&modified).await;
assert!(result.is_ok());
let loaded = manager.load("feature/test").await.unwrap();
assert!(loaded.packages.contains(&"test-package".to_string()));
assert!(loaded.updated_at > original_updated_at);
}
#[tokio::test]
async fn test_update_changeset_validation_failure() {
let manager = create_test_manager();
let changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["invalid-env".to_string()]);
let result = manager.update(&changeset).await;
assert!(result.is_err());
match result.unwrap_err() {
ChangesetError::ValidationFailed { .. } => {
}
_ => panic!("Expected ValidationFailed error"),
}
}
#[tokio::test]
async fn test_delete_changeset_success() {
let manager = create_test_manager();
manager
.create("feature/test", VersionBump::Minor, vec!["production".to_string()])
.await
.unwrap();
let result = manager.delete("feature/test").await;
assert!(result.is_ok());
let load_result = manager.load("feature/test").await;
assert!(load_result.is_err());
}
#[tokio::test]
async fn test_delete_changeset_not_found() {
let manager = create_test_manager();
let result = manager.delete("nonexistent").await;
assert!(result.is_err());
match result.unwrap_err() {
ChangesetError::NotFound { branch } => {
assert_eq!(branch, "nonexistent");
}
_ => panic!("Expected NotFound error"),
}
}
#[tokio::test]
async fn test_list_pending_empty() {
let manager = create_test_manager();
let result = manager.list_pending().await;
assert!(result.is_ok());
let changesets = result.unwrap();
assert_eq!(changesets.len(), 0);
}
#[tokio::test]
async fn test_list_pending_multiple() {
let manager = create_test_manager();
manager
.create("feature/one", VersionBump::Minor, vec!["production".to_string()])
.await
.unwrap();
manager
.create("feature/two", VersionBump::Patch, vec!["staging".to_string()])
.await
.unwrap();
manager
.create("feature/three", VersionBump::Major, vec!["development".to_string()])
.await
.unwrap();
let result = manager.list_pending().await;
assert!(result.is_ok());
let changesets = result.unwrap();
assert_eq!(changesets.len(), 3);
let branches: Vec<&str> = changesets.iter().map(|cs| cs.branch.as_str()).collect();
assert!(branches.contains(&"feature/one"));
assert!(branches.contains(&"feature/two"));
assert!(branches.contains(&"feature/three"));
}
#[tokio::test]
async fn test_manager_accessors() {
let manager = create_test_manager();
let storage = manager.storage();
assert_eq!(storage.get_count(), 0);
assert!(manager.git_repo().is_none());
let config = manager.config();
assert_eq!(config.path, ".changesets");
assert_eq!(config.available_environments.len(), 3);
}
#[tokio::test]
async fn test_update_with_multiple_modifications() {
let manager = create_test_manager();
let changeset = manager
.create("feature/test", VersionBump::Minor, vec!["production".to_string()])
.await
.unwrap();
let mut modified = changeset.clone();
modified.add_package("package-1");
modified.add_package("package-2");
modified.add_commit("abc123");
modified.add_commit("def456");
modified.set_bump(VersionBump::Major);
manager.update(&modified).await.unwrap();
let loaded = manager.load("feature/test").await.unwrap();
assert_eq!(loaded.packages.len(), 2);
assert!(loaded.packages.contains(&"package-1".to_string()));
assert!(loaded.packages.contains(&"package-2".to_string()));
assert_eq!(loaded.changes.len(), 2);
assert!(loaded.changes.contains(&"abc123".to_string()));
assert!(loaded.changes.contains(&"def456".to_string()));
assert_eq!(loaded.bump, VersionBump::Major);
}
#[tokio::test]
async fn test_create_with_multiple_environments() {
let manager = create_test_manager();
let changeset = manager
.create(
"feature/test",
VersionBump::Minor,
vec!["development".to_string(), "staging".to_string(), "production".to_string()],
)
.await
.unwrap();
assert_eq!(changeset.environments.len(), 3);
assert!(changeset.environments.contains(&"development".to_string()));
assert!(changeset.environments.contains(&"staging".to_string()));
assert!(changeset.environments.contains(&"production".to_string()));
}
#[tokio::test]
async fn test_manager_with_file_based_storage() {
let temp_dir = tempfile::tempdir().unwrap();
let fs = FileSystemManager::new();
let config = create_test_config();
let manager =
ChangesetManager::new(temp_dir.path().to_path_buf(), fs, config).await.unwrap();
let changeset = manager
.create("feature/file-test", VersionBump::Minor, vec!["production".to_string()])
.await
.unwrap();
let loaded = manager.load("feature/file-test").await.unwrap();
assert_eq!(loaded.branch, changeset.branch);
assert_eq!(loaded.bump, changeset.bump);
let mut modified = loaded.clone();
modified.add_package("test-package");
manager.update(&modified).await.unwrap();
let updated = manager.load("feature/file-test").await.unwrap();
assert!(updated.packages.contains(&"test-package".to_string()));
manager.delete("feature/file-test").await.unwrap();
let result = manager.load("feature/file-test").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_create_validates_all_environments() {
let manager = create_test_manager();
let result = manager
.create(
"feature/test",
VersionBump::Minor,
vec!["production".to_string(), "invalid".to_string()],
)
.await;
assert!(result.is_err());
match result.unwrap_err() {
ChangesetError::InvalidEnvironment { environment, .. } => {
assert_eq!(environment, "invalid");
}
_ => panic!("Expected InvalidEnvironment error"),
}
}
#[tokio::test]
async fn test_list_pending_returns_loaded_changesets() {
let manager = create_test_manager();
manager
.create("feature/one", VersionBump::Major, vec!["production".to_string()])
.await
.unwrap();
manager
.create("feature/two", VersionBump::Minor, vec!["staging".to_string()])
.await
.unwrap();
let changesets = manager.list_pending().await.unwrap();
assert_eq!(changesets.len(), 2);
for changeset in changesets {
assert!(!changeset.branch.is_empty());
assert!(!changeset.environments.is_empty());
}
}
#[tokio::test]
async fn test_manager_add_commits() {
let manager = create_test_manager();
let mut changeset = manager
.create("feature/test", VersionBump::Minor, vec!["production".to_string()])
.await
.unwrap();
changeset.add_package("test-package");
manager.update(&changeset).await.unwrap();
let commits = vec!["abc123".to_string(), "def456".to_string()];
let summary = manager.add_commits("feature/test", commits).await.unwrap();
assert_eq!(summary.commits_added, 2);
assert_eq!(summary.commit_ids.len(), 2);
assert!(summary.new_packages.is_empty());
let changeset = manager.load("feature/test").await.unwrap();
assert_eq!(changeset.changes.len(), 2);
assert!(changeset.has_commit("abc123"));
assert!(changeset.has_commit("def456"));
}
#[tokio::test]
async fn test_manager_add_commits_duplicate() {
let manager = create_test_manager();
let mut changeset = manager
.create("feature/test", VersionBump::Minor, vec!["production".to_string()])
.await
.unwrap();
changeset.add_package("test-package");
manager.update(&changeset).await.unwrap();
let commits = vec!["abc123".to_string(), "def456".to_string()];
manager.add_commits("feature/test", commits).await.unwrap();
let commits = vec!["abc123".to_string(), "ghi789".to_string()];
let summary = manager.add_commits("feature/test", commits).await.unwrap();
assert_eq!(summary.commits_added, 1);
assert_eq!(summary.commit_ids, vec!["ghi789".to_string()]);
let changeset = manager.load("feature/test").await.unwrap();
assert_eq!(changeset.changes.len(), 3);
}
#[tokio::test]
async fn test_manager_add_commits_empty() {
let manager = create_test_manager();
let mut changeset = manager
.create("feature/test", VersionBump::Minor, vec!["production".to_string()])
.await
.unwrap();
changeset.add_package("test-package");
manager.update(&changeset).await.unwrap();
let summary = manager.add_commits("feature/test", vec![]).await.unwrap();
assert_eq!(summary.commits_added, 0);
assert!(summary.commit_ids.is_empty());
}
#[tokio::test]
async fn test_manager_add_commits_nonexistent_changeset() {
let manager = create_test_manager();
let result = manager.add_commits("nonexistent", vec!["abc123".to_string()]).await;
assert!(result.is_err());
match result.unwrap_err() {
ChangesetError::NotFound { branch } => {
assert_eq!(branch, "nonexistent");
}
_ => panic!("Expected NotFound error"),
}
}
#[tokio::test]
async fn test_manager_archive_success() {
let manager = create_test_manager();
let mut changeset = manager
.create("feature/archive-test", VersionBump::Major, vec!["production".to_string()])
.await
.unwrap();
changeset.add_package("@myorg/core");
changeset.add_package("@myorg/utils");
manager.update(&changeset).await.unwrap();
let mut versions = HashMap::new();
versions.insert("@myorg/core".to_string(), "2.0.0".to_string());
versions.insert("@myorg/utils".to_string(), "1.5.0".to_string());
let release_info = crate::types::ReleaseInfo::new(
"ci-bot@example.com".to_string(),
"abc123def456789".to_string(),
versions,
);
let result = manager.archive("feature/archive-test", release_info).await;
assert!(result.is_ok(), "Archive should succeed");
let exists = manager.storage().exists("feature/archive-test").await.unwrap();
assert!(!exists, "Changeset should not exist in pending after archiving");
let archived = manager.storage().load_archived("feature/archive-test").await;
assert!(archived.is_ok(), "Archived changeset should be loadable");
let archived = archived.unwrap();
assert_eq!(archived.changeset.branch, "feature/archive-test");
assert_eq!(archived.changeset.bump, VersionBump::Major);
assert_eq!(archived.release_info.applied_by, "ci-bot@example.com");
assert_eq!(archived.release_info.git_commit, "abc123def456789");
assert_eq!(archived.release_info.package_count(), 2);
assert_eq!(archived.release_info.get_version("@myorg/core"), Some("2.0.0"));
assert_eq!(archived.release_info.get_version("@myorg/utils"), Some("1.5.0"));
}
#[tokio::test]
async fn test_manager_archive_nonexistent_changeset() {
let manager = create_test_manager();
let versions = HashMap::new();
let release_info = crate::types::ReleaseInfo::new(
"user@example.com".to_string(),
"commit123".to_string(),
versions,
);
let result = manager.archive("nonexistent", release_info).await;
assert!(result.is_err(), "Archive should fail for nonexistent changeset");
match result.unwrap_err() {
ChangesetError::NotFound { branch } => {
assert_eq!(branch, "nonexistent");
}
_ => panic!("Expected NotFound error"),
}
}
}
mod git_integration_tests {
use crate::changeset::{ChangesetManager, FileBasedChangesetStorage, PackageDetector};
use crate::config::ChangesetConfig;
use crate::error::ChangesetError;
use crate::types::VersionBump;
use std::fs;
use sublime_git_tools::Repo;
use sublime_standard_tools::filesystem::FileSystemManager;
use tempfile::TempDir;
fn setup_git_repo() -> (TempDir, Repo) {
let temp_dir = TempDir::new().unwrap();
let repo_path = temp_dir.path();
let repo = Repo::create(repo_path.to_str().unwrap()).unwrap();
repo.config("user.name", "Test User").unwrap();
repo.config("user.email", "test@example.com").unwrap();
fs::write(repo_path.join("README.md"), "# Test Repo").unwrap();
repo.add("README.md").unwrap();
repo.commit("Initial commit").unwrap();
(temp_dir, repo)
}
fn setup_monorepo(repo_path: &std::path::Path) {
let root_package = serde_json::json!({
"name": "@test/monorepo",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "echo 'Building all packages...'"
}
});
fs::write(
repo_path.join("package.json"),
serde_json::to_string_pretty(&root_package).unwrap(),
)
.unwrap();
let pnpm_workspace = "packages:\n - 'packages/*'\n";
fs::write(repo_path.join("pnpm-workspace.yaml"), pnpm_workspace).unwrap();
fs::create_dir_all(repo_path.join("packages")).unwrap();
fs::create_dir_all(repo_path.join("packages/package1/src")).unwrap();
let pkg1 = serde_json::json!({
"name": "@test/package1",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"build": "echo 'Building package1...'"
}
});
fs::write(
repo_path.join("packages/package1/package.json"),
serde_json::to_string_pretty(&pkg1).unwrap(),
)
.unwrap();
fs::write(
repo_path.join("packages/package1/src/index.js"),
"module.exports = { name: 'package1' };\n",
)
.unwrap();
fs::create_dir_all(repo_path.join("packages/package2/src")).unwrap();
let pkg2 = serde_json::json!({
"name": "@test/package2",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"build": "echo 'Building package2...'"
},
"dependencies": {
"@test/package1": "workspace:*"
}
});
fs::write(
repo_path.join("packages/package2/package.json"),
serde_json::to_string_pretty(&pkg2).unwrap(),
)
.unwrap();
fs::write(
repo_path.join("packages/package2/src/index.js"),
"module.exports = { name: 'package2' };\n",
)
.unwrap();
}
fn setup_single_package(repo_path: &std::path::Path) {
let package = serde_json::json!({
"name": "single-package",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"build": "echo 'Building...'"
}
});
fs::write(repo_path.join("package.json"), serde_json::to_string_pretty(&package).unwrap())
.unwrap();
fs::create_dir_all(repo_path.join("src")).unwrap();
fs::write(repo_path.join("src/index.js"), "module.exports = { name: 'single-package' };\n")
.unwrap();
}
#[tokio::test]
async fn test_package_detector_is_monorepo() {
let (temp_dir, repo) = setup_git_repo();
setup_monorepo(temp_dir.path());
repo.add_all().unwrap();
repo.commit("Setup monorepo").unwrap();
let detector =
PackageDetector::new(temp_dir.path().to_path_buf(), &repo, FileSystemManager::new());
let is_monorepo = detector.is_monorepo().await.unwrap();
assert!(is_monorepo);
}
#[tokio::test]
async fn test_package_detector_is_not_monorepo() {
let (temp_dir, repo) = setup_git_repo();
setup_single_package(temp_dir.path());
repo.add_all().unwrap();
repo.commit("Setup single package").unwrap();
let detector =
PackageDetector::new(temp_dir.path().to_path_buf(), &repo, FileSystemManager::new());
let is_monorepo = detector.is_monorepo().await.unwrap();
assert!(!is_monorepo);
}
#[tokio::test]
async fn test_package_detector_list_packages_monorepo() {
let (temp_dir, repo) = setup_git_repo();
setup_monorepo(temp_dir.path());
repo.add_all().unwrap();
repo.commit("Setup monorepo").unwrap();
let detector =
PackageDetector::new(temp_dir.path().to_path_buf(), &repo, FileSystemManager::new());
let packages = detector.list_packages().await.unwrap();
assert_eq!(packages.len(), 2, "Should detect both packages in monorepo");
assert!(packages.contains(&"@test/package1".to_string()));
assert!(packages.contains(&"@test/package2".to_string()));
}
#[tokio::test]
async fn test_package_detector_list_packages_single() {
let (temp_dir, repo) = setup_git_repo();
setup_single_package(temp_dir.path());
repo.add_all().unwrap();
repo.commit("Setup single package").unwrap();
let detector =
PackageDetector::new(temp_dir.path().to_path_buf(), &repo, FileSystemManager::new());
let packages = detector.list_packages().await.unwrap();
assert_eq!(packages.len(), 1);
assert_eq!(packages[0], "single-package");
}
#[tokio::test]
async fn test_package_detector_detect_affected_packages_monorepo() {
let (temp_dir, repo) = setup_git_repo();
setup_monorepo(temp_dir.path());
repo.add_all().unwrap();
repo.commit("Setup monorepo").unwrap();
fs::write(temp_dir.path().join("packages/package1/src/index.js"), "console.log('hello');")
.unwrap();
repo.add("packages/package1/src/index.js").unwrap();
repo.commit("Update package1").unwrap();
let commits = repo.get_commits_since(None, &None).unwrap();
let last_commit = &commits[0].hash;
let detector =
PackageDetector::new(temp_dir.path().to_path_buf(), &repo, FileSystemManager::new());
let affected =
detector.detect_affected_packages(std::slice::from_ref(last_commit)).await.unwrap();
assert!(!affected.is_empty(), "Should detect at least one affected package");
assert!(
affected.contains(&"@test/package1".to_string()),
"Should detect package1 was affected"
);
}
#[tokio::test]
async fn test_package_detector_detect_affected_packages_multiple() {
let (temp_dir, repo) = setup_git_repo();
setup_monorepo(temp_dir.path());
repo.add_all().unwrap();
repo.commit("Setup monorepo").unwrap();
fs::write(temp_dir.path().join("packages/package1/src/index.js"), "console.log('hello');")
.unwrap();
repo.add("packages/package1/src/index.js").unwrap();
repo.commit("Update package1").unwrap();
let commit1 = repo.get_current_sha().unwrap();
fs::write(temp_dir.path().join("packages/package2/src/index.js"), "console.log('world');")
.unwrap();
repo.add("packages/package2/src/index.js").unwrap();
repo.commit("Update package2").unwrap();
let commit2 = repo.get_current_sha().unwrap();
let detector =
PackageDetector::new(temp_dir.path().to_path_buf(), &repo, FileSystemManager::new());
let affected = detector.detect_affected_packages(&[commit1, commit2]).await.unwrap();
assert_eq!(affected.len(), 2, "Should detect both affected packages");
assert!(affected.contains(&"@test/package1".to_string()));
assert!(affected.contains(&"@test/package2".to_string()));
}
#[tokio::test]
async fn test_package_detector_detect_affected_packages_single() {
let (temp_dir, repo) = setup_git_repo();
setup_single_package(temp_dir.path());
repo.add_all().unwrap();
repo.commit("Setup single package").unwrap();
fs::create_dir_all(temp_dir.path().join("src")).unwrap();
fs::write(temp_dir.path().join("src/index.js"), "console.log('test');").unwrap();
repo.add("src/index.js").unwrap();
repo.commit("Update code").unwrap();
let commits = repo.get_commits_since(None, &None).unwrap();
let last_commit = &commits[0].hash;
let detector =
PackageDetector::new(temp_dir.path().to_path_buf(), &repo, FileSystemManager::new());
let affected =
detector.detect_affected_packages(std::slice::from_ref(last_commit)).await.unwrap();
assert!(!affected.is_empty(), "Should detect the single package when files change");
}
#[tokio::test]
async fn test_package_detector_empty_commit_list() {
let (temp_dir, repo) = setup_git_repo();
setup_monorepo(temp_dir.path());
let detector =
PackageDetector::new(temp_dir.path().to_path_buf(), &repo, FileSystemManager::new());
let affected = detector.detect_affected_packages(&[]).await.unwrap();
assert_eq!(affected.len(), 0);
}
#[tokio::test]
async fn test_package_detector_get_commits_since() {
let (temp_dir, repo) = setup_git_repo();
for i in 1..=3 {
fs::write(temp_dir.path().join(format!("file{}.txt", i)), format!("content {}", i))
.unwrap();
repo.add(&format!("file{}.txt", i)).unwrap();
repo.commit(&format!("Commit {}", i)).unwrap();
}
let detector =
PackageDetector::new(temp_dir.path().to_path_buf(), &repo, FileSystemManager::new());
let commits = detector.get_commits_since(None).unwrap();
assert!(commits.len() >= 3, "Should have at least 3 commits plus initial");
if commits.len() > 1 {
let first_commit = commits.last().unwrap().hash.clone();
let recent_commits = detector.get_commits_since(Some(first_commit)).unwrap();
assert!(!recent_commits.is_empty(), "Should have commits since first commit");
}
}
#[allow(clippy::len_zero)]
#[tokio::test]
async fn test_package_detector_get_commits_between() {
let (temp_dir, repo) = setup_git_repo();
fs::write(temp_dir.path().join("file1.txt"), "content 1").unwrap();
repo.add("file1.txt").unwrap();
repo.commit("Commit 1").unwrap();
repo.create_tag("v1.0.0", Some("Version 1.0.0".to_string())).unwrap();
fs::write(temp_dir.path().join("file2.txt"), "content 2").unwrap();
repo.add("file2.txt").unwrap();
repo.commit("Commit 2").unwrap();
fs::write(temp_dir.path().join("file3.txt"), "content 3").unwrap();
repo.add("file3.txt").unwrap();
repo.commit("Commit 3").unwrap();
repo.create_tag("v2.0.0", Some("Version 2.0.0".to_string())).unwrap();
let detector =
PackageDetector::new(temp_dir.path().to_path_buf(), &repo, FileSystemManager::new());
let commits = detector.get_commits_between("v1.0.0", "v2.0.0").unwrap();
assert!(!commits.is_empty(), "Should have commits between tags");
assert!(commits.len() >= 1, "Should have at least one commit between v1.0.0 and v2.0.0");
}
#[tokio::test]
async fn test_package_detector_workspace_root() {
let (temp_dir, repo) = setup_git_repo();
let detector =
PackageDetector::new(temp_dir.path().to_path_buf(), &repo, FileSystemManager::new());
assert_eq!(detector.workspace_root(), temp_dir.path());
}
#[tokio::test]
async fn test_package_detector_repo_reference() {
let (temp_dir, repo) = setup_git_repo();
let detector =
PackageDetector::new(temp_dir.path().to_path_buf(), &repo, FileSystemManager::new());
let repo_ref = detector.repo();
let repo_path = repo_ref.get_repo_path();
assert!(!repo_path.as_os_str().is_empty());
}
#[tokio::test]
async fn test_manager_add_commits_from_git_monorepo() {
let (temp_dir, repo) = setup_git_repo();
setup_monorepo(temp_dir.path());
repo.add_all().unwrap();
repo.commit("Setup monorepo").unwrap();
fs::write(
temp_dir.path().join("packages/package1/src/index.js"),
"console.log('updated');",
)
.unwrap();
repo.add("packages/package1/src/index.js").unwrap();
repo.commit("Update package1").unwrap();
let commit1 = repo.get_current_sha().unwrap();
fs::write(
temp_dir.path().join("packages/package2/src/index.js"),
"console.log('also updated');",
)
.unwrap();
repo.add("packages/package2/src/index.js").unwrap();
repo.commit("Update package2").unwrap();
let commit2 = repo.get_current_sha().unwrap();
let storage = FileBasedChangesetStorage::new(
temp_dir.path().to_path_buf(),
".changesets".into(),
".changesets/history".into(),
FileSystemManager::new(),
);
let config = crate::config::PackageToolsConfig {
changeset: ChangesetConfig {
path: ".changesets".into(),
history_path: ".changesets/history".into(),
available_environments: vec!["production".to_string()],
default_environments: vec!["production".to_string()],
},
..Default::default()
};
let manager = ChangesetManager::with_storage(
storage,
temp_dir.path().to_path_buf(),
Some(repo),
config,
);
manager
.create("feature/test", VersionBump::Minor, vec!["production".to_string()])
.await
.unwrap();
let mut changeset = manager.load("feature/test").await.unwrap();
changeset.add_package("@test/package1");
let all_commits =
manager.git_repo().as_ref().unwrap().get_commits_since(None, &None).unwrap();
let setup_commit = all_commits
.iter()
.find(|c| c.message.contains("Setup monorepo"))
.map(|c| &c.hash)
.expect("Setup monorepo commit not found");
changeset.add_commit(setup_commit);
manager.update(&changeset).await.unwrap();
let summary = manager.add_commits_from_git("feature/test").await.unwrap();
assert_eq!(summary.commits_added, 2, "Should have added 2 commits");
let total_packages = summary.new_packages.len() + summary.existing_packages.len();
assert_eq!(total_packages, 2, "Should have affected 2 packages");
let all_packages: Vec<String> =
summary.new_packages.iter().chain(summary.existing_packages.iter()).cloned().collect();
assert!(all_packages.contains(&"@test/package1".to_string()));
assert!(all_packages.contains(&"@test/package2".to_string()));
let changeset = manager.load("feature/test").await.unwrap();
assert_eq!(changeset.packages.len(), 2, "Should have 2 packages in changeset");
assert!(changeset.packages.contains(&"@test/package1".to_string()));
assert!(changeset.packages.contains(&"@test/package2".to_string()));
assert!(changeset.changes.contains(&commit1));
assert!(changeset.changes.contains(&commit2));
}
#[tokio::test]
async fn test_manager_add_commits_from_git_single_package() {
let (temp_dir, repo) = setup_git_repo();
setup_single_package(temp_dir.path());
repo.add_all().unwrap();
repo.commit("Setup single package").unwrap();
fs::create_dir_all(temp_dir.path().join("src")).unwrap();
fs::write(temp_dir.path().join("src/index.js"), "console.log('test');").unwrap();
repo.add("src/index.js").unwrap();
repo.commit("Update code").unwrap();
let commit_hash = repo.get_current_sha().unwrap();
let storage = FileBasedChangesetStorage::new(
temp_dir.path().to_path_buf(),
".changesets".into(),
".changesets/history".into(),
FileSystemManager::new(),
);
let config = crate::config::PackageToolsConfig {
changeset: ChangesetConfig {
path: ".changesets".into(),
history_path: ".changesets/history".into(),
available_environments: vec!["production".to_string()],
default_environments: vec!["production".to_string()],
},
..Default::default()
};
let manager = ChangesetManager::with_storage(
storage,
temp_dir.path().to_path_buf(),
Some(repo),
config,
);
manager
.create("feature/test", VersionBump::Minor, vec!["production".to_string()])
.await
.unwrap();
let mut changeset = manager.load("feature/test").await.unwrap();
changeset.add_package("single-package");
let all_commits =
manager.git_repo().as_ref().unwrap().get_commits_since(None, &None).unwrap();
let setup_commit = all_commits
.iter()
.find(|c| c.message.contains("Setup single package"))
.map(|c| &c.hash)
.expect("Setup single package commit not found");
changeset.add_commit(setup_commit);
manager.update(&changeset).await.unwrap();
let summary = manager.add_commits_from_git("feature/test").await.unwrap();
assert_eq!(summary.commits_added, 1, "Should have added 1 commit");
let total_packages = summary.new_packages.len() + summary.existing_packages.len();
assert_eq!(total_packages, 1, "Should have affected 1 package");
let all_packages: Vec<String> =
summary.new_packages.iter().chain(summary.existing_packages.iter()).cloned().collect();
assert!(all_packages.contains(&"single-package".to_string()));
let changeset = manager.load("feature/test").await.unwrap();
assert_eq!(changeset.packages.len(), 1, "Should have 1 package in changeset");
assert!(changeset.packages.contains(&"single-package".to_string()));
assert!(changeset.changes.contains(&commit_hash));
}
#[tokio::test]
async fn test_manager_add_commits_from_git_no_git_repo() {
let temp_dir = TempDir::new().unwrap();
let storage = FileBasedChangesetStorage::new(
temp_dir.path().to_path_buf(),
".changesets".into(),
".changesets/history".into(),
FileSystemManager::new(),
);
let config = crate::config::PackageToolsConfig {
changeset: ChangesetConfig {
path: ".changesets".into(),
history_path: ".changesets/history".into(),
available_environments: vec!["production".to_string()],
default_environments: vec!["production".to_string()],
},
..Default::default()
};
let manager =
ChangesetManager::with_storage(storage, temp_dir.path().to_path_buf(), None, config);
manager
.create("feature/test", VersionBump::Minor, vec!["production".to_string()])
.await
.unwrap();
let result = manager.add_commits_from_git("feature/test").await;
assert!(result.is_err(), "Should fail when no Git repo is available");
if let Err(ChangesetError::GitIntegration { operation, reason }) = result {
assert_eq!(operation, "add commits from git");
assert!(reason.contains("Git repository not available"));
} else {
panic!("Expected GitIntegration error");
}
}
}
mod history_tests {
use super::*;
use crate::changeset::ChangesetHistory;
use chrono::{Duration, Utc};
#[tokio::test]
async fn test_history_list_all_empty() {
let storage = MockStorage::new();
let history = ChangesetHistory::new(Box::new(storage));
let all = history.list_all().await.unwrap();
assert_eq!(all.len(), 0);
}
#[tokio::test]
async fn test_history_list_all_multiple() {
let storage = MockStorage::new();
for i in 1..=5 {
let mut changeset = Changeset::new(
format!("feature/history-{}", i),
VersionBump::Minor,
vec!["production".to_string()],
);
changeset.add_package(format!("package{}", i));
storage.save(&changeset).await.unwrap();
let release_info = ReleaseInfo::new(
format!("user{}@example.com", i),
format!("commit{}", i),
versions_map(vec![(format!("package{}", i), format!("1.{}.0", i))]),
);
storage.archive(&changeset, release_info).await.unwrap();
}
let history = ChangesetHistory::new(Box::new(storage));
let all = history.list_all().await.unwrap();
assert_eq!(all.len(), 5);
let branches: Vec<String> = all.iter().map(|a| a.changeset.branch.clone()).collect();
for i in 1..=5 {
let expected = format!("feature/history-{}", i);
assert!(branches.contains(&expected), "Expected to find branch {}", expected);
}
}
#[tokio::test]
async fn test_history_list_all_sorted_by_date() {
let storage = MockStorage::new();
let now = Utc::now();
for i in 1..=3 {
let mut changeset = Changeset::new(
format!("feature/sorted-{}", i),
VersionBump::Minor,
vec!["production".to_string()],
);
changeset.add_package("package");
storage.save(&changeset).await.unwrap();
let applied_at = now - Duration::days(i);
let mut release_info = ReleaseInfo::new(
"user@example.com".to_string(),
format!("commit{}", i),
versions_map(vec![("package".to_string(), "1.0.0".to_string())]),
);
release_info.applied_at = applied_at;
storage.archive(&changeset, release_info).await.unwrap();
}
let history = ChangesetHistory::new(Box::new(storage));
let all = history.list_all().await.unwrap();
assert_eq!(all[0].changeset.branch, "feature/sorted-1");
assert_eq!(all[1].changeset.branch, "feature/sorted-2");
assert_eq!(all[2].changeset.branch, "feature/sorted-3");
}
#[tokio::test]
async fn test_history_get_existing() {
let storage = MockStorage::new();
let mut changeset =
Changeset::new("feature/get-test", VersionBump::Major, vec!["production".to_string()]);
changeset.add_package("test-package");
storage.save(&changeset).await.unwrap();
let release_info = ReleaseInfo::new(
"ci-bot@example.com".to_string(),
"abc123".to_string(),
versions_map(vec![("test-package".to_string(), "2.0.0".to_string())]),
);
storage.archive(&changeset, release_info).await.unwrap();
let history = ChangesetHistory::new(Box::new(storage));
let archived = history.get("feature/get-test").await.unwrap();
assert_eq!(archived.changeset.branch, "feature/get-test");
assert_eq!(archived.changeset.bump, VersionBump::Major);
assert_eq!(archived.release_info.applied_by, "ci-bot@example.com");
assert_eq!(archived.release_info.git_commit, "abc123");
}
#[tokio::test]
async fn test_history_get_nonexistent() {
let storage = MockStorage::new();
let history = ChangesetHistory::new(Box::new(storage));
let result = history.get("nonexistent").await;
assert!(result.is_err());
match result {
Err(ChangesetError::NotFound { branch }) => {
assert_eq!(branch, "nonexistent");
}
_ => panic!("Expected NotFound error"),
}
}
#[tokio::test]
async fn test_query_by_date_range() {
let storage = MockStorage::new();
let now = Utc::now();
let dates = [
now - Duration::days(10), now - Duration::days(5), now - Duration::days(3), now - Duration::days(1), ];
for (i, date) in dates.iter().enumerate() {
let mut changeset = Changeset::new(
format!("feature/date-{}", i),
VersionBump::Patch,
vec!["production".to_string()],
);
changeset.add_package("package");
storage.save(&changeset).await.unwrap();
let mut release_info = ReleaseInfo::new(
"user@example.com".to_string(),
format!("commit{}", i),
versions_map(vec![("package".to_string(), "1.0.0".to_string())]),
);
release_info.applied_at = *date;
storage.archive(&changeset, release_info).await.unwrap();
}
let history = ChangesetHistory::new(Box::new(storage));
let start = now - Duration::days(6);
let end = now - Duration::days(2);
let results = history.query_by_date(start, end).await.unwrap();
assert_eq!(results.len(), 2);
assert!(results.iter().any(|a| a.changeset.branch == "feature/date-1"));
assert!(results.iter().any(|a| a.changeset.branch == "feature/date-2"));
}
#[tokio::test]
async fn test_query_by_date_no_results() {
let storage = MockStorage::new();
let now = Utc::now();
let mut changeset =
Changeset::new("feature/old", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("package");
storage.save(&changeset).await.unwrap();
let mut release_info = ReleaseInfo::new(
"user@example.com".to_string(),
"commit".to_string(),
versions_map(vec![("package".to_string(), "1.0.0".to_string())]),
);
release_info.applied_at = now - Duration::days(100);
storage.archive(&changeset, release_info).await.unwrap();
let history = ChangesetHistory::new(Box::new(storage));
let start = now - Duration::days(7);
let end = now;
let results = history.query_by_date(start, end).await.unwrap();
assert_eq!(results.len(), 0);
}
#[tokio::test]
async fn test_query_by_package_single_match() {
let storage = MockStorage::new();
for i in 1..=3 {
let mut changeset = Changeset::new(
format!("feature/pkg-{}", i),
VersionBump::Minor,
vec!["production".to_string()],
);
if i == 2 {
changeset.add_package("target-package");
} else {
changeset.add_package(format!("other-package-{}", i));
}
storage.save(&changeset).await.unwrap();
let release_info = ReleaseInfo::new(
"user@example.com".to_string(),
format!("commit{}", i),
versions_map(vec![("package".to_string(), "1.0.0".to_string())]),
);
storage.archive(&changeset, release_info).await.unwrap();
}
let history = ChangesetHistory::new(Box::new(storage));
let results = history.query_by_package("target-package").await.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].changeset.branch, "feature/pkg-2");
}
#[tokio::test]
async fn test_query_by_package_multiple_matches() {
let storage = MockStorage::new();
for i in 1..=4 {
let mut changeset = Changeset::new(
format!("feature/common-pkg-{}", i),
VersionBump::Patch,
vec!["production".to_string()],
);
changeset.add_package("@myorg/core");
changeset.add_package(format!("package-{}", i));
storage.save(&changeset).await.unwrap();
let release_info = ReleaseInfo::new(
"user@example.com".to_string(),
format!("commit{}", i),
versions_map(vec![
("@myorg/core".to_string(), format!("1.{}.0", i)),
(format!("package-{}", i), "1.0.0".to_string()),
]),
);
storage.archive(&changeset, release_info).await.unwrap();
}
let history = ChangesetHistory::new(Box::new(storage));
let results = history.query_by_package("@myorg/core").await.unwrap();
assert_eq!(results.len(), 4);
for result in results {
assert!(result.changeset.has_package("@myorg/core"));
}
}
#[tokio::test]
async fn test_query_by_package_no_matches() {
let storage = MockStorage::new();
let mut changeset =
Changeset::new("feature/other", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("package1");
storage.save(&changeset).await.unwrap();
let release_info = ReleaseInfo::new(
"user@example.com".to_string(),
"commit".to_string(),
versions_map(vec![("package1".to_string(), "1.0.0".to_string())]),
);
storage.archive(&changeset, release_info).await.unwrap();
let history = ChangesetHistory::new(Box::new(storage));
let results = history.query_by_package("nonexistent-package").await.unwrap();
assert_eq!(results.len(), 0);
}
#[tokio::test]
async fn test_query_by_environment_single() {
let storage = MockStorage::new();
let envs = [vec!["production"], vec!["staging"], vec!["production"], vec!["development"]];
for (i, env) in envs.iter().enumerate() {
let changeset = Changeset::new(
format!("feature/env-{}", i),
VersionBump::Minor,
env.iter().map(|s| s.to_string()).collect(),
);
storage.save(&changeset).await.unwrap();
let release_info = ReleaseInfo::new(
"user@example.com".to_string(),
format!("commit{}", i),
versions_map(vec![("package".to_string(), "1.0.0".to_string())]),
);
storage.archive(&changeset, release_info).await.unwrap();
}
let history = ChangesetHistory::new(Box::new(storage));
let results = history.query_by_environment("production").await.unwrap();
assert_eq!(results.len(), 2);
assert!(results.iter().any(|a| a.changeset.branch == "feature/env-0"));
assert!(results.iter().any(|a| a.changeset.branch == "feature/env-2"));
}
#[tokio::test]
async fn test_query_by_environment_multiple() {
let storage = MockStorage::new();
let mut changeset = Changeset::new(
"feature/multi-env",
VersionBump::Major,
vec!["staging".to_string(), "production".to_string()],
);
changeset.add_package("package");
storage.save(&changeset).await.unwrap();
let release_info = ReleaseInfo::new(
"user@example.com".to_string(),
"commit".to_string(),
versions_map(vec![("package".to_string(), "2.0.0".to_string())]),
);
storage.archive(&changeset, release_info).await.unwrap();
let history = ChangesetHistory::new(Box::new(storage));
let staging_results = history.query_by_environment("staging").await.unwrap();
assert_eq!(staging_results.len(), 1);
let prod_results = history.query_by_environment("production").await.unwrap();
assert_eq!(prod_results.len(), 1);
}
#[tokio::test]
async fn test_query_by_environment_no_matches() {
let storage = MockStorage::new();
let changeset =
Changeset::new("feature/prod-only", VersionBump::Minor, vec!["production".to_string()]);
storage.save(&changeset).await.unwrap();
let release_info = ReleaseInfo::new(
"user@example.com".to_string(),
"commit".to_string(),
versions_map(vec![("package".to_string(), "1.0.0".to_string())]),
);
storage.archive(&changeset, release_info).await.unwrap();
let history = ChangesetHistory::new(Box::new(storage));
let results = history.query_by_environment("development").await.unwrap();
assert_eq!(results.len(), 0);
}
#[tokio::test]
async fn test_query_by_bump_type() {
let storage = MockStorage::new();
let bumps = [
VersionBump::Major,
VersionBump::Minor,
VersionBump::Major,
VersionBump::Patch,
VersionBump::Minor,
];
for (i, bump) in bumps.iter().enumerate() {
let changeset = Changeset::new(
format!("feature/bump-{}", i),
*bump,
vec!["production".to_string()],
);
storage.save(&changeset).await.unwrap();
let release_info = ReleaseInfo::new(
"user@example.com".to_string(),
format!("commit{}", i),
versions_map(vec![("package".to_string(), "1.0.0".to_string())]),
);
storage.archive(&changeset, release_info).await.unwrap();
}
let history = ChangesetHistory::new(Box::new(storage));
let major_results = history.query_by_bump(VersionBump::Major).await.unwrap();
assert_eq!(major_results.len(), 2);
assert!(major_results.iter().any(|a| a.changeset.branch == "feature/bump-0"));
assert!(major_results.iter().any(|a| a.changeset.branch == "feature/bump-2"));
let minor_results = history.query_by_bump(VersionBump::Minor).await.unwrap();
assert_eq!(minor_results.len(), 2);
assert!(minor_results.iter().any(|a| a.changeset.branch == "feature/bump-1"));
assert!(minor_results.iter().any(|a| a.changeset.branch == "feature/bump-4"));
let patch_results = history.query_by_bump(VersionBump::Patch).await.unwrap();
assert_eq!(patch_results.len(), 1);
assert_eq!(patch_results[0].changeset.branch, "feature/bump-3");
}
#[tokio::test]
async fn test_query_by_bump_none() {
let storage = MockStorage::new();
let changeset =
Changeset::new("feature/no-bump", VersionBump::None, vec!["production".to_string()]);
storage.save(&changeset).await.unwrap();
let release_info = ReleaseInfo::new(
"user@example.com".to_string(),
"commit".to_string(),
versions_map(vec![("package".to_string(), "1.0.0".to_string())]),
);
storage.archive(&changeset, release_info).await.unwrap();
let history = ChangesetHistory::new(Box::new(storage));
let results = history.query_by_bump(VersionBump::None).await.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].changeset.branch, "feature/no-bump");
let major_results = history.query_by_bump(VersionBump::Major).await.unwrap();
assert_eq!(major_results.len(), 0);
}
#[tokio::test]
async fn test_combined_queries() {
let storage = MockStorage::new();
let now = Utc::now();
let mut changeset1 = Changeset::new(
"feature/combined-1",
VersionBump::Major,
vec!["production".to_string()],
);
changeset1.add_package("@myorg/core");
let mut changeset2 =
Changeset::new("feature/combined-2", VersionBump::Minor, vec!["staging".to_string()]);
changeset2.add_package("@myorg/utils");
let mut changeset3 = Changeset::new(
"feature/combined-3",
VersionBump::Major,
vec!["production".to_string()],
);
changeset3.add_package("@myorg/core");
storage.save(&changeset1).await.unwrap();
storage.save(&changeset2).await.unwrap();
storage.save(&changeset3).await.unwrap();
let mut release_info1 = ReleaseInfo::new(
"user@example.com".to_string(),
"commit1".to_string(),
versions_map(vec![("@myorg/core".to_string(), "2.0.0".to_string())]),
);
release_info1.applied_at = now - Duration::days(2);
let mut release_info2 = ReleaseInfo::new(
"user@example.com".to_string(),
"commit2".to_string(),
versions_map(vec![("@myorg/utils".to_string(), "1.1.0".to_string())]),
);
release_info2.applied_at = now - Duration::days(5);
let mut release_info3 = ReleaseInfo::new(
"user@example.com".to_string(),
"commit3".to_string(),
versions_map(vec![("@myorg/core".to_string(), "3.0.0".to_string())]),
);
release_info3.applied_at = now - Duration::days(1);
storage.archive(&changeset1, release_info1).await.unwrap();
storage.archive(&changeset2, release_info2).await.unwrap();
storage.archive(&changeset3, release_info3).await.unwrap();
let history = ChangesetHistory::new(Box::new(storage));
let core_releases = history.query_by_package("@myorg/core").await.unwrap();
assert_eq!(core_releases.len(), 2);
let prod_releases = history.query_by_environment("production").await.unwrap();
assert_eq!(prod_releases.len(), 2);
let major_releases = history.query_by_bump(VersionBump::Major).await.unwrap();
assert_eq!(major_releases.len(), 2);
let start = now - Duration::days(3);
let end = now;
let recent_releases = history.query_by_date(start, end).await.unwrap();
assert_eq!(recent_releases.len(), 2);
}
#[tokio::test]
async fn test_history_with_release_version_info() {
let storage = MockStorage::new();
let mut changeset =
Changeset::new("feature/versions", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("package1");
changeset.add_package("package2");
storage.save(&changeset).await.unwrap();
let release_info = ReleaseInfo::new(
"ci-bot@example.com".to_string(),
"abc123def456".to_string(),
versions_map(vec![
("package1".to_string(), "1.5.0".to_string()),
("package2".to_string(), "2.3.0".to_string()),
]),
);
storage.archive(&changeset, release_info).await.unwrap();
let history = ChangesetHistory::new(Box::new(storage));
let archived = history.get("feature/versions").await.unwrap();
assert_eq!(archived.release_info.get_version("package1"), Some("1.5.0"));
assert_eq!(archived.release_info.get_version("package2"), Some("2.3.0"));
assert_eq!(archived.release_info.get_version("nonexistent"), None);
assert_eq!(archived.release_info.package_count(), 2);
}
}