#![allow(clippy::expect_used)]
#![allow(clippy::panic)]
use std::collections::HashMap;
use std::path::PathBuf;
use sublime_pkg_tools::audit::{AuditManager, AuditReportExt, FormatOptions, Verbosity};
use sublime_pkg_tools::config::PackageToolsConfig;
mod common;
use common::fixtures::MonorepoFixtureBuilder;
async fn create_monorepo_with_circular_deps() -> (tempfile::TempDir, PathBuf) {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
std::process::Command::new("git")
.args(["init"])
.current_dir(&root)
.output()
.expect("Failed to init git");
std::process::Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(&root)
.output()
.expect("Failed to config git email");
std::process::Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(&root)
.output()
.expect("Failed to config git name");
let root_package = r#"{
"name": "test-monorepo",
"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": "test-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 dir");
let pkg_a_dir = root.join("packages/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",
"dependencies": {
"@test/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/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"
}
}"#;
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_monorepo_with_version_conflicts() -> (tempfile::TempDir, PathBuf) {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
std::process::Command::new("git")
.args(["init"])
.current_dir(&root)
.output()
.expect("Failed to init git");
std::process::Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(&root)
.output()
.expect("Failed to config git email");
std::process::Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(&root)
.output()
.expect("Failed to config git name");
let root_package = r#"{
"name": "conflict-monorepo",
"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": "conflict-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 dir");
let pkg_a_dir = root.join("packages/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",
"dependencies": {
"lodash": "^4.17.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/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": {
"lodash": "^4.16.0"
}
}"#;
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/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": "1.0.0",
"dependencies": {
"lodash": "~4.17.21"
}
}"#;
tokio::fs::write(pkg_c_dir.join("package.json"), pkg_c)
.await
.expect("Failed to write pkg-c package.json");
(temp_dir, root)
}
async fn create_monorepo_with_inconsistent_internal_versions() -> (tempfile::TempDir, PathBuf) {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
std::process::Command::new("git")
.args(["init"])
.current_dir(&root)
.output()
.expect("Failed to init git");
std::process::Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(&root)
.output()
.expect("Failed to config git email");
std::process::Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(&root)
.output()
.expect("Failed to config git name");
let root_package = r#"{
"name": "inconsistent-monorepo",
"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": "inconsistent-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 dir");
let pkg_core_dir = root.join("packages/core");
tokio::fs::create_dir_all(&pkg_core_dir).await.expect("Failed to create core dir");
let pkg_core = r#"{
"name": "@test/core",
"version": "2.5.0"
}"#;
tokio::fs::write(pkg_core_dir.join("package.json"), pkg_core)
.await
.expect("Failed to write core package.json");
let pkg_a_dir = root.join("packages/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",
"dependencies": {
"@test/core": "^2.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/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/core": "^2.3.0"
}
}"#;
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/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": "1.0.0",
"dependencies": {
"@test/core": "~2.5.0"
}
}"#;
tokio::fs::write(pkg_c_dir.join("package.json"), pkg_c)
.await
.expect("Failed to write pkg-c package.json");
(temp_dir, root)
}
async fn create_complex_monorepo_with_issues() -> (tempfile::TempDir, PathBuf) {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
std::process::Command::new("git")
.args(["init"])
.current_dir(&root)
.output()
.expect("Failed to init git");
std::process::Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(&root)
.output()
.expect("Failed to config git email");
std::process::Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(&root)
.output()
.expect("Failed to config git name");
let root_package = r#"{
"name": "complex-monorepo",
"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": "complex-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 dir");
tokio::fs::create_dir_all(root.join("tools")).await.expect("Failed to create tools dir");
let core_dir = root.join("packages/core");
tokio::fs::create_dir_all(&core_dir).await.expect("Failed to create core dir");
let core = r#"{
"name": "@complex/core",
"version": "3.0.0",
"dependencies": {
"lodash": "^4.17.20",
"axios": "^0.21.0"
},
"devDependencies": {
"jest": "^27.0.0"
}
}"#;
tokio::fs::write(core_dir.join("package.json"), core)
.await
.expect("Failed to write core package.json");
let utils_dir = root.join("packages/utils");
tokio::fs::create_dir_all(&utils_dir).await.expect("Failed to create utils dir");
let utils = r#"{
"name": "@complex/utils",
"version": "2.1.0",
"dependencies": {
"@complex/core": "^2.0.0",
"lodash": "^4.17.15"
}
}"#;
tokio::fs::write(utils_dir.join("package.json"), utils)
.await
.expect("Failed to write utils package.json");
let ui_dir = root.join("packages/ui");
tokio::fs::create_dir_all(&ui_dir).await.expect("Failed to create ui dir");
let ui = r#"{
"name": "@complex/ui",
"version": "1.5.0",
"dependencies": {
"@complex/core": "workspace:*",
"@complex/utils": "workspace:*",
"@complex/components": "workspace:*",
"react": "^18.0.0"
}
}"#;
tokio::fs::write(ui_dir.join("package.json"), ui)
.await
.expect("Failed to write ui package.json");
let components_dir = root.join("packages/components");
tokio::fs::create_dir_all(&components_dir).await.expect("Failed to create components dir");
let components = r#"{
"name": "@complex/components",
"version": "1.2.0",
"dependencies": {
"@complex/ui": "workspace:*",
"react": "^17.0.0"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0"
}
}"#;
tokio::fs::write(components_dir.join("package.json"), components)
.await
.expect("Failed to write components package.json");
let cli_dir = root.join("tools/cli");
tokio::fs::create_dir_all(&cli_dir).await.expect("Failed to create cli dir");
let cli = r#"{
"name": "@complex/cli",
"version": "1.0.0",
"dependencies": {
"@complex/core": "file:../../packages/core",
"commander": "^9.0.0"
}
}"#;
tokio::fs::write(cli_dir.join("package.json"), cli)
.await
.expect("Failed to write cli package.json");
(temp_dir, root)
}
async fn create_large_monorepo_for_performance() -> (tempfile::TempDir, PathBuf) {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
std::process::Command::new("git")
.args(["init"])
.current_dir(&root)
.output()
.expect("Failed to init git");
std::process::Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(&root)
.output()
.expect("Failed to config git email");
std::process::Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(&root)
.output()
.expect("Failed to config git name");
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 package.json");
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 dir");
for i in 0..50 {
let pkg_dir = root.join(format!("packages/pkg-{}", i));
tokio::fs::create_dir_all(&pkg_dir).await.expect("Failed to create package dir");
let mut deps = HashMap::new();
if i > 0 {
deps.insert(format!("@large/pkg-{}", i - 1), "workspace:*".to_string());
}
if i > 5 {
deps.insert(format!("@large/pkg-{}", i - 5), "workspace:*".to_string());
}
deps.insert("lodash".to_string(), "^4.17.21".to_string());
if i % 2 == 0 {
deps.insert("axios".to_string(), "^0.21.0".to_string());
}
if i % 3 == 0 {
deps.insert("react".to_string(), "^18.0.0".to_string());
}
let pkg_json = serde_json::json!({
"name": format!("@large/pkg-{}", i),
"version": "1.0.0",
"dependencies": deps
});
tokio::fs::write(
pkg_dir.join("package.json"),
serde_json::to_string_pretty(&pkg_json).expect("Failed to serialize"),
)
.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();
std::process::Command::new("git")
.args(["init"])
.current_dir(&root)
.output()
.expect("Failed to init git");
std::process::Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(&root)
.output()
.expect("Failed to config git email");
std::process::Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(&root)
.output()
.expect("Failed to config git name");
let package = r#"{
"name": "single-package",
"version": "1.0.0",
"dependencies": {
"lodash": "^4.17.20",
"axios": "^0.21.0"
},
"devDependencies": {
"jest": "^27.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_complete_audit_with_all_sections() {
let (_temp, root) = create_complex_monorepo_with_issues().await;
let config = PackageToolsConfig::default();
let manager =
AuditManager::new(root.clone(), config).await.expect("Failed to create audit manager");
let upgrades_result = manager.audit_upgrades().await;
assert!(upgrades_result.is_ok(), "Upgrades audit should succeed");
let deps_result = manager.audit_dependencies().await;
assert!(deps_result.is_ok(), "Dependencies audit should succeed");
let categorization_result = manager.categorize_dependencies().await;
assert!(categorization_result.is_ok(), "Categorization should succeed");
let categorization = categorization_result.expect("Should have categorization");
assert!(!categorization.internal_packages.is_empty(), "Should have internal packages");
assert!(!categorization.external_packages.is_empty(), "Should have external packages");
assert!(!categorization.workspace_links.is_empty(), "Should have workspace links");
assert!(!categorization.local_links.is_empty(), "Should have local links");
let consistency_result = manager.audit_version_consistency().await;
assert!(consistency_result.is_ok(), "Version consistency audit should succeed");
}
#[tokio::test]
async fn test_audit_circular_dependencies_detection() {
let (_temp, root) = create_monorepo_with_circular_deps().await;
let mut config = PackageToolsConfig::default();
config.audit.dependencies.check_circular = true;
let manager = AuditManager::new(root, config).await.expect("Failed to create audit manager");
let result = manager.audit_dependencies().await;
assert!(result.is_ok(), "Audit should succeed");
let section = result.expect("Should have dependency audit section");
assert!(!section.circular_dependencies.is_empty(), "Should detect circular dependencies");
assert!(
section.issues.iter().any(|i| i.title.contains("Circular")),
"Should have circular dependency issue"
);
}
#[tokio::test]
async fn test_audit_version_conflicts_detection() {
let (_temp, root) = create_monorepo_with_version_conflicts().await;
let mut config = PackageToolsConfig::default();
config.audit.dependencies.check_version_conflicts = true;
let manager = AuditManager::new(root, config).await.expect("Failed to create audit manager");
let result = manager.audit_dependencies().await;
assert!(result.is_ok(), "Audit should succeed");
let section = result.expect("Should have dependency audit section");
assert!(!section.version_conflicts.is_empty(), "Should detect version conflicts for lodash");
let has_lodash_conflict =
section.version_conflicts.iter().any(|c| c.dependency_name == "lodash");
assert!(has_lodash_conflict, "Should detect lodash version conflict");
}
#[tokio::test]
async fn test_audit_inconsistent_internal_versions() {
let (_temp, root) = create_monorepo_with_inconsistent_internal_versions().await;
let mut config = PackageToolsConfig::default();
config.audit.version_consistency.warn_on_inconsistency = true;
let manager = AuditManager::new(root, config).await.expect("Failed to create audit manager");
let result = manager.audit_version_consistency().await;
assert!(result.is_ok(), "Audit should succeed");
let section = result.expect("Should have version consistency section");
assert!(
!section.inconsistencies.is_empty(),
"Should detect @test/core version inconsistencies"
);
let has_core_inconsistency =
section.inconsistencies.iter().any(|i| i.package_name == "@test/core");
assert!(has_core_inconsistency, "Should detect @test/core version inconsistency");
}
#[tokio::test]
async fn test_audit_single_package_project() {
let (_temp, root) = create_single_package().await;
let config = PackageToolsConfig::default();
let manager = AuditManager::new(root, config).await.expect("Failed to create audit manager");
let result = manager.audit_upgrades().await;
assert!(result.is_ok(), "Single package audit should succeed");
let deps_result = manager.audit_dependencies().await;
assert!(deps_result.is_ok(), "Dependencies audit should succeed for single package");
}
#[tokio::test]
async fn test_audit_with_sections_disabled() {
let (_temp, root) = create_complex_monorepo_with_issues().await;
let mut config = PackageToolsConfig::default();
config.audit.sections.upgrades = false;
config.audit.sections.dependencies = false;
config.audit.sections.version_consistency = false;
let manager = AuditManager::new(root, config).await.expect("Failed to create audit manager");
let upgrades_result = manager.audit_upgrades().await;
assert!(upgrades_result.is_ok() || upgrades_result.is_err());
let deps_result = manager.audit_dependencies().await;
assert!(deps_result.is_ok() || deps_result.is_err());
let consistency_result = manager.audit_version_consistency().await;
assert!(consistency_result.is_ok() || consistency_result.is_err());
let cat_result = manager.categorize_dependencies().await;
assert!(cat_result.is_ok() || cat_result.is_err());
}
#[tokio::test]
async fn test_audit_categorization_workspace_protocols() {
let (_temp, root) = create_complex_monorepo_with_issues().await;
let config = PackageToolsConfig::default();
let manager = AuditManager::new(root, config).await.expect("Failed to create audit manager");
let result = manager.categorize_dependencies().await;
assert!(result.is_ok(), "Categorization should succeed");
let categorization = result.expect("Should have categorization");
assert!(!categorization.workspace_links.is_empty(), "Should detect workspace: protocol links");
let has_workspace_link =
categorization.workspace_links.iter().any(|l| l.version_spec.starts_with("workspace:"));
assert!(has_workspace_link, "Should have workspace: protocol in links");
}
#[tokio::test]
async fn test_audit_categorization_local_protocols() {
let (_temp, root) = create_complex_monorepo_with_issues().await;
let config = PackageToolsConfig::default();
let manager = AuditManager::new(root, config).await.expect("Failed to create audit manager");
let result = manager.categorize_dependencies().await;
assert!(result.is_ok(), "Categorization should succeed");
let categorization = result.expect("Should have categorization");
assert!(!categorization.local_links.is_empty(), "Should detect file: protocol links");
let has_file_link =
categorization.local_links.iter().any(|l| l.link_type.to_string() == "file");
assert!(has_file_link, "Should have file: protocol in links");
}
#[tokio::test]
async fn test_audit_categorization_statistics() {
let (_temp, root) = create_complex_monorepo_with_issues().await;
let config = PackageToolsConfig::default();
let manager = AuditManager::new(root, config).await.expect("Failed to create audit manager");
let result = manager.categorize_dependencies().await;
assert!(result.is_ok(), "Categorization should succeed");
let categorization = result.expect("Should have categorization");
let stats = &categorization.stats;
assert!(stats.total_packages > 0, "Should have total packages");
assert!(stats.internal_packages > 0, "Should have internal packages");
assert!(stats.external_packages > 0, "Should have external packages");
assert!(
stats.internal_packages + stats.external_packages > 0,
"Should have categorized packages"
);
}
#[tokio::test]
async fn test_audit_performance_large_monorepo() {
let (_temp, root) = create_large_monorepo_for_performance().await;
let config = PackageToolsConfig::default();
let start = std::time::Instant::now();
let manager = AuditManager::new(root, config).await.expect("Failed to create audit manager");
let init_duration = start.elapsed();
assert!(
init_duration.as_secs() < 5,
"Manager initialization should be fast: {:?}",
init_duration
);
let start = std::time::Instant::now();
let deps_result = manager.audit_dependencies().await;
let deps_duration = start.elapsed();
assert!(deps_result.is_ok(), "Dependencies audit should succeed");
assert!(deps_duration.as_secs() < 10, "Dependencies audit should be fast: {:?}", deps_duration);
let start = std::time::Instant::now();
let cat_result = manager.categorize_dependencies().await;
let cat_duration = start.elapsed();
assert!(cat_result.is_ok(), "Categorization should succeed");
assert!(cat_duration.as_secs() < 5, "Categorization should be fast: {:?}", cat_duration);
let start = std::time::Instant::now();
let consistency_result = manager.audit_version_consistency().await;
let consistency_duration = start.elapsed();
assert!(consistency_result.is_ok(), "Version consistency should succeed");
assert!(
consistency_duration.as_secs() < 5,
"Version consistency should be fast: {:?}",
consistency_duration
);
}
#[tokio::test]
async fn test_audit_performance_memory_efficiency() {
let (_temp, root) = create_large_monorepo_for_performance().await;
let config = PackageToolsConfig::default();
let manager = AuditManager::new(root, config).await.expect("Failed to create audit manager");
for _ in 0..3 {
let _ = manager.audit_dependencies().await;
let _ = manager.categorize_dependencies().await;
let _ = manager.audit_version_consistency().await;
}
}
#[tokio::test]
async fn test_audit_report_markdown_formatting() {
let (_temp, root) = create_monorepo_with_circular_deps().await;
let mut config = PackageToolsConfig::default();
config.audit.dependencies.check_circular = true;
let manager =
AuditManager::new(root.clone(), config).await.expect("Failed to create audit manager");
let deps_section =
manager.audit_dependencies().await.expect("Should have dependency audit section");
let upgrades_section = manager.audit_upgrades().await.expect("Should have upgrades section");
let breaking_section = sublime_pkg_tools::audit::BreakingChangesAuditSection::empty();
let categorization =
manager.categorize_dependencies().await.expect("Should have categorization");
let consistency_section =
manager.audit_version_consistency().await.expect("Should have consistency section");
use sublime_pkg_tools::audit::{AuditReport, AuditSections, calculate_health_score};
let sections = AuditSections::new(
upgrades_section,
deps_section,
breaking_section,
categorization,
consistency_section,
);
let mut all_issues = Vec::new();
all_issues.extend(sections.upgrades.issues.iter().cloned());
all_issues.extend(sections.dependencies.issues.iter().cloned());
all_issues.extend(sections.breaking_changes.issues.iter().cloned());
all_issues.extend(sections.version_consistency.issues.iter().cloned());
let health_score = calculate_health_score(&all_issues, &Default::default());
let report = AuditReport::new(root, true, sections, health_score);
let options = FormatOptions::default().with_verbosity(Verbosity::Normal).with_suggestions(true);
let markdown = report.to_markdown_with_options(&options);
assert!(markdown.contains("# Audit Report"), "Should have report header");
assert!(markdown.contains("Circular") || !markdown.is_empty(), "Should have content");
}
#[tokio::test]
async fn test_audit_report_json_formatting() {
let (_temp, root) = create_monorepo_with_version_conflicts().await;
let mut config = PackageToolsConfig::default();
config.audit.dependencies.check_version_conflicts = true;
let manager =
AuditManager::new(root.clone(), config).await.expect("Failed to create audit manager");
let deps_section =
manager.audit_dependencies().await.expect("Should have dependency audit section");
let upgrades_section = manager.audit_upgrades().await.expect("Should have upgrades section");
let breaking_section = sublime_pkg_tools::audit::BreakingChangesAuditSection::empty();
let categorization =
manager.categorize_dependencies().await.expect("Should have categorization");
let consistency_section =
manager.audit_version_consistency().await.expect("Should have consistency section");
use sublime_pkg_tools::audit::{AuditReport, AuditSections, calculate_health_score};
let sections = AuditSections::new(
upgrades_section,
deps_section,
breaking_section,
categorization,
consistency_section,
);
let mut all_issues = Vec::new();
all_issues.extend(sections.upgrades.issues.iter().cloned());
all_issues.extend(sections.dependencies.issues.iter().cloned());
all_issues.extend(sections.breaking_changes.issues.iter().cloned());
all_issues.extend(sections.version_consistency.issues.iter().cloned());
let health_score = calculate_health_score(&all_issues, &Default::default());
let report = AuditReport::new(root, true, sections, health_score);
let json = report.to_json().expect("Should format as JSON");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should be valid JSON");
assert!(parsed.is_object(), "Should be a JSON object");
}
#[tokio::test]
async fn test_audit_report_verbosity_levels() {
let (_temp, root) = create_complex_monorepo_with_issues().await;
let config = PackageToolsConfig::default();
let manager =
AuditManager::new(root.clone(), config).await.expect("Failed to create audit manager");
let deps_section =
manager.audit_dependencies().await.expect("Should have dependency audit section");
let upgrades_section = manager.audit_upgrades().await.expect("Should have upgrades section");
let breaking_section = sublime_pkg_tools::audit::BreakingChangesAuditSection::empty();
let categorization =
manager.categorize_dependencies().await.expect("Should have categorization");
let consistency_section =
manager.audit_version_consistency().await.expect("Should have consistency section");
use sublime_pkg_tools::audit::{AuditReport, AuditSections, calculate_health_score};
let sections = AuditSections::new(
upgrades_section,
deps_section,
breaking_section,
categorization,
consistency_section,
);
let mut all_issues = Vec::new();
all_issues.extend(sections.upgrades.issues.iter().cloned());
all_issues.extend(sections.dependencies.issues.iter().cloned());
all_issues.extend(sections.breaking_changes.issues.iter().cloned());
all_issues.extend(sections.version_consistency.issues.iter().cloned());
let health_score = calculate_health_score(&all_issues, &Default::default());
let report = AuditReport::new(root, true, sections, health_score);
let minimal_options =
FormatOptions::default().with_verbosity(Verbosity::Minimal).with_suggestions(false);
let minimal = report.to_markdown_with_options(&minimal_options);
let detailed_options =
FormatOptions::default().with_verbosity(Verbosity::Detailed).with_suggestions(true);
let detailed = report.to_markdown_with_options(&detailed_options);
assert!(
detailed.len() >= minimal.len(),
"Detailed output should be at least as long as minimal"
);
}
#[tokio::test]
async fn test_audit_empty_monorepo() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
std::process::Command::new("git")
.args(["init"])
.current_dir(&root)
.output()
.expect("Failed to init git");
std::process::Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(&root)
.output()
.expect("Failed to config git email");
std::process::Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(&root)
.output()
.expect("Failed to config git name");
let root_package = r#"{
"name": "empty-monorepo",
"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": "empty-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 dir");
let config = PackageToolsConfig::default();
let manager = AuditManager::new(root, config).await.expect("Failed to create audit manager");
let categorization = manager.categorize_dependencies().await;
if let Ok(cat) = categorization {
assert_eq!(cat.internal_packages.len(), 0, "Should have no internal packages");
assert_eq!(cat.stats.total_packages, 0, "Should report zero packages");
}
}
#[tokio::test]
async fn test_audit_with_custom_config() {
let (_temp, root) = create_monorepo_with_circular_deps().await;
let mut config = PackageToolsConfig::default();
config.audit.enabled = true;
config.audit.min_severity = "warning".to_string();
config.audit.sections.upgrades = true;
config.audit.sections.dependencies = true;
config.audit.sections.version_consistency = true;
config.audit.dependencies.check_circular = true;
config.audit.dependencies.check_version_conflicts = true;
let manager = AuditManager::new(root, config).await.expect("Failed to create audit manager");
let deps_result = manager.audit_dependencies().await;
assert!(deps_result.is_ok(), "Should work with custom config");
}
#[tokio::test]
async fn test_audit_concurrent_operations() {
let (_temp, root) = create_complex_monorepo_with_issues().await;
let config = PackageToolsConfig::default();
let manager = AuditManager::new(root, config).await.expect("Failed to create audit manager");
let deps_future = manager.audit_dependencies();
let cat_future = manager.categorize_dependencies();
let consistency_future = manager.audit_version_consistency();
let (deps_result, cat_result, consistency_result) =
tokio::join!(deps_future, cat_future, consistency_future);
assert!(deps_result.is_ok(), "Dependencies audit should succeed");
assert!(cat_result.is_ok(), "Categorization should succeed");
assert!(consistency_result.is_ok(), "Version consistency should succeed");
}
#[tokio::test]
async fn test_audit_real_world_monorepo_scenario() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
let fixture = MonorepoFixtureBuilder::new("real-world-app")
.add_package("packages/shared", "@app/shared", "1.0.0")
.add_package("packages/api", "@app/api", "2.0.0")
.add_package("packages/web", "@app/web", "2.1.0")
.add_package("packages/mobile", "@app/mobile", "1.5.0")
.build();
fixture.write_to_dir(&root).expect("Failed to write fixture");
std::process::Command::new("git")
.args(["init"])
.current_dir(&root)
.output()
.expect("Failed to init git");
std::process::Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(&root)
.output()
.expect("Failed to config git email");
std::process::Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(&root)
.output()
.expect("Failed to config git name");
let package_lock = r#"{
"name": "real-world-app",
"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");
let config = PackageToolsConfig::default();
let manager = AuditManager::new(root, config).await.expect("Failed to create audit manager");
let categorization =
manager.categorize_dependencies().await.expect("Should categorize dependencies");
assert!(categorization.stats.total_packages >= 4, "Should have at least 4 packages");
}
#[tokio::test]
async fn test_audit_with_scoped_packages() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let root = temp_dir.path().to_path_buf();
std::process::Command::new("git")
.args(["init"])
.current_dir(&root)
.output()
.expect("Failed to init git");
std::process::Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(&root)
.output()
.expect("Failed to config git email");
std::process::Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(&root)
.output()
.expect("Failed to config git name");
let root_package = r#"{
"name": "scoped-monorepo",
"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": "scoped-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 dir");
for scope in &["@company", "@internal", "@public"] {
let pkg_dir = root.join("packages").join(scope.trim_start_matches('@'));
tokio::fs::create_dir_all(&pkg_dir).await.expect("Failed to create package dir");
let pkg = serde_json::json!({
"name": format!("{}/core", scope),
"version": "1.0.0",
"dependencies": {
"lodash": "^4.17.21"
}
});
tokio::fs::write(
pkg_dir.join("package.json"),
serde_json::to_string_pretty(&pkg).expect("Failed to serialize"),
)
.await
.expect("Failed to write package.json");
}
let config = PackageToolsConfig::default();
let manager = AuditManager::new(root, config).await.expect("Failed to create audit manager");
let categorization =
manager.categorize_dependencies().await.expect("Should handle scoped packages");
assert!(
!categorization.internal_packages.is_empty() || categorization.stats.total_packages > 0,
"Should detect packages: internal={}, total={}",
categorization.internal_packages.len(),
categorization.stats.total_packages
);
}