#![allow(clippy::expect_used)]
#![allow(clippy::panic)]
use std::path::PathBuf;
use sublime_pkg_tools::config::PackageToolsConfig;
use sublime_pkg_tools::types::{Changeset, UpdateReason, VersionBump, VersioningStrategy};
use sublime_pkg_tools::version::VersionResolver;
mod common;
async fn create_complex_monorepo() -> (tempfile::TempDir, PathBuf) {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
let root_package = r#"{
"name": "monorepo-root",
"version": "1.0.0",
"private": true,
"workspaces": ["packages/*", "tools/*"]
}"#;
tokio::fs::write(root.join("package.json"), root_package)
.await
.expect("Failed to write root package.json");
let package_lock = r#"{
"name": "monorepo-root",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}"#;
tokio::fs::write(root.join("package-lock.json"), package_lock)
.await
.expect("Failed to write package-lock.json");
tokio::fs::create_dir_all(root.join("packages")).await.expect("Failed to create packages dir");
tokio::fs::create_dir_all(root.join("tools")).await.expect("Failed to create tools dir");
let pkg_a_dir = root.join("packages").join("pkg-a");
tokio::fs::create_dir_all(&pkg_a_dir).await.expect("Failed to create pkg-a dir");
let pkg_a = r#"{
"name": "@test/pkg-a",
"version": "1.0.0",
"description": "Package A"
}"#;
tokio::fs::write(pkg_a_dir.join("package.json"), pkg_a)
.await
.expect("Failed to write pkg-a package.json");
let pkg_b_dir = root.join("packages").join("pkg-b");
tokio::fs::create_dir_all(&pkg_b_dir).await.expect("Failed to create pkg-b dir");
let pkg_b = r#"{
"name": "@test/pkg-b",
"version": "1.0.0",
"dependencies": {
"@test/pkg-a": "^1.0.0",
"lodash": "^4.17.21"
}
}"#;
tokio::fs::write(pkg_b_dir.join("package.json"), pkg_b)
.await
.expect("Failed to write pkg-b package.json");
let pkg_c_dir = root.join("packages").join("pkg-c");
tokio::fs::create_dir_all(&pkg_c_dir).await.expect("Failed to create pkg-c dir");
let pkg_c = r#"{
"name": "@test/pkg-c",
"version": "2.0.0",
"dependencies": {
"@test/pkg-b": "^1.0.0"
},
"devDependencies": {
"@test/pkg-a": "^1.0.0"
}
}"#;
tokio::fs::write(pkg_c_dir.join("package.json"), pkg_c)
.await
.expect("Failed to write pkg-c package.json");
let pkg_d_dir = root.join("packages").join("pkg-d");
tokio::fs::create_dir_all(&pkg_d_dir).await.expect("Failed to create pkg-d dir");
let pkg_d = r#"{
"name": "@test/pkg-d",
"version": "0.5.0",
"dependencies": {
"react": "^18.0.0"
}
}"#;
tokio::fs::write(pkg_d_dir.join("package.json"), pkg_d)
.await
.expect("Failed to write pkg-d package.json");
let tool_dir = root.join("tools").join("build-tool");
tokio::fs::create_dir_all(&tool_dir).await.expect("Failed to create build-tool dir");
let tool = r#"{
"name": "@test/build-tool",
"version": "1.0.0",
"dependencies": {
"@test/pkg-a": "workspace:*",
"@test/pkg-b": "workspace:^"
}
}"#;
tokio::fs::write(tool_dir.join("package.json"), tool)
.await
.expect("Failed to write build-tool package.json");
(temp_dir, root)
}
async fn create_circular_monorepo() -> (tempfile::TempDir, PathBuf) {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
let root_package = r#"{
"name": "circular-root",
"version": "1.0.0",
"private": true,
"workspaces": ["packages/*"]
}"#;
tokio::fs::write(root.join("package.json"), root_package)
.await
.expect("Failed to write root package.json");
let package_lock = r#"{
"name": "circular-root",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}"#;
tokio::fs::write(root.join("package-lock.json"), package_lock)
.await
.expect("Failed to write package-lock.json");
tokio::fs::create_dir_all(root.join("packages")).await.expect("Failed to create packages dir");
let pkg_a_dir = root.join("packages").join("pkg-a");
tokio::fs::create_dir_all(&pkg_a_dir).await.expect("Failed to create pkg-a dir");
let pkg_a = r#"{
"name": "@circular/pkg-a",
"version": "1.0.0",
"dependencies": {
"@circular/pkg-b": "^1.0.0"
}
}"#;
tokio::fs::write(pkg_a_dir.join("package.json"), pkg_a)
.await
.expect("Failed to write pkg-a package.json");
let pkg_b_dir = root.join("packages").join("pkg-b");
tokio::fs::create_dir_all(&pkg_b_dir).await.expect("Failed to create pkg-b dir");
let pkg_b = r#"{
"name": "@circular/pkg-b",
"version": "1.0.0",
"dependencies": {
"@circular/pkg-a": "^1.0.0"
}
}"#;
tokio::fs::write(pkg_b_dir.join("package.json"), pkg_b)
.await
.expect("Failed to write pkg-b package.json");
(temp_dir, root)
}
async fn create_deep_chain_monorepo(depth: usize) -> (tempfile::TempDir, PathBuf) {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
let root_package = r#"{
"name": "deep-chain-root",
"version": "1.0.0",
"private": true,
"workspaces": ["packages/*"]
}"#;
tokio::fs::write(root.join("package.json"), root_package)
.await
.expect("Failed to write root package.json");
let package_lock = r#"{
"name": "deep-chain-root",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}"#;
tokio::fs::write(root.join("package-lock.json"), package_lock)
.await
.expect("Failed to write package-lock.json");
tokio::fs::create_dir_all(root.join("packages")).await.expect("Failed to create packages dir");
for i in 0..depth {
let pkg_dir = root.join("packages").join(format!("pkg-{}", i));
tokio::fs::create_dir_all(&pkg_dir).await.expect("Failed to create package dir");
let pkg_json = if i == 0 {
format!(
r#"{{
"name": "@chain/pkg-{}",
"version": "1.0.0"
}}"#,
i
)
} else {
format!(
r#"{{
"name": "@chain/pkg-{}",
"version": "1.0.0",
"dependencies": {{
"@chain/pkg-{}": "^1.0.0"
}}
}}"#,
i,
i - 1
)
};
tokio::fs::write(pkg_dir.join("package.json"), pkg_json)
.await
.expect("Failed to write package.json");
}
(temp_dir, root)
}
async fn create_single_package() -> (tempfile::TempDir, PathBuf) {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
let package = r#"{
"name": "single-package",
"version": "1.5.0",
"description": "A single package project",
"dependencies": {
"lodash": "^4.17.21",
"axios": "^1.0.0"
},
"devDependencies": {
"jest": "^29.0.0",
"typescript": "^5.0.0"
}
}"#;
tokio::fs::write(root.join("package.json"), package)
.await
.expect("Failed to write package.json");
(temp_dir, root)
}
#[tokio::test]
async fn test_integration_complete_resolution_workflow_independent() {
let (_temp, root) = create_complex_monorepo().await;
let config = PackageToolsConfig::default();
let resolver =
VersionResolver::new(root.clone(), config).await.expect("Failed to create resolver");
assert!(resolver.is_monorepo(), "Should detect monorepo");
let mut changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("@test/pkg-a");
let resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
assert_eq!(resolution.updates.len(), 3, "Should update pkg-a, pkg-b, and pkg-c");
let pkg_a_update = resolution
.updates
.iter()
.find(|u| u.name == "@test/pkg-a")
.expect("Should find pkg-a update");
assert_eq!(pkg_a_update.current_version.to_string(), "1.0.0");
assert_eq!(pkg_a_update.next_version.to_string(), "1.1.0");
assert!(!pkg_a_update.is_propagated(), "pkg-a is direct change");
let pkg_b_update = resolution
.updates
.iter()
.find(|u| u.name == "@test/pkg-b")
.expect("Should find pkg-b update");
assert_eq!(pkg_b_update.current_version.to_string(), "1.0.0");
assert_eq!(pkg_b_update.next_version.to_string(), "1.0.1");
assert!(pkg_b_update.is_propagated(), "pkg-b should be propagated");
let dry_result =
resolver.apply_versions(&changeset, true).await.expect("Failed to apply dry run");
assert!(dry_result.dry_run, "Should be dry run");
assert_eq!(dry_result.modified_files.len(), 0, "Should not modify files in dry run");
assert_eq!(dry_result.summary.packages_updated, 3);
assert_eq!(dry_result.summary.direct_updates, 1);
assert_eq!(dry_result.summary.propagated_updates, 2);
let apply_result =
resolver.apply_versions(&changeset, false).await.expect("Failed to apply versions");
assert!(!apply_result.dry_run, "Should not be dry run");
assert_eq!(apply_result.modified_files.len(), 3, "Should modify 3 package.json files");
assert_eq!(apply_result.summary.packages_updated, 3);
let pkg_a_content = tokio::fs::read_to_string(root.join("packages/pkg-a/package.json"))
.await
.expect("Failed to read pkg-a");
assert!(pkg_a_content.contains(r#""version": "1.1.0""#), "pkg-a version should be updated");
let pkg_b_content = tokio::fs::read_to_string(root.join("packages/pkg-b/package.json"))
.await
.expect("Failed to read pkg-b");
assert!(pkg_b_content.contains(r#""version": "1.0.1""#), "pkg-b version should be updated");
assert!(
pkg_b_content.contains(r#""@test/pkg-a": "^1.1.0""#),
"pkg-b dependency should be updated"
);
let pkg_c_content = tokio::fs::read_to_string(root.join("packages/pkg-c/package.json"))
.await
.expect("Failed to read pkg-c");
assert!(pkg_c_content.contains(r#""version": "2.0.1""#), "pkg-c version should be updated");
assert!(
pkg_c_content.contains(r#""@test/pkg-b": "^1.0.1""#),
"pkg-c dependency should be updated"
);
}
#[tokio::test]
async fn test_integration_unified_strategy_workflow() {
let (_temp, root) = create_complex_monorepo().await;
let mut config = PackageToolsConfig::default();
config.version.strategy = VersioningStrategy::Unified;
let resolver =
VersionResolver::new(root.clone(), config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("release/v2", VersionBump::Major, vec!["production".to_string()]);
changeset.add_package("@test/pkg-a");
changeset.add_package("@test/pkg-b");
let resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
let major_versions: Vec<_> =
resolution.updates.iter().filter(|u| u.next_version.major() >= 2).collect();
assert!(!major_versions.is_empty(), "Should have major version updates");
let result =
resolver.apply_versions(&changeset, false).await.expect("Failed to apply versions");
assert!(!result.dry_run);
assert!(result.summary.packages_updated > 0);
let pkg_a_content = tokio::fs::read_to_string(root.join("packages/pkg-a/package.json"))
.await
.expect("Failed to read pkg-a");
assert!(
pkg_a_content.contains(r#""version": "3.0.0""#),
"pkg-a should have unified version 3.0.0"
);
let pkg_c_content = tokio::fs::read_to_string(root.join("packages/pkg-c/package.json"))
.await
.expect("Failed to read pkg-c");
assert!(
pkg_c_content.contains(r#""version": "3.0.0""#),
"pkg-c should have unified version 3.0.0"
);
let first_version = resolution.updates[0].next_version.clone();
for update in &resolution.updates {
assert_eq!(
update.next_version, first_version,
"All packages should have the same version in unified strategy"
);
}
}
#[tokio::test]
async fn test_integration_circular_dependency_detection() {
let (_temp, root) = create_circular_monorepo().await;
let config = PackageToolsConfig::default();
let resolver =
VersionResolver::new(root.clone(), config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("@circular/pkg-a");
let resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
assert!(!resolution.circular_dependencies.is_empty(), "Should detect circular dependencies");
assert_eq!(resolution.circular_dependencies.len(), 1);
let cycle = &resolution.circular_dependencies[0];
assert!(cycle.cycle.contains(&"@circular/pkg-a".to_string()));
assert!(cycle.cycle.contains(&"@circular/pkg-b".to_string()));
}
#[tokio::test]
async fn test_integration_max_depth_propagation() {
let depth = 10;
let (_temp, root) = create_deep_chain_monorepo(depth).await;
let mut config = PackageToolsConfig::default();
config.dependency.max_depth = 3;
let resolver =
VersionResolver::new(root.clone(), config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("@chain/pkg-0");
let resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
assert!(
resolution.updates.len() <= 4,
"Should respect max depth of 3, got {} updates",
resolution.updates.len()
);
for update in &resolution.updates {
if let UpdateReason::DependencyPropagation { depth, .. } = &update.reason {
assert!(depth <= &3, "Propagation depth should not exceed 3");
}
}
}
#[tokio::test]
async fn test_integration_workspace_protocol_preservation() {
let (_temp, root) = create_complex_monorepo().await;
let config = PackageToolsConfig::default();
let resolver =
VersionResolver::new(root.clone(), config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("@test/pkg-a");
let _result =
resolver.apply_versions(&changeset, false).await.expect("Failed to apply versions");
let tool_content = tokio::fs::read_to_string(root.join("tools/build-tool/package.json"))
.await
.expect("Failed to read build-tool");
assert!(
tool_content.contains(r#""@test/pkg-a": "workspace:*""#),
"workspace:* protocol should be preserved"
);
assert!(
tool_content.contains(r#""@test/pkg-b": "workspace:^""#),
"workspace:^ protocol should be preserved"
);
}
#[tokio::test]
async fn test_integration_single_package_workflow() {
let (_temp, root) = create_single_package().await;
let config = PackageToolsConfig::default();
let resolver =
VersionResolver::new(root.clone(), config).await.expect("Failed to create resolver");
assert!(!resolver.is_monorepo(), "Should not detect as monorepo");
let mut changeset =
Changeset::new("release/v2", VersionBump::Major, vec!["production".to_string()]);
changeset.add_package("single-package");
let resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
assert_eq!(resolution.updates.len(), 1);
let update = &resolution.updates[0];
assert_eq!(update.current_version.to_string(), "1.5.0");
assert_eq!(update.next_version.to_string(), "2.0.0");
let result =
resolver.apply_versions(&changeset, false).await.expect("Failed to apply versions");
assert_eq!(result.modified_files.len(), 1);
assert_eq!(result.summary.packages_updated, 1);
assert_eq!(result.summary.direct_updates, 1);
assert_eq!(result.summary.propagated_updates, 0);
let content = tokio::fs::read_to_string(root.join("package.json"))
.await
.expect("Failed to read package.json");
assert!(content.contains(r#""version": "2.0.0""#));
}
#[tokio::test]
async fn test_integration_dry_run_then_apply() {
let (_temp, root) = create_complex_monorepo().await;
let config = PackageToolsConfig::default();
let resolver =
VersionResolver::new(root.clone(), config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("feature/test", VersionBump::Patch, vec!["staging".to_string()]);
changeset.add_package("@test/pkg-d");
let dry_result =
resolver.apply_versions(&changeset, true).await.expect("Failed to apply dry run");
assert!(dry_result.dry_run);
assert_eq!(dry_result.modified_files.len(), 0);
let dry_summary = dry_result.summary;
let original_content = tokio::fs::read_to_string(root.join("packages/pkg-d/package.json"))
.await
.expect("Failed to read pkg-d");
assert!(original_content.contains(r#""version": "0.5.0""#));
let apply_result =
resolver.apply_versions(&changeset, false).await.expect("Failed to apply versions");
assert!(!apply_result.dry_run);
assert_eq!(apply_result.modified_files.len(), 1);
assert_eq!(dry_summary.packages_updated, apply_result.summary.packages_updated);
assert_eq!(dry_summary.direct_updates, apply_result.summary.direct_updates);
let updated_content = tokio::fs::read_to_string(root.join("packages/pkg-d/package.json"))
.await
.expect("Failed to read pkg-d");
assert!(updated_content.contains(r#""version": "0.5.1""#));
}
#[tokio::test]
async fn test_integration_no_propagation_config() {
let (_temp, root) = create_complex_monorepo().await;
let mut config = PackageToolsConfig::default();
config.dependency.propagation_bump = "none".to_string();
let resolver =
VersionResolver::new(root.clone(), config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("@test/pkg-a");
let resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
assert_eq!(resolution.updates.len(), 1);
assert_eq!(resolution.updates[0].name, "@test/pkg-a");
assert!(!resolution.updates[0].is_propagated());
}
#[tokio::test]
async fn test_integration_dev_dependencies_propagation() {
let (_temp, root) = create_complex_monorepo().await;
let mut config = PackageToolsConfig::default();
config.dependency.propagate_dev_dependencies = true;
let resolver =
VersionResolver::new(root.clone(), config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("@test/pkg-a");
let resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
let pkg_c_update = resolution.updates.iter().find(|u| u.name == "@test/pkg-c");
assert!(pkg_c_update.is_some(), "pkg-c should be updated due to devDependency");
let _result =
resolver.apply_versions(&changeset, false).await.expect("Failed to apply versions");
let pkg_c_content = tokio::fs::read_to_string(root.join("packages/pkg-c/package.json"))
.await
.expect("Failed to read pkg-c");
let pkg_c_json: serde_json::Value =
serde_json::from_str(&pkg_c_content).expect("Failed to parse JSON");
let dev_deps = pkg_c_json["devDependencies"].as_object().expect("Should have devDependencies");
let pkg_a_version = dev_deps["@test/pkg-a"].as_str().expect("Should have @test/pkg-a");
assert!(pkg_a_version.contains("1.1.0"), "devDependency should be updated to 1.1.0");
}
#[tokio::test]
async fn test_integration_empty_changeset() {
let (_temp, root) = create_complex_monorepo().await;
let config = PackageToolsConfig::default();
let resolver = VersionResolver::new(root, config).await.expect("Failed to create resolver");
let changeset =
Changeset::new("feature/empty", VersionBump::Minor, vec!["production".to_string()]);
let resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
assert_eq!(resolution.updates.len(), 0);
assert_eq!(resolution.circular_dependencies.len(), 0);
}
#[tokio::test]
async fn test_integration_nonexistent_package_error() {
let (_temp, root) = create_complex_monorepo().await;
let config = PackageToolsConfig::default();
let resolver = VersionResolver::new(root, config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("@test/nonexistent");
let result = resolver.resolve_versions(&changeset).await;
assert!(result.is_err(), "Should error on non-existent package");
}
#[tokio::test]
async fn test_integration_multiple_packages_independent_bumps() {
let (_temp, root) = create_complex_monorepo().await;
let config = PackageToolsConfig::default();
let resolver =
VersionResolver::new(root.clone(), config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("release/multi", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("@test/pkg-a");
changeset.add_package("@test/pkg-d");
let resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
assert!(resolution.updates.len() >= 2);
let direct_updates: Vec<_> = resolution.updates.iter().filter(|u| !u.is_propagated()).collect();
assert_eq!(direct_updates.len(), 2);
let result =
resolver.apply_versions(&changeset, false).await.expect("Failed to apply versions");
assert_eq!(result.summary.direct_updates, 2);
}
#[tokio::test]
async fn test_integration_all_version_bumps() {
let (_temp, root) = create_single_package().await;
let config = PackageToolsConfig::default();
let resolver =
VersionResolver::new(root.clone(), config).await.expect("Failed to create resolver");
let mut major_changeset =
Changeset::new("release/v2", VersionBump::Major, vec!["production".to_string()]);
major_changeset.add_package("single-package");
let major_resolution =
resolver.resolve_versions(&major_changeset).await.expect("Failed to resolve major");
assert_eq!(major_resolution.updates[0].next_version.to_string(), "2.0.0");
let mut minor_changeset =
Changeset::new("feature/new", VersionBump::Minor, vec!["production".to_string()]);
minor_changeset.add_package("single-package");
let minor_resolution =
resolver.resolve_versions(&minor_changeset).await.expect("Failed to resolve minor");
assert_eq!(minor_resolution.updates[0].next_version.to_string(), "1.6.0");
let mut patch_changeset =
Changeset::new("fix/bug", VersionBump::Patch, vec!["production".to_string()]);
patch_changeset.add_package("single-package");
let patch_resolution =
resolver.resolve_versions(&patch_changeset).await.expect("Failed to resolve patch");
assert_eq!(patch_resolution.updates[0].next_version.to_string(), "1.5.1");
}
#[tokio::test]
async fn test_integration_json_formatting_preservation() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
let formatted_package = r#"{
"name": "formatted-package",
"version": "1.0.0",
"description": "Test formatting",
"dependencies": {
"lodash": "^4.17.21"
}
}"#;
tokio::fs::write(root.join("package.json"), formatted_package)
.await
.expect("Failed to write package.json");
let config = PackageToolsConfig::default();
let resolver =
VersionResolver::new(root.clone(), config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("formatted-package");
let _result =
resolver.apply_versions(&changeset, false).await.expect("Failed to apply versions");
let content = tokio::fs::read_to_string(root.join("package.json"))
.await
.expect("Failed to read package.json");
assert!(content.contains(r#""version": "1.1.0""#));
assert!(content.contains(" \"name\""));
assert!(content.contains(" \"version\""));
}
#[tokio::test]
async fn test_integration_discover_packages() {
let (_temp, root) = create_complex_monorepo().await;
let config = PackageToolsConfig::default();
let resolver = VersionResolver::new(root, config).await.expect("Failed to create resolver");
let packages = resolver.discover_packages().await.expect("Failed to discover packages");
assert_eq!(packages.len(), 5, "Should discover all 5 packages");
let names: Vec<String> = packages.iter().map(|p| p.name().to_string()).collect();
assert!(names.contains(&"@test/pkg-a".to_string()));
assert!(names.contains(&"@test/pkg-b".to_string()));
assert!(names.contains(&"@test/pkg-c".to_string()));
assert!(names.contains(&"@test/pkg-d".to_string()));
assert!(names.contains(&"@test/build-tool".to_string()));
}
#[tokio::test]
async fn test_integration_edge_case_all_packages_independent() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
let root_package = r#"{
"name": "independent-root",
"version": "1.0.0",
"private": true,
"workspaces": ["packages/*"]
}"#;
tokio::fs::write(root.join("package.json"), root_package).await.expect("Failed to write root");
let package_lock = r#"{
"name": "independent-root",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}"#;
tokio::fs::write(root.join("package-lock.json"), package_lock)
.await
.expect("Failed to write package-lock.json");
tokio::fs::create_dir_all(root.join("packages")).await.expect("Failed to create packages");
for i in 1..=3 {
let pkg_dir = root.join("packages").join(format!("pkg-{}", i));
tokio::fs::create_dir_all(&pkg_dir).await.expect("Failed to create pkg dir");
let pkg = format!(
r#"{{
"name": "@independent/pkg-{}",
"version": "1.0.0",
"dependencies": {{
"external-lib": "^1.0.0"
}}
}}"#,
i
);
tokio::fs::write(pkg_dir.join("package.json"), pkg).await.expect("Failed to write package");
}
let config = PackageToolsConfig::default();
let resolver = VersionResolver::new(root, config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("@independent/pkg-1");
let resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
assert_eq!(resolution.updates.len(), 1);
assert_eq!(resolution.updates[0].name, "@independent/pkg-1");
}
#[tokio::test]
async fn test_integration_edge_case_version_none_bump() {
let (_temp, root) = create_single_package().await;
let config = PackageToolsConfig::default();
let resolver =
VersionResolver::new(root.clone(), config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("chore/update", VersionBump::None, vec!["production".to_string()]);
changeset.add_package("single-package");
let resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
assert_eq!(resolution.updates.len(), 1);
assert_eq!(resolution.updates[0].current_version, resolution.updates[0].next_version);
}
#[tokio::test]
async fn test_integration_stress_large_monorepo() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
let root_package = r#"{
"name": "large-monorepo",
"version": "1.0.0",
"private": true,
"workspaces": ["packages/*"]
}"#;
tokio::fs::write(root.join("package.json"), root_package).await.expect("Failed to write root");
let package_lock = r#"{
"name": "large-monorepo",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}"#;
tokio::fs::write(root.join("package-lock.json"), package_lock)
.await
.expect("Failed to write package-lock.json");
tokio::fs::create_dir_all(root.join("packages")).await.expect("Failed to create packages");
for i in 0..50 {
let pkg_dir = root.join("packages").join(format!("pkg-{}", i));
tokio::fs::create_dir_all(&pkg_dir).await.expect("Failed to create pkg dir");
let deps = if i > 0 {
format!(
r#","dependencies": {{
"@large/pkg-{}": "^1.0.0"
}}"#,
i - 1
)
} else {
String::new()
};
let pkg = format!(
r#"{{
"name": "@large/pkg-{}",
"version": "1.0.0"{}
}}"#,
i, deps
);
tokio::fs::write(pkg_dir.join("package.json"), pkg).await.expect("Failed to write package");
}
let config = PackageToolsConfig::default();
let resolver = VersionResolver::new(root, config).await.expect("Failed to create resolver");
let packages = resolver.discover_packages().await.expect("Failed to discover packages");
assert_eq!(packages.len(), 50);
let mut changeset =
Changeset::new("feature/test", VersionBump::Patch, vec!["production".to_string()]);
changeset.add_package("@large/pkg-0");
let resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
assert!(resolution.updates.len() > 1, "Should propagate updates");
}
#[tokio::test]
async fn test_integration_performance_resolution_speed() {
let (_temp, root) = create_deep_chain_monorepo(20).await;
let config = PackageToolsConfig::default();
let resolver = VersionResolver::new(root, config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("@chain/pkg-0");
let start = std::time::Instant::now();
let _resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
let duration = start.elapsed();
assert!(duration.as_secs() < 1, "Resolution took too long: {:?}", duration);
}
#[tokio::test]
async fn test_integration_performance_apply_speed() {
let (_temp, root) = create_complex_monorepo().await;
let config = PackageToolsConfig::default();
let resolver = VersionResolver::new(root, config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("@test/pkg-a");
let start = std::time::Instant::now();
let _result =
resolver.apply_versions(&changeset, false).await.expect("Failed to apply versions");
let duration = start.elapsed();
assert!(duration.as_millis() < 500, "Apply took too long: {:?}", duration);
}
#[tokio::test]
async fn test_integration_concurrent_resolution() {
let (_temp, root) = create_complex_monorepo().await;
let config = PackageToolsConfig::default();
let resolver = VersionResolver::new(root, config).await.expect("Failed to create resolver");
let mut changeset1 =
Changeset::new("feature/1", VersionBump::Minor, vec!["production".to_string()]);
changeset1.add_package("@test/pkg-a");
let mut changeset2 =
Changeset::new("feature/2", VersionBump::Patch, vec!["staging".to_string()]);
changeset2.add_package("@test/pkg-d");
let resolver_clone = resolver.clone();
let handle1 = tokio::spawn(async move { resolver_clone.resolve_versions(&changeset1).await });
let resolver_clone2 = resolver.clone();
let handle2 = tokio::spawn(async move { resolver_clone2.resolve_versions(&changeset2).await });
let result1 = handle1.await.expect("Task panicked").expect("Resolution 1 failed");
let result2 = handle2.await.expect("Task panicked").expect("Resolution 2 failed");
assert!(!result1.updates.is_empty());
assert!(!result2.updates.is_empty());
}
#[tokio::test]
async fn test_integration_cross_platform_paths() {
let (_temp, root) = create_complex_monorepo().await;
let config = PackageToolsConfig::default();
let resolver = VersionResolver::new(root, config).await.expect("Failed to create resolver");
let packages = resolver.discover_packages().await.expect("Failed to discover packages");
for package in packages {
let path = package.path();
assert!(path.is_absolute() || path.starts_with("packages") || path.starts_with("tools"));
assert!(path.components().count() > 0, "Path should have components");
}
}
#[tokio::test]
async fn test_integration_normalized_paths_in_resolution() {
let (_temp, root) = create_complex_monorepo().await;
let config = PackageToolsConfig::default();
let resolver =
VersionResolver::new(root.clone(), config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("@test/pkg-a");
let result =
resolver.apply_versions(&changeset, false).await.expect("Failed to apply versions");
for path in &result.modified_files {
let path_str = path.to_string_lossy();
let has_extended_prefix = cfg!(windows) && path_str.starts_with(r"\\?\");
if !has_extended_prefix {
assert!(!path_str.contains("//"), "Path should not have double slashes: {}", path_str);
assert!(
!path_str.contains("\\\\"),
"Path should not have double backslashes: {}",
path_str
);
}
assert!(path.is_absolute(), "Path should be absolute: {:?}", path);
}
}
#[tokio::test]
async fn test_integration_custom_config_propagation_bump() {
let (_temp, root) = create_complex_monorepo().await;
let mut config = PackageToolsConfig::default();
config.dependency.propagation_bump = "minor".to_string();
let resolver = VersionResolver::new(root, config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("feature/test", VersionBump::Major, vec!["production".to_string()]);
changeset.add_package("@test/pkg-a");
let resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
let propagated: Vec<_> = resolution.updates.iter().filter(|u| u.is_propagated()).collect();
for update in propagated {
assert!(
update.next_version.minor() > update.current_version.minor()
|| update.next_version.major() > update.current_version.major(),
"Propagated update should have at least minor bump"
);
}
}
#[tokio::test]
async fn test_integration_skip_protocols_config() {
let (_temp, root) = create_complex_monorepo().await;
let mut config = PackageToolsConfig::default();
config.dependency.skip_workspace_protocol = true;
config.dependency.skip_file_protocol = true;
config.dependency.skip_link_protocol = true;
let resolver = VersionResolver::new(root, config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("@test/pkg-a");
let _result =
resolver.apply_versions(&changeset, false).await.expect("Failed to apply versions");
}
#[tokio::test]
async fn test_integration_scenario_hotfix_release() {
let (_temp, root) = create_complex_monorepo().await;
let config = PackageToolsConfig::default();
let resolver =
VersionResolver::new(root.clone(), config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("hotfix/critical-bug", VersionBump::Patch, vec!["production".to_string()]);
changeset.add_package("@test/pkg-b");
let result =
resolver.apply_versions(&changeset, false).await.expect("Failed to apply versions");
assert_eq!(result.summary.direct_updates, 1);
let pkg_b_content = tokio::fs::read_to_string(root.join("packages/pkg-b/package.json"))
.await
.expect("Failed to read pkg-b");
assert!(pkg_b_content.contains(r#""version": "1.0.1""#));
}
#[tokio::test]
async fn test_integration_scenario_major_breaking_change() {
let (_temp, root) = create_complex_monorepo().await;
let config = PackageToolsConfig::default();
let resolver =
VersionResolver::new(root.clone(), config).await.expect("Failed to create resolver");
let mut changeset = Changeset::new(
"feat/breaking-api-change",
VersionBump::Major,
vec!["production".to_string()],
);
changeset.add_package("@test/pkg-a");
let resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
let pkg_a =
resolution.updates.iter().find(|u| u.name == "@test/pkg-a").expect("Should find pkg-a");
assert_eq!(pkg_a.next_version.major(), 2);
let pkg_b =
resolution.updates.iter().find(|u| u.name == "@test/pkg-b").expect("Should find pkg-b");
assert!(pkg_b.is_propagated());
}
#[tokio::test]
async fn test_integration_scenario_feature_release_multiple_packages() {
let (_temp, root) = create_complex_monorepo().await;
let config = PackageToolsConfig::default();
let resolver = VersionResolver::new(root, config).await.expect("Failed to create resolver");
let mut changeset = Changeset::new(
"feat/new-feature",
VersionBump::Minor,
vec!["production".to_string(), "staging".to_string()],
);
changeset.add_package("@test/pkg-a");
changeset.add_package("@test/pkg-b");
changeset.add_package("@test/pkg-c");
let resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
assert!(resolution.updates.len() >= 3);
let direct_updates = resolution.updates.iter().filter(|u| !u.is_propagated()).count();
assert_eq!(direct_updates, 3, "Should have 3 direct updates");
}
#[tokio::test]
async fn test_integration_scenario_preview_before_release() {
let (_temp, root) = create_complex_monorepo().await;
let config = PackageToolsConfig::default();
let resolver = VersionResolver::new(root, config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("release/v2.0", VersionBump::Major, vec!["production".to_string()]);
changeset.add_package("@test/pkg-a");
let preview = resolver.apply_versions(&changeset, true).await.expect("Failed to preview");
assert!(preview.dry_run);
println!("Preview: {} packages will be updated", preview.summary.packages_updated);
if preview.resolution.circular_dependencies.is_empty() {
let actual = resolver.apply_versions(&changeset, false).await.expect("Failed to apply");
assert!(!actual.dry_run);
assert_eq!(preview.summary.packages_updated, actual.summary.packages_updated);
}
}
#[tokio::test]
async fn test_integration_regression_empty_dependencies() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
let package = r#"{
"name": "empty-deps",
"version": "1.0.0",
"dependencies": {},
"devDependencies": {}
}"#;
tokio::fs::write(root.join("package.json"), package)
.await
.expect("Failed to write package.json");
let config = PackageToolsConfig::default();
let resolver = VersionResolver::new(root, config).await.expect("Failed to create resolver");
let mut changeset =
Changeset::new("feature/test", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("empty-deps");
let result = resolver.resolve_versions(&changeset).await;
assert!(result.is_ok(), "Should handle empty dependency objects");
}
#[tokio::test]
async fn test_integration_regression_missing_version_field() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
let package = r#"{
"name": "no-version",
"private": true
}"#;
tokio::fs::write(root.join("package.json"), package)
.await
.expect("Failed to write package.json");
let config = PackageToolsConfig::default();
let resolver = VersionResolver::new(root, config).await;
if let Ok(resolver) = resolver {
let packages_result = resolver.discover_packages().await;
if let Ok(packages) = packages_result {
assert!(
packages.is_empty() || packages[0].version().to_string() == "0.0.0",
"Package without explicit version should default or be empty"
);
}
}
}
#[tokio::test]
async fn test_integration_regression_special_characters_in_names() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
let root_pkg = r#"{
"name": "special-root",
"version": "1.0.0",
"private": true,
"workspaces": ["packages/*"]
}"#;
tokio::fs::write(root.join("package.json"), root_pkg).await.expect("Failed to write root");
let package_lock = r#"{
"name": "special-root",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}"#;
tokio::fs::write(root.join("package-lock.json"), package_lock)
.await
.expect("Failed to write package-lock.json");
tokio::fs::create_dir_all(root.join("packages")).await.expect("Failed to create packages");
let pkg_dir = root.join("packages").join("special-pkg");
tokio::fs::create_dir_all(&pkg_dir).await.expect("Failed to create pkg dir");
let pkg = r#"{
"name": "@scope/package-with-dashes_and_underscores.dots",
"version": "1.0.0"
}"#;
tokio::fs::write(pkg_dir.join("package.json"), pkg).await.expect("Failed to write package");
let config = PackageToolsConfig::default();
let resolver = VersionResolver::new(root, config).await.expect("Failed to create resolver");
let packages = resolver.discover_packages().await.expect("Failed to discover packages");
assert_eq!(packages.len(), 1);
assert_eq!(packages[0].name(), "@scope/package-with-dashes_and_underscores.dots");
}
#[tokio::test]
async fn test_integration_full_workflow_end_to_end() {
let (_temp, root) = create_complex_monorepo().await;
let config = PackageToolsConfig::default();
let resolver = VersionResolver::new(root.clone(), config.clone())
.await
.expect("Failed to initialize resolver");
assert!(resolver.is_monorepo());
assert_eq!(resolver.strategy(), VersioningStrategy::Independent);
assert_eq!(resolver.workspace_root(), &root);
let packages = resolver.discover_packages().await.expect("Failed to discover packages");
assert_eq!(packages.len(), 5);
let mut changeset =
Changeset::new("release/v1.1", VersionBump::Minor, vec!["production".to_string()]);
changeset.add_package("@test/pkg-a");
let resolution =
resolver.resolve_versions(&changeset).await.expect("Failed to resolve versions");
assert!(!resolution.updates.is_empty());
assert_eq!(resolution.circular_dependencies.len(), 0);
let preview = resolver.apply_versions(&changeset, true).await.expect("Failed to preview");
assert!(preview.dry_run);
assert_eq!(preview.modified_files.len(), 0);
assert!(preview.summary.packages_updated > 0);
let result =
resolver.apply_versions(&changeset, false).await.expect("Failed to apply versions");
assert!(!result.dry_run);
assert!(!result.modified_files.is_empty());
assert_eq!(result.summary.packages_updated, preview.summary.packages_updated);
for path in &result.modified_files {
assert!(tokio::fs::metadata(path).await.is_ok(), "Modified file should exist: {:?}", path);
}
let updated_packages =
resolver.discover_packages().await.expect("Failed to re-discover packages");
let pkg_a =
updated_packages.iter().find(|p| p.name() == "@test/pkg-a").expect("Should find pkg-a");
assert_eq!(pkg_a.version().to_string(), "1.1.0", "Version should be updated");
println!("✅ Full end-to-end workflow completed successfully");
println!(" - Packages updated: {}", result.summary.packages_updated);
println!(" - Direct updates: {}", result.summary.direct_updates);
println!(" - Propagated updates: {}", result.summary.propagated_updates);
println!(" - Files modified: {}", result.modified_files.len());
}