mod common;
use std::process::ExitCode;
use common::{
temp_git_repo, temp_git_repo_with_cargo_workspace, temp_git_repo_with_project, write_changeset,
};
use cursus::model::config::PackageManager;
use cursus::test_logging::{init_test_logger, take_logs};
#[tokio::test]
async fn prepare_fails_when_no_config() {
let dir = temp_git_repo();
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("No configuration found"),
"Expected 'No configuration found' error, got: {err}"
);
}
#[tokio::test]
async fn prepare_with_no_changesets_is_noop() {
init_test_logger();
let _ = take_logs();
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), ExitCode::SUCCESS);
let logs = take_logs();
assert!(
logs.iter()
.any(|(level, m)| *level == log::Level::Info
&& m.contains("No pending changesets found")),
"Expected info 'No pending changesets found' log, got: {logs:?}"
);
}
#[tokio::test]
async fn prepare_with_single_changeset_cargo() {
init_test_logger();
let _ = take_logs();
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
write_changeset(
dir.path(),
"test-change.md",
"+++\ntest-project = \"minor\"\n+++\n\nAdded a feature\n",
);
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), ExitCode::SUCCESS);
let logs = take_logs();
assert!(
logs.iter().any(|(level, m)| *level == log::Level::Info
&& m.contains("test-project: 0.1.0 -> 0.2.0 (minor)")),
"Expected info version bump summary log, got: {logs:?}"
);
let cargo_toml = std::fs::read_to_string(dir.path().join("Cargo.toml")).unwrap();
assert!(
cargo_toml.contains("version = \"0.2.0\""),
"Expected version 0.2.0, got: {cargo_toml}"
);
assert!(
!dir.path().join(".cursus/test-change.md").exists(),
"Changeset file should be deleted"
);
let today = cursus::utils::today_iso_date();
let changelog = std::fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap();
assert!(
changelog.contains(&format!("## 0.2.0 - {today}")),
"Changelog should contain version header with date, got: {changelog}"
);
assert!(
changelog.contains("Added a feature"),
"Changelog should contain the message, got: {changelog}"
);
}
#[tokio::test]
async fn prepare_with_single_changeset_npm() {
init_test_logger();
let _ = take_logs();
let dir = temp_git_repo_with_project(PackageManager::Npm).await;
write_changeset(
dir.path(),
"test-change.md",
"+++\ntest-project = \"patch\"\n+++\n\nFixed a bug\n",
);
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), ExitCode::SUCCESS);
let logs = take_logs();
assert!(
logs.iter().any(|(level, m)| *level == log::Level::Info
&& m.contains("test-project: 0.1.0 -> 0.1.1 (patch)")),
"Expected info version bump summary log, got: {logs:?}"
);
let pkg_json = std::fs::read_to_string(dir.path().join("package.json")).unwrap();
assert!(
pkg_json.contains("\"0.1.1\""),
"Expected version 0.1.1, got: {pkg_json}"
);
assert!(!dir.path().join(".cursus/test-change.md").exists());
}
#[tokio::test]
async fn prepare_aggregates_to_highest_change_type() {
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
write_changeset(
dir.path(),
"change-1.md",
"+++\ntest-project = \"patch\"\n+++\n\nBug fix\n",
);
write_changeset(
dir.path(),
"change-2.md",
"+++\ntest-project = \"minor\"\n+++\n\nNew feature\n",
);
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), ExitCode::SUCCESS);
let cargo_toml = std::fs::read_to_string(dir.path().join("Cargo.toml")).unwrap();
assert!(
cargo_toml.contains("version = \"0.2.0\""),
"Expected version 0.2.0 (minor bump), got: {cargo_toml}"
);
assert!(!dir.path().join(".cursus/change-1.md").exists());
assert!(!dir.path().join(".cursus/change-2.md").exists());
}
#[tokio::test]
async fn prepare_dry_run_does_not_modify_files() {
init_test_logger();
let _ = take_logs();
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
write_changeset(
dir.path(),
"test-change.md",
"+++\ntest-project = \"major\"\n+++\n\nBreaking change\n",
);
let original_cargo = std::fs::read_to_string(dir.path().join("Cargo.toml")).unwrap();
let result = common::run_cursus(
["cursus", "--no-interactive", "prepare", "--dry-run"],
dir.path(),
)
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), ExitCode::SUCCESS);
let logs = take_logs();
assert!(
logs.iter()
.any(|(level, m)| *level == log::Level::Info && m.contains("Would consume changeset")),
"Expected info 'Would consume changeset' log in dry-run, got: {logs:?}"
);
let cargo_toml = std::fs::read_to_string(dir.path().join("Cargo.toml")).unwrap();
assert_eq!(
cargo_toml, original_cargo,
"Cargo.toml should not change in dry-run"
);
assert!(
dir.path().join(".cursus/test-change.md").exists(),
"Changeset should not be deleted in dry-run"
);
assert!(
!dir.path().join("CHANGELOG.md").exists(),
"Changelog should not be created in dry-run"
);
}
#[tokio::test]
async fn prepare_major_bump_resets_minor_and_patch() {
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"test-project\"\nversion = \"1.5.9\"\nedition = \"2024\"\n",
)
.unwrap();
write_changeset(
dir.path(),
"test-change.md",
"+++\ntest-project = \"major\"\n+++\n\nBreaking\n",
);
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok());
let cargo_toml = std::fs::read_to_string(dir.path().join("Cargo.toml")).unwrap();
assert!(
cargo_toml.contains("version = \"2.0.0\""),
"Expected version 2.0.0, got: {cargo_toml}"
);
}
#[tokio::test]
async fn prepare_idempotent_no_changesets_after_release() {
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
write_changeset(
dir.path(),
"test-change.md",
"+++\ntest-project = \"minor\"\n+++\n\nFeature\n",
);
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok());
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), ExitCode::SUCCESS);
}
#[tokio::test]
async fn prepare_changelog_has_proper_sections() {
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
write_changeset(
dir.path(),
"change-1.md",
"+++\ntest-project = \"major\"\n+++\n\nBreaking API change\n",
);
write_changeset(
dir.path(),
"change-2.md",
"+++\ntest-project = \"minor\"\n+++\n\nNew feature\n",
);
write_changeset(
dir.path(),
"change-3.md",
"+++\ntest-project = \"patch\"\n+++\n\nBug fix\n",
);
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok());
let today = cursus::utils::today_iso_date();
let changelog = std::fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap();
assert!(
changelog.contains(&format!("## 1.0.0 - {today}")),
"Should have major bumped version with date, got: {changelog}"
);
assert!(
changelog.contains("### Breaking Changes"),
"Should have breaking changes section, got: {changelog}"
);
assert!(
changelog.contains("### Features"),
"Should have features section, got: {changelog}"
);
assert!(
changelog.contains("### Bug Fixes"),
"Should have bug fixes section, got: {changelog}"
);
}
#[tokio::test]
async fn prepare_successive_releases_prepend_to_changelog() {
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
write_changeset(
dir.path(),
"change-1.md",
"+++\ntest-project = \"minor\"\n+++\n\nFirst feature\n",
);
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok());
write_changeset(
dir.path(),
"change-2.md",
"+++\ntest-project = \"patch\"\n+++\n\nA bug fix\n",
);
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok());
let today = cursus::utils::today_iso_date();
let changelog = std::fs::read_to_string(dir.path().join("CHANGELOG.md")).unwrap();
assert!(
changelog.contains(&format!("## 0.2.1 - {today}")),
"Should contain newer version with date, got: {changelog}"
);
assert!(
changelog.contains(&format!("## 0.2.0 - {today}")),
"Should contain older version with date, got: {changelog}"
);
let pos_new = changelog.find(&format!("## 0.2.1 - {today}")).unwrap();
let pos_old = changelog.find(&format!("## 0.2.0 - {today}")).unwrap();
assert!(
pos_new < pos_old,
"Newer version should appear before older version in changelog, got: {changelog}"
);
}
#[tokio::test]
async fn prepare_unknown_package_in_changeset_fails() {
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
write_changeset(
dir.path(),
"test-change.md",
"+++\nnonexistent-package = \"minor\"\n+++\n\nSome change\n",
);
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("not found in projects"),
"Expected 'not found in projects' error, got: {err}"
);
}
#[tokio::test]
async fn prepare_package_flag_filters_packages() {
let dir = temp_git_repo_with_cargo_workspace(&[("pkg-a", "0.1.0"), ("pkg-b", "0.2.0")]).await;
write_changeset(
dir.path(),
"test-change.md",
"+++\npkg-a = \"patch\"\npkg-b = \"minor\"\n+++\n\nSome change\n",
);
let result = common::run_cursus(
[
"cursus",
"--no-interactive",
"prepare",
"--package",
"pkg-a",
],
dir.path(),
)
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), ExitCode::SUCCESS);
let pkg_a_toml = std::fs::read_to_string(dir.path().join("pkg-a/Cargo.toml")).unwrap();
assert!(
pkg_a_toml.contains("version = \"0.1.1\""),
"Expected pkg-a version 0.1.1, got: {pkg_a_toml}"
);
let pkg_b_toml = std::fs::read_to_string(dir.path().join("pkg-b/Cargo.toml")).unwrap();
assert!(
pkg_b_toml.contains("version = \"0.2.0\""),
"Expected pkg-b version 0.2.0 (unchanged), got: {pkg_b_toml}"
);
assert!(
dir.path().join(".cursus/test-change.md").exists(),
"Changeset should still exist (partially consumed)"
);
let rewritten = std::fs::read_to_string(dir.path().join(".cursus/test-change.md")).unwrap();
insta::assert_snapshot!(rewritten);
}
#[tokio::test]
async fn prepare_scoped_fully_consumed_changeset_is_deleted() {
let dir = temp_git_repo_with_cargo_workspace(&[("pkg-a", "0.1.0")]).await;
write_changeset(
dir.path(),
"only-a.md",
"+++\npkg-a = \"patch\"\n+++\n\nFix in pkg-a\n",
);
let result = common::run_cursus(
[
"cursus",
"--no-interactive",
"prepare",
"--package",
"pkg-a",
],
dir.path(),
)
.await;
assert!(result.is_ok());
assert!(
!dir.path().join(".cursus/only-a.md").exists(),
"Fully consumed changeset should be deleted"
);
}
#[tokio::test]
async fn prepare_scoped_sequential_releases() {
let dir = temp_git_repo_with_cargo_workspace(&[("pkg-a", "0.1.0"), ("pkg-b", "0.2.0")]).await;
write_changeset(
dir.path(),
"shared.md",
"+++\npkg-a = \"patch\"\npkg-b = \"minor\"\n+++\n\nShared change\n",
);
let result = common::run_cursus(
[
"cursus",
"--no-interactive",
"prepare",
"--package",
"pkg-a",
],
dir.path(),
)
.await;
assert!(result.is_ok(), "First release failed: {result:?}");
assert!(
dir.path().join(".cursus/shared.md").exists(),
"Changeset should still exist after first release"
);
let intermediate = std::fs::read_to_string(dir.path().join(".cursus/shared.md")).unwrap();
insta::assert_snapshot!(intermediate);
let pkg_a_toml = std::fs::read_to_string(dir.path().join("pkg-a/Cargo.toml")).unwrap();
assert!(
pkg_a_toml.contains("version = \"0.1.1\""),
"pkg-a should be at 0.1.1, got: {pkg_a_toml}"
);
let result = common::run_cursus(
[
"cursus",
"--no-interactive",
"prepare",
"--package",
"pkg-b",
],
dir.path(),
)
.await;
assert!(result.is_ok(), "Second release failed: {result:?}");
assert!(
!dir.path().join(".cursus/shared.md").exists(),
"Changeset should be deleted after second release"
);
let pkg_b_toml = std::fs::read_to_string(dir.path().join("pkg-b/Cargo.toml")).unwrap();
assert!(
pkg_b_toml.contains("version = \"0.3.0\""),
"pkg-b should be at 0.3.0, got: {pkg_b_toml}"
);
}
#[tokio::test]
async fn prepare_scoped_unrelated_changeset_untouched() {
let dir = temp_git_repo_with_cargo_workspace(&[("pkg-a", "0.1.0"), ("pkg-b", "0.2.0")]).await;
let pkg_b_content = "+++\npkg-b = \"minor\"\n+++\n\nPkg-b only change\n";
write_changeset(
dir.path(),
"only-a.md",
"+++\npkg-a = \"patch\"\n+++\n\nPkg-a only change\n",
);
write_changeset(dir.path(), "only-b.md", pkg_b_content);
let result = common::run_cursus(
[
"cursus",
"--no-interactive",
"prepare",
"--package",
"pkg-a",
],
dir.path(),
)
.await;
assert!(result.is_ok());
assert!(
!dir.path().join(".cursus/only-a.md").exists(),
"pkg-a changeset should be deleted"
);
assert!(
dir.path().join(".cursus/only-b.md").exists(),
"pkg-b changeset should be untouched"
);
let b_content = std::fs::read_to_string(dir.path().join(".cursus/only-b.md")).unwrap();
assert_eq!(
b_content, pkg_b_content,
"pkg-b changeset should be unchanged"
);
}
#[tokio::test]
async fn prepare_unknown_package_flag_fails() {
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
write_changeset(
dir.path(),
"test-change.md",
"+++\ntest-project = \"minor\"\n+++\n\nSome change\n",
);
let result = common::run_cursus(
[
"cursus",
"--no-interactive",
"prepare",
"--package",
"nonexistent",
],
dir.path(),
)
.await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("Unknown package: nonexistent"),
"Expected 'Unknown package: nonexistent' error, got: {err}"
);
}
#[tokio::test]
async fn prepare_updates_cargo_lock_file() {
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
write_changeset(
dir.path(),
"test-change.md",
"+++\ntest-project = \"minor\"\n+++\n\nAdded a feature\n",
);
let lock_file = dir.path().join("Cargo.lock");
if lock_file.exists() {
std::fs::remove_file(&lock_file).unwrap();
}
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
if let Err(ref err) = result {
eprintln!("Release failed: {:#}", err);
}
assert!(result.is_ok());
assert_eq!(result.unwrap(), std::process::ExitCode::SUCCESS);
assert!(
lock_file.exists(),
"Cargo.lock should be created after release"
);
let lock_content = std::fs::read_to_string(&lock_file).unwrap();
assert!(
lock_content.contains("0.2.0"),
"Cargo.lock should contain the new version, got: {lock_content}"
);
}
#[tokio::test]
async fn prepare_updates_cargo_intra_workspace_dep_version() {
let dir = temp_git_repo_with_cargo_workspace(&[("pkg-a", "0.1.0"), ("pkg-b", "0.2.0")]).await;
std::fs::write(
dir.path().join("pkg-a/Cargo.toml"),
"[package]\nname = \"pkg-a\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\npkg-b = { path = \"../pkg-b\", version = \"0.2.0\" }\n",
)
.unwrap();
write_changeset(
dir.path(),
"bump-b.md",
"+++\npkg-b = \"minor\"\n+++\n\nAdded feature to pkg-b\n",
);
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok(), "release failed: {:?}", result.unwrap_err());
let pkg_b_toml = std::fs::read_to_string(dir.path().join("pkg-b/Cargo.toml")).unwrap();
assert!(
pkg_b_toml.contains("version = \"0.3.0\""),
"Expected pkg-b version 0.3.0, got: {pkg_b_toml}"
);
let pkg_a_toml = std::fs::read_to_string(dir.path().join("pkg-a/Cargo.toml")).unwrap();
assert!(
pkg_a_toml.contains("version = \"0.3.0\""),
"Expected pkg-a to reference pkg-b 0.3.0, got: {pkg_a_toml}"
);
assert!(
pkg_a_toml.contains("path = \"../pkg-b\""),
"Expected path dep to be preserved, got: {pkg_a_toml}"
);
}
#[tokio::test]
async fn prepare_updates_cargo_workspace_dep_in_root() {
let dir = temp_git_repo_with_cargo_workspace(&[("pkg-a", "0.1.0"), ("pkg-b", "0.2.0")]).await;
std::fs::write(
dir.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"pkg-a\", \"pkg-b\"]\n\n[workspace.dependencies]\npkg-b = { path = \"pkg-b\", version = \"0.2.0\" }\n",
)
.unwrap();
std::fs::write(
dir.path().join("pkg-a/Cargo.toml"),
"[package]\nname = \"pkg-a\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\npkg-b = { workspace = true }\n",
)
.unwrap();
write_changeset(
dir.path(),
"bump-b.md",
"+++\npkg-b = \"minor\"\n+++\n\nAdded feature\n",
);
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok(), "release failed: {:?}", result.unwrap_err());
let root_toml = std::fs::read_to_string(dir.path().join("Cargo.toml")).unwrap();
assert!(
root_toml.contains("version = \"0.3.0\""),
"Expected root [workspace.dependencies] pkg-b version 0.3.0, got: {root_toml}"
);
assert!(
root_toml.contains("path = \"pkg-b\""),
"Expected path to be preserved in workspace.dependencies, got: {root_toml}"
);
}
#[tokio::test]
async fn prepare_dry_run_shows_dep_updates_without_modifying_files() {
let dir = temp_git_repo_with_cargo_workspace(&[("pkg-a", "0.1.0"), ("pkg-b", "0.2.0")]).await;
std::fs::write(
dir.path().join("pkg-a/Cargo.toml"),
"[package]\nname = \"pkg-a\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\npkg-b = { path = \"../pkg-b\", version = \"0.2.0\" }\n",
)
.unwrap();
write_changeset(
dir.path(),
"bump-b.md",
"+++\npkg-b = \"patch\"\n+++\n\nBug fix\n",
);
let original_a = std::fs::read_to_string(dir.path().join("pkg-a/Cargo.toml")).unwrap();
let result = common::run_cursus(
["cursus", "--no-interactive", "prepare", "--dry-run"],
dir.path(),
)
.await;
assert!(result.is_ok());
let after_a = std::fs::read_to_string(dir.path().join("pkg-a/Cargo.toml")).unwrap();
assert_eq!(original_a, after_a, "dry-run must not modify dep files");
}
#[tokio::test]
async fn prepare_branch_arg_with_push_strategy_warns() {
init_test_logger();
let _ = take_logs();
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
write_changeset(
dir.path(),
"change.md",
"+++\ntest-project = \"patch\"\n+++\n\nFix\n",
);
let result = common::run_cursus(
[
"cursus",
"--no-interactive",
"prepare",
"--branch",
"custom-branch",
],
dir.path(),
)
.await;
assert!(result.is_ok(), "Expected Ok, got: {result:?}");
let logs = take_logs();
assert!(
logs.iter()
.any(|(level, m)| *level == log::Level::Warn && m.contains("no effect")),
"Expected a 'no effect' warning when --branch is given with push strategy, got: {logs:?}"
);
}
#[tokio::test]
async fn prepare_no_branch_arg_with_push_strategy_no_warning() {
init_test_logger();
let _ = take_logs();
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
write_changeset(
dir.path(),
"change.md",
"+++\ntest-project = \"patch\"\n+++\n\nFix\n",
);
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok(), "Expected Ok, got: {result:?}");
let logs = take_logs();
assert!(
!logs
.iter()
.any(|(_, m)| m.contains("no effect") && m.contains("branch")),
"Should not warn about --branch when it was not passed, got: {logs:?}"
);
}
#[tokio::test]
async fn prepare_updates_npm_intra_workspace_dep_version() {
let dir = temp_git_repo();
let env = common::test_env(dir.path());
let config =
cursus::model::config::Config::new().with_npm(cursus::model::config::NpmConfig::enabled());
config.save(env.fs(), env.git().path()).await.unwrap();
std::fs::write(
dir.path().join("package.json"),
r#"{"name": "root", "version": "0.0.0", "private": true, "workspaces": ["pkg-a", "pkg-b"]}"#,
)
.unwrap();
std::fs::create_dir_all(dir.path().join("pkg-a")).unwrap();
std::fs::write(
dir.path().join("pkg-a/package.json"),
r#"{"name": "pkg-a", "version": "0.1.0", "dependencies": {"pkg-b": "^0.2.0"}}"#,
)
.unwrap();
std::fs::create_dir_all(dir.path().join("pkg-b")).unwrap();
std::fs::write(
dir.path().join("pkg-b/package.json"),
r#"{"name": "pkg-b", "version": "0.2.0"}"#,
)
.unwrap();
write_changeset(
dir.path(),
"bump-b.md",
"+++\npkg-b = \"minor\"\n+++\n\nAdded feature to pkg-b\n",
);
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok(), "release failed: {:?}", result.unwrap_err());
let pkg_b_json = std::fs::read_to_string(dir.path().join("pkg-b/package.json")).unwrap();
assert!(
pkg_b_json.contains("\"0.3.0\""),
"Expected pkg-b version 0.3.0, got: {pkg_b_json}"
);
let pkg_a_json = std::fs::read_to_string(dir.path().join("pkg-a/package.json")).unwrap();
assert!(
pkg_a_json.contains("\"pkg-b\": \"^0.3.0\""),
"Expected pkg-a to reference pkg-b ^0.3.0, got: {pkg_a_json}"
);
}
#[tokio::test]
async fn prepare_skips_workspace_protocol_dep_version() {
let dir = temp_git_repo();
let env = common::test_env(dir.path());
let config =
cursus::model::config::Config::new().with_npm(cursus::model::config::NpmConfig::enabled());
config.save(env.fs(), env.git().path()).await.unwrap();
std::fs::write(
dir.path().join("package.json"),
r#"{"name": "root", "version": "0.0.0", "private": true, "workspaces": ["pkg-a", "pkg-b"]}"#,
)
.unwrap();
std::fs::create_dir_all(dir.path().join("pkg-a")).unwrap();
std::fs::write(
dir.path().join("pkg-a/package.json"),
r#"{"name": "pkg-a", "version": "0.1.0", "dependencies": {"pkg-b": "workspace:*"}}"#,
)
.unwrap();
std::fs::create_dir_all(dir.path().join("pkg-b")).unwrap();
std::fs::write(
dir.path().join("pkg-b/package.json"),
r#"{"name": "pkg-b", "version": "0.2.0"}"#,
)
.unwrap();
write_changeset(
dir.path(),
"bump-b.md",
"+++\npkg-b = \"minor\"\n+++\n\nAdded feature to pkg-b\n",
);
let result = common::run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok(), "prepare failed: {:?}", result.unwrap_err());
let pkg_b_json = std::fs::read_to_string(dir.path().join("pkg-b/package.json")).unwrap();
assert!(
pkg_b_json.contains("\"0.3.0\""),
"Expected pkg-b version 0.3.0, got: {pkg_b_json}"
);
let pkg_a_json = std::fs::read_to_string(dir.path().join("pkg-a/package.json")).unwrap();
assert!(
pkg_a_json.contains("\"pkg-b\": \"workspace:*\""),
"Expected workspace: dep to be preserved unchanged, got: {pkg_a_json}"
);
}