use super::*;
use crate::contract::schema::{
Adapter, Changelog, ChangelogMode, ChangelogSource, Contract, ContributionProvenance,
DependencyBot, Distribution, DistributionAdapter, DocsSite, Ecosystem, Installer, Maturity,
ProvenanceLevel, Registry, Release, ReleaseLayout, ReleaseModel, Status, Target,
VersioningBase,
};
use crate::protocol::facts::{Facts, MaturitySignals, Package, RustWorkspace, WorkspaceMember};
use crate::protocol::plan::PlanPhase;
#[test]
fn sha256_empty_string() {
assert_eq!(
sha256::hex(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn sha256_abc() {
assert_eq!(
sha256::hex(b"abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
#[test]
fn sha256_multiblock() {
assert_eq!(
sha256::hex(b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"),
"248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"
);
}
#[test]
fn sha256_one_million_a() {
let data = vec![b'a'; 1_000_000];
assert_eq!(
sha256::hex(&data),
"cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"
);
}
#[test]
fn sha256_pad_boundaries_are_deterministic_and_distinct() {
let mut seen = std::collections::HashSet::new();
for len in [54usize, 55, 56, 57, 63, 64, 65] {
let data = vec![b'x'; len];
let a = sha256::hex(&data);
let b = sha256::hex(&data);
assert_eq!(a, b, "len {len} must be deterministic");
assert_eq!(a.len(), 64);
assert!(seen.insert(a), "len {len} collided with another length");
}
}
fn rust_contract() -> Contract {
Contract {
schema_version: 1,
status: Status::Approved,
maturity: Maturity::Mvp,
ecosystems: vec![Ecosystem::Rust],
targets: vec![Target {
ecosystem: Ecosystem::Rust,
package: None,
registry: Registry::CratesIo,
adapter: Adapter::CargoPublish,
}],
distributions: vec![],
versioning: VersioningBase::Semver,
versioning_pattern: None,
changelog: Changelog {
mode: ChangelogMode::Curated,
source: ChangelogSource::Manual,
fragment_dir: "changelog/fragments".to_string(),
},
conventional_commits: false,
release: Release {
model: ReleaseModel::Gated,
layout: ReleaseLayout::Single,
bump_hook: None,
},
contribution_provenance: ContributionProvenance::None,
provenance_level: ProvenanceLevel::None,
dependency_bot: DependencyBot::None,
health_badges: vec![],
license: "MIT".to_string(),
docs_site: DocsSite::None,
extra_fields: serde_json::Map::new(),
warnings: vec![],
}
}
fn rust_facts() -> Facts {
Facts {
repo_root: "/repo".to_string(),
is_git: true,
has_commits: true,
ecosystems: vec![Ecosystem::Rust],
packages: vec![Package {
ecosystem: Ecosystem::Rust,
manifest: "Cargo.toml".to_string(),
package: Some("acme".to_string()),
version: Some("0.1.0".to_string()),
}],
committers_total: 3,
committers_recent_year: 2,
tags: vec!["v0.1.0".to_string()],
has_semver_tag: true,
has_ge_1_0_release: false,
has_ci: true,
dependency_bot: None,
has_issues_dir: false,
readme_self_label: None,
description: Some("An acme crate".to_string()),
maturity_signals: MaturitySignals {
production: false,
spike: false,
},
inferred_maturity: Maturity::Mvp,
rust_workspace: None,
}
}
const HEAD: &str = "0123456789abcdef0123456789abcdef01234567";
#[test]
fn plan_id_is_stable_for_identical_inputs() {
let (c, f) = (rust_contract(), rust_facts());
let a = build(&c, &f, HEAD, "1.2.0");
let b = build(&c, &f, HEAD, "1.2.0");
assert_eq!(
a.plan_id, b.plan_id,
"identical inputs must seal the same id"
);
assert_eq!(a.plan_id.len(), 64);
assert!(a
.plan_id
.chars()
.all(|ch| ch.is_ascii_hexdigit() && !ch.is_ascii_uppercase()));
}
#[test]
fn compute_plan_id_matches_build() {
let (c, f) = (rust_contract(), rust_facts());
let plan = build(&c, &f, HEAD, "1.2.0");
assert_eq!(plan.plan_id, compute_plan_id(&c, &f, HEAD, "1.2.0"));
}
#[test]
fn changed_head_changes_the_id() {
let (c, f) = (rust_contract(), rust_facts());
let base = compute_plan_id(&c, &f, HEAD, "1.2.0");
let other = compute_plan_id(&c, &f, "ffffffffffffffffffffffffffffffffffffffff", "1.2.0");
assert_ne!(base, other);
}
#[test]
fn changed_version_changes_the_id() {
let (c, f) = (rust_contract(), rust_facts());
assert_ne!(
compute_plan_id(&c, &f, HEAD, "1.2.0"),
compute_plan_id(&c, &f, HEAD, "1.3.0")
);
}
#[test]
fn changed_contract_field_changes_the_id() {
let (c, f) = (rust_contract(), rust_facts());
let base = compute_plan_id(&c, &f, HEAD, "1.2.0");
let mut c2 = c.clone();
c2.license = "Apache-2.0".to_string();
assert_ne!(base, compute_plan_id(&c2, &f, HEAD, "1.2.0"));
}
#[test]
fn changed_resolved_package_changes_the_id() {
let c = rust_contract();
let base = compute_plan_id(&c, &rust_facts(), HEAD, "1.2.0");
let mut f2 = rust_facts();
f2.packages[0].package = Some("renamed".to_string());
assert_ne!(base, compute_plan_id(&c, &f2, HEAD, "1.2.0"));
}
#[test]
fn changed_adapter_changes_the_id() {
let c = rust_contract();
let base = compute_plan_id(&c, &rust_facts(), HEAD, "1.2.0");
let mut c2 = c.clone();
c2.targets[0].adapter = Adapter::CargoDist;
assert_ne!(base, compute_plan_id(&c2, &rust_facts(), HEAD, "1.2.0"));
}
#[test]
fn changed_registry_changes_the_id() {
let c = rust_contract();
let base = compute_plan_id(&c, &rust_facts(), HEAD, "1.2.0");
let mut c2 = c.clone();
c2.targets[0].registry = Registry::TestPypi;
assert_ne!(base, compute_plan_id(&c2, &rust_facts(), HEAD, "1.2.0"));
}
#[test]
fn plan_id_golden_vector() {
let plan = build(&rust_contract(), &rust_facts(), HEAD, "1.2.0");
assert_eq!(
plan.plan_id,
"5ee31eacdddd882dfb69bd63f7fcbeee98b00a4f7fd7a46f1dd78ff769ebf703"
);
}
#[test]
fn plan_id_golden_vector_with_distribution() {
let mut contract = rust_contract();
contract.distributions = vec![Distribution {
package: Some("acme".to_string()),
adapter: DistributionAdapter::CargoDist,
gh_releases: true,
installers: vec![Installer::Shell, Installer::Homebrew],
homebrew_tap: Some("acme/homebrew-acme".to_string()),
platforms: vec![
"aarch64-apple-darwin".to_string(),
"x86_64-unknown-linux-musl".to_string(),
],
extra_fields: serde_json::Map::new(),
}];
let plan = build(&contract, &rust_facts(), HEAD, "1.2.0");
assert_eq!(
plan.plan_id,
"d0a9b8debb288ad7b6b9fc96226fc1113220e4f77c283c005c1335be5e2b5e9d"
);
assert_eq!(plan.homebrew_tap.as_deref(), Some("acme/homebrew-acme"));
}
#[test]
fn verify_ok_when_repo_unchanged() {
let (c, f) = (rust_contract(), rust_facts());
let approved = build(&c, &f, HEAD, "1.2.0");
assert!(verify(&approved, &c, &f, HEAD).is_ok());
}
#[test]
fn verify_reports_head_drift() {
let (c, f) = (rust_contract(), rust_facts());
let approved = build(&c, &f, HEAD, "1.2.0");
let moved = "ffffffffffffffffffffffffffffffffffffffff";
let drift = verify(&approved, &c, &f, moved).unwrap_err();
assert_eq!(drift.approved_plan_id, approved.plan_id);
assert_ne!(drift.current_plan_id, approved.plan_id);
assert!(
drift.reasons.iter().any(|r| r.contains("HEAD moved")),
"reasons: {:?}",
drift.reasons
);
}
#[test]
fn verify_reports_target_drift() {
let (c, f) = (rust_contract(), rust_facts());
let approved = build(&c, &f, HEAD, "1.2.0");
let mut f2 = f.clone();
f2.packages[0].package = Some("renamed".to_string());
let drift = verify(&approved, &c, &f2, HEAD).unwrap_err();
assert!(
drift.reasons.iter().any(|r| r.contains("target set")),
"reasons: {:?}",
drift.reasons
);
}
#[test]
fn verify_reports_contract_change_when_no_specific_probe_matches() {
let (c, f) = (rust_contract(), rust_facts());
let approved = build(&c, &f, HEAD, "1.2.0");
let mut c2 = c.clone();
c2.dependency_bot = DependencyBot::Dependabot; let drift = verify(&approved, &c2, &f, HEAD).unwrap_err();
assert!(
drift
.reasons
.iter()
.any(|r| r.contains("normalized contract changed")),
"reasons: {:?}",
drift.reasons
);
}
#[test]
fn build_resolves_null_package_from_facts() {
let plan = build(&rust_contract(), &rust_facts(), HEAD, "1.2.0");
assert_eq!(plan.targets.len(), 1);
let t = &plan.targets[0];
assert_eq!(t.ecosystem, Ecosystem::Rust);
assert_eq!(t.package.as_deref(), Some("acme"));
assert_eq!(t.registry, Registry::CratesIo);
assert_eq!(t.adapter, Adapter::CargoPublish);
}
#[test]
fn build_leaves_package_null_when_facts_have_none() {
let c = rust_contract();
let mut f = rust_facts();
f.packages.clear();
let plan = build(&c, &f, HEAD, "1.2.0");
assert_eq!(plan.targets[0].package, None);
}
#[test]
fn build_leaves_package_null_when_ecosystem_is_ambiguous() {
let c = rust_contract();
let mut f = rust_facts();
f.packages = vec![
Package {
ecosystem: Ecosystem::Rust,
manifest: "crates/a/Cargo.toml".to_string(),
package: Some("a".to_string()),
version: Some("0.1.0".to_string()),
},
Package {
ecosystem: Ecosystem::Rust,
manifest: "crates/b/Cargo.toml".to_string(),
package: Some("b".to_string()),
version: Some("0.1.0".to_string()),
},
];
let plan = build(&c, &f, HEAD, "1.2.0");
assert_eq!(plan.targets[0].package, None);
}
#[test]
fn ambiguous_resolution_does_not_depend_on_facts_order() {
let c = rust_contract();
let mut f1 = rust_facts();
f1.packages = vec![
Package {
ecosystem: Ecosystem::Rust,
manifest: "crates/a/Cargo.toml".to_string(),
package: Some("a".to_string()),
version: None,
},
Package {
ecosystem: Ecosystem::Rust,
manifest: "crates/b/Cargo.toml".to_string(),
package: Some("b".to_string()),
version: None,
},
];
let mut f2 = f1.clone();
f2.packages.reverse();
assert_eq!(
compute_plan_id(&c, &f1, HEAD, "1.2.0"),
compute_plan_id(&c, &f2, HEAD, "1.2.0")
);
}
#[test]
fn build_with_no_targets_yields_empty_targets_and_stable_id() {
let mut c = rust_contract();
c.ecosystems.clear();
c.targets.clear();
let plan = build(&c, &rust_facts(), HEAD, "1.2.0");
assert!(plan.targets.is_empty());
assert_eq!(plan.plan_id.len(), 64);
assert_eq!(
plan.plan_id,
compute_plan_id(&c, &rust_facts(), HEAD, "1.2.0")
);
}
#[test]
fn build_keeps_explicit_contract_package_over_facts() {
let mut c = rust_contract();
c.targets[0].package = Some("explicit".to_string());
let plan = build(&c, &rust_facts(), HEAD, "1.2.0");
assert_eq!(plan.targets[0].package.as_deref(), Some("explicit"));
}
#[test]
fn build_emits_the_invariant_phase_sequence() {
let plan = build(&rust_contract(), &rust_facts(), HEAD, "1.2.0");
assert_eq!(
plan.phases,
vec![
PlanPhase::DryRunAll,
PlanPhase::BuildAll,
PlanPhase::PublishAll,
PlanPhase::Tag,
PlanPhase::Dist
]
);
assert_eq!(plan.contract_schema_version, 1);
assert_eq!(plan.head_sha, HEAD);
assert_eq!(plan.version, "1.2.0");
}
#[test]
fn build_preserves_multi_target_order() {
let mut c = rust_contract();
c.ecosystems = vec![Ecosystem::Rust, Ecosystem::Python];
c.targets = vec![
Target {
ecosystem: Ecosystem::Rust,
package: None,
registry: Registry::CratesIo,
adapter: Adapter::CargoPublish,
},
Target {
ecosystem: Ecosystem::Python,
package: None,
registry: Registry::Pypi,
adapter: Adapter::GhActionPypiPublish,
},
];
let mut f = rust_facts();
f.packages.push(Package {
ecosystem: Ecosystem::Python,
manifest: "pyproject.toml".to_string(),
package: Some("acme-py".to_string()),
version: Some("0.1.0".to_string()),
});
let plan = build(&c, &f, HEAD, "1.2.0");
let seq: Vec<_> = plan.targets.iter().map(|t| t.ecosystem).collect();
assert_eq!(seq, vec![Ecosystem::Rust, Ecosystem::Python]);
assert_eq!(plan.targets[1].package.as_deref(), Some("acme-py"));
}
fn target(ecosystem: Ecosystem, package: &str, registry: Registry, adapter: Adapter) -> Target {
Target {
ecosystem,
package: Some(package.to_string()),
registry,
adapter,
}
}
fn package(ecosystem: Ecosystem, manifest: &str, name: &str, version: Option<&str>) -> Package {
Package {
ecosystem,
manifest: manifest.to_string(),
package: Some(name.to_string()),
version: version.map(str::to_string),
}
}
#[test]
fn version_derived_from_the_manifest() {
let v = resolve_release_version(&rust_contract(), &rust_facts())
.expect("the manifest version must resolve");
assert_eq!(v, "0.1.0");
}
#[test]
fn a_lockstep_two_crate_workspace_derives_the_shared_version() {
let mut c = rust_contract();
c.targets = vec![
target(
Ecosystem::Rust,
"acme",
Registry::CratesIo,
Adapter::CargoPublish,
),
target(
Ecosystem::Rust,
"acme-core",
Registry::CratesIo,
Adapter::CargoPublish,
),
];
let mut f = rust_facts();
f.packages.push(package(
Ecosystem::Rust,
"core/Cargo.toml",
"acme-core",
Some("0.1.0"),
));
let v = resolve_release_version(&c, &f).expect("a lockstep workspace resolves");
assert_eq!(v, "0.1.0");
}
#[test]
fn an_inconsistent_tree_has_no_single_source_of_truth() {
let mut c = rust_contract();
c.targets = vec![
target(
Ecosystem::Rust,
"acme",
Registry::CratesIo,
Adapter::CargoPublish,
),
target(
Ecosystem::Rust,
"acme-core",
Registry::CratesIo,
Adapter::CargoPublish,
),
];
let mut f = rust_facts();
f.packages.push(package(
Ecosystem::Rust,
"core/Cargo.toml",
"acme-core",
Some("0.2.0"),
));
let err =
resolve_release_version(&c, &f).expect_err("a self-inconsistent tree must be rejected");
match err {
VersionResolveError::InconsistentTree { versions } => {
let pairs: Vec<(&str, &str)> = versions
.iter()
.map(|m| (m.package.as_str(), m.manifest_version.as_str()))
.collect();
assert_eq!(pairs, vec![("acme", "0.1.0"), ("acme-core", "0.2.0")]);
}
other => panic!("expected an InconsistentTree, got {other:?}"),
}
}
#[test]
fn no_manifest_version_anywhere_is_undeterminable() {
let mut c = rust_contract();
c.targets = vec![Target {
ecosystem: Ecosystem::Rust,
package: None,
registry: Registry::CratesIo,
adapter: Adapter::CargoPublish,
}];
let mut f = rust_facts();
f.packages.clear();
assert!(matches!(
resolve_release_version(&c, &f),
Err(VersionResolveError::Undeterminable)
));
}
#[test]
fn version_source_classifies_every_ecosystem() {
for e in [Ecosystem::Rust, Ecosystem::Node, Ecosystem::Python] {
assert_eq!(VersionSource::of(e), VersionSource::Manifest, "{e:?}");
}
for e in [Ecosystem::Binary, Ecosystem::Go] {
assert_eq!(VersionSource::of(e), VersionSource::Distribution, "{e:?}");
}
}
#[test]
fn a_manifest_versioned_npm_target_without_a_detected_version_fails_closed() {
let mut c = rust_contract();
c.ecosystems = vec![Ecosystem::Node];
c.targets = vec![target(
Ecosystem::Node,
"acme-js",
Registry::Npm,
Adapter::NpmPublish,
)];
let mut f = rust_facts();
f.packages = vec![package(Ecosystem::Node, "package.json", "acme-js", None)];
let err =
resolve_release_version(&c, &f).expect_err("a versionless npm target must fail closed");
match err {
VersionResolveError::MissingManifestVersion { targets } => {
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].package, "acme-js");
assert_eq!(targets[0].ecosystem, Ecosystem::Node);
assert_eq!(targets[0].registry, Registry::Npm);
}
other => panic!("expected MissingManifestVersion, got {other:?}"),
}
}
#[test]
fn a_versionless_pypi_target_fails_closed_even_beside_a_versioned_rust_target() {
let mut c = rust_contract();
c.ecosystems = vec![Ecosystem::Rust, Ecosystem::Python];
c.targets = vec![
target(
Ecosystem::Rust,
"acme",
Registry::CratesIo,
Adapter::CargoPublish,
),
target(
Ecosystem::Python,
"acme-py",
Registry::Pypi,
Adapter::GhActionPypiPublish,
),
];
let mut f = rust_facts(); f.packages.push(package(
Ecosystem::Python,
"pyproject.toml",
"acme-py",
None,
));
let err = resolve_release_version(&c, &f)
.expect_err("the versionless python target must fail closed");
match err {
VersionResolveError::MissingManifestVersion { targets } => {
let pkgs: Vec<&str> = targets.iter().map(|t| t.package.as_str()).collect();
assert_eq!(
pkgs,
vec!["acme-py"],
"only the unreadable target is reported"
);
}
other => panic!("expected MissingManifestVersion, got {other:?}"),
}
}
#[test]
fn a_binary_distribution_target_is_skipped_not_failed() {
let mut c = rust_contract();
c.targets = vec![
target(
Ecosystem::Rust,
"acme",
Registry::CratesIo,
Adapter::CargoPublish,
),
target(
Ecosystem::Binary,
"acme",
Registry::GhReleases,
Adapter::CargoDist,
),
];
let v = resolve_release_version(&c, &rust_facts())
.expect("the binary target is skipped; the crate version resolves");
assert_eq!(v, "0.1.0");
}
#[test]
fn a_distribution_only_rust_repo_resolves_from_the_crate_manifest() {
let mut c = rust_contract();
c.targets = vec![
target(
Ecosystem::Rust,
"acme",
Registry::Homebrew,
Adapter::HomebrewTap,
),
target(
Ecosystem::Rust,
"acme",
Registry::GhReleases,
Adapter::CargoDist,
),
];
let v = resolve_release_version(&c, &rust_facts())
.expect("a distribution-only rust repo resolves its version from Cargo.toml");
assert_eq!(v, "0.1.0");
}
#[test]
fn an_all_distribution_ecosystem_repo_has_no_derivable_version() {
let mut c = rust_contract();
c.ecosystems = vec![Ecosystem::Binary, Ecosystem::Go];
c.targets = vec![
target(
Ecosystem::Binary,
"acme",
Registry::GhReleases,
Adapter::CargoDist,
),
target(
Ecosystem::Go,
"acme",
Registry::ProxyGolangOrg,
Adapter::Goreleaser,
),
];
assert!(matches!(
resolve_release_version(&c, &rust_facts()),
Err(VersionResolveError::Undeterminable)
));
}
fn member(name: &str, version: &str, deps: &[&str]) -> WorkspaceMember {
WorkspaceMember {
package: name.to_string(),
version: Some(version.to_string()),
workspace_deps: deps.iter().map(|d| (*d).to_string()).collect(),
dep_reqs: deps
.iter()
.map(|d| ((*d).to_string(), format!("={version}")))
.collect(),
}
}
fn lib_bin_workspace_facts(lib: &str, bin: &str) -> Facts {
let mut f = rust_facts();
f.rust_workspace = Some(RustWorkspace {
members: vec![member(lib, "0.1.6", &[]), member(bin, "0.1.6", &[lib])],
});
f
}
fn target_packages(plan: &ReleasePlan) -> Vec<Option<&str>> {
plan.targets.iter().map(|t| t.package.as_deref()).collect()
}
#[test]
fn two_crate_workspace_declaring_only_the_bin_derives_both_members_lib_first() {
let mut c = rust_contract();
c.targets = vec![target(
Ecosystem::Rust,
"orchestratectl",
Registry::CratesIo,
Adapter::CargoPublish,
)];
let f = lib_bin_workspace_facts("octl-core", "orchestratectl");
let plan = build(&c, &f, HEAD, "0.1.6");
assert_eq!(
target_packages(&plan),
vec![Some("octl-core"), Some("orchestratectl")],
"the lib is derived as its own target and ordered before the bin"
);
for t in &plan.targets {
assert_eq!(t.ecosystem, Ecosystem::Rust);
assert_eq!(t.registry, Registry::CratesIo);
assert_eq!(t.adapter, Adapter::CargoPublish);
}
}
#[test]
fn a_contract_declaring_only_the_bin_yields_the_same_target_set_as_declaring_both() {
let f = lib_bin_workspace_facts("octl-core", "orchestratectl");
let mut only_bin = rust_contract();
only_bin.targets = vec![target(
Ecosystem::Rust,
"orchestratectl",
Registry::CratesIo,
Adapter::CargoPublish,
)];
let mut both = rust_contract();
both.targets = vec![
target(
Ecosystem::Rust,
"octl-core",
Registry::CratesIo,
Adapter::CargoPublish,
),
target(
Ecosystem::Rust,
"orchestratectl",
Registry::CratesIo,
Adapter::CargoPublish,
),
];
let derived = build(&only_bin, &f, HEAD, "0.1.6");
let declared = build(&both, &f, HEAD, "0.1.6");
assert_eq!(
target_packages(&derived),
target_packages(&declared),
"derived and fully-declared target sets match"
);
assert_eq!(
derived.targets, declared.targets,
"the derivation is a strict superset — the executed target set is identical"
);
}
#[test]
fn ossctl_own_plan_is_unchanged_by_the_derivation() {
let mut c = rust_contract();
c.targets = vec![
target(
Ecosystem::Rust,
"ossctl-core",
Registry::CratesIo,
Adapter::CargoPublish,
),
target(
Ecosystem::Rust,
"ossctl",
Registry::CratesIo,
Adapter::CargoPublish,
),
];
let mut f = rust_facts();
f.rust_workspace = Some(RustWorkspace {
members: vec![
member("ossctl-core", "0.4.0", &[]),
member("ossctl", "0.4.0", &["ossctl-core"]),
],
});
let mut f_no_graph = f.clone();
f_no_graph.rust_workspace = None;
let with_graph = build(&c, &f, HEAD, "0.4.0");
let without_graph = build(&c, &f_no_graph, HEAD, "0.4.0");
assert_eq!(with_graph.plan_id, without_graph.plan_id);
assert_eq!(
target_packages(&with_graph),
vec![Some("ossctl-core"), Some("ossctl")]
);
}
#[test]
fn derivation_reorders_a_bin_first_declaration_into_dependency_order() {
let mut c = rust_contract();
c.targets = vec![
target(
Ecosystem::Rust,
"orchestratectl",
Registry::CratesIo,
Adapter::CargoPublish,
),
target(
Ecosystem::Rust,
"octl-core",
Registry::CratesIo,
Adapter::CargoPublish,
),
];
let f = lib_bin_workspace_facts("octl-core", "orchestratectl");
let plan = build(&c, &f, HEAD, "0.1.6");
assert_eq!(
target_packages(&plan),
vec![Some("octl-core"), Some("orchestratectl")]
);
}
#[test]
fn derived_members_are_spliced_before_dist_and_homebrew_targets() {
let mut c = rust_contract();
c.targets = vec![
target(
Ecosystem::Rust,
"orchestratectl",
Registry::CratesIo,
Adapter::CargoPublish,
),
target(
Ecosystem::Rust,
"orchestratectl",
Registry::GhReleases,
Adapter::CargoDist,
),
target(
Ecosystem::Rust,
"orchestratectl",
Registry::Homebrew,
Adapter::HomebrewTap,
),
];
let f = lib_bin_workspace_facts("octl-core", "orchestratectl");
let plan = build(&c, &f, HEAD, "0.1.6");
let shape: Vec<(&str, Registry)> = plan
.targets
.iter()
.map(|t| (t.package.as_deref().unwrap(), t.registry))
.collect();
assert_eq!(
shape,
vec![
("octl-core", Registry::CratesIo),
("orchestratectl", Registry::CratesIo),
("orchestratectl", Registry::GhReleases),
("orchestratectl", Registry::Homebrew),
]
);
}
#[test]
fn derivation_is_a_superset_keeping_a_declared_package_absent_from_the_graph() {
let mut c = rust_contract();
c.targets = vec![target(
Ecosystem::Rust,
"extra-crate",
Registry::CratesIo,
Adapter::CargoPublish,
)];
let f = lib_bin_workspace_facts("octl-core", "orchestratectl");
let plan = build(&c, &f, HEAD, "0.1.6");
assert_eq!(
target_packages(&plan),
vec![Some("extra-crate")],
"a declared crate absent from the graph is planned alone — no unrelated members"
);
}
#[test]
fn derivation_publishes_the_closure_not_every_member() {
let mut c = rust_contract();
c.targets = vec![target(
Ecosystem::Rust,
"orchestratectl",
Registry::CratesIo,
Adapter::CargoPublish,
)];
let mut f = rust_facts();
f.rust_workspace = Some(RustWorkspace {
members: vec![
member("octl-core", "0.1.6", &[]),
member("orchestratectl", "0.1.6", &["octl-core"]),
member("experimental", "0.1.6", &[]), ],
});
let plan = build(&c, &f, HEAD, "0.1.6");
assert_eq!(
target_packages(&plan),
vec![Some("octl-core"), Some("orchestratectl")],
"only the declared crate and its dependency closure are published"
);
assert!(
!target_packages(&plan).contains(&Some("experimental")),
"an unrelated undeclared member must never be pulled into an irreversible publish"
);
}
#[test]
fn derivation_pulls_a_transitive_dependency_closure() {
let mut c = rust_contract();
c.targets = vec![target(
Ecosystem::Rust,
"app",
Registry::CratesIo,
Adapter::CargoPublish,
)];
let mut f = rust_facts();
f.rust_workspace = Some(RustWorkspace {
members: vec![
member("app", "1.0.0", &["mid"]),
member("mid", "1.0.0", &["core"]),
member("core", "1.0.0", &[]),
member("unrelated", "1.0.0", &[]),
],
});
let plan = build(&c, &f, HEAD, "1.0.0");
assert_eq!(
target_packages(&plan),
vec![Some("core"), Some("mid"), Some("app")]
);
}
#[test]
fn an_ambiguous_null_rust_target_is_never_expanded_to_publish_everything() {
let mut c = rust_contract();
c.targets = vec![Target {
ecosystem: Ecosystem::Rust,
package: None,
registry: Registry::CratesIo,
adapter: Adapter::CargoPublish,
}];
let mut f = rust_facts();
f.packages = vec![
package(Ecosystem::Rust, "crates/a/Cargo.toml", "a", Some("1.0.0")),
package(Ecosystem::Rust, "crates/b/Cargo.toml", "b", Some("1.0.0")),
];
f.rust_workspace = Some(RustWorkspace {
members: vec![member("a", "1.0.0", &[]), member("b", "1.0.0", &["a"])],
});
let plan = build(&c, &f, HEAD, "1.0.0");
assert_eq!(
target_packages(&plan),
vec![None],
"an unresolved rust target is preserved, never expanded into publish-everything"
);
}
#[test]
fn a_single_crate_repo_is_unaffected_by_the_derivation() {
let c = rust_contract();
let plan = build(&c, &rust_facts(), HEAD, "0.1.0");
assert_eq!(target_packages(&plan), vec![Some("acme")]);
}
#[test]
fn homebrew_tap_carries_into_a_multi_crate_plan() {
let mut c = rust_contract();
c.targets = vec![
target(
Ecosystem::Rust,
"orchestratectl",
Registry::CratesIo,
Adapter::CargoPublish,
),
target(
Ecosystem::Rust,
"orchestratectl",
Registry::Homebrew,
Adapter::HomebrewTap,
),
];
c.distributions = vec![Distribution {
package: None,
adapter: DistributionAdapter::CargoDist,
gh_releases: true,
installers: vec![Installer::Homebrew],
homebrew_tap: Some("jarimustonen/orchestratectl".to_string()),
platforms: vec!["aarch64-apple-darwin".to_string()],
extra_fields: serde_json::Map::new(),
}];
let f = lib_bin_workspace_facts("octl-core", "orchestratectl");
let plan = build(&c, &f, HEAD, "0.1.6");
assert_eq!(
plan.homebrew_tap.as_deref(),
Some("jarimustonen/orchestratectl")
);
assert_eq!(
plan.targets
.iter()
.filter(|t| t.registry == Registry::CratesIo)
.count(),
2
);
}
#[test]
fn orchestratectl_plan_id_differs_once_the_lib_target_is_derived() {
let mut c = rust_contract();
c.targets = vec![target(
Ecosystem::Rust,
"orchestratectl",
Registry::CratesIo,
Adapter::CargoPublish,
)];
let mut f_no_graph = rust_facts();
f_no_graph.rust_workspace = None;
let f_graph = lib_bin_workspace_facts("octl-core", "orchestratectl");
let before = build(&c, &f_no_graph, HEAD, "0.1.6");
let after = build(&c, &f_graph, HEAD, "0.1.6");
assert_eq!(target_packages(&before), vec![Some("orchestratectl")]);
assert_ne!(before.plan_id, after.plan_id);
}
#[test]
fn topo_order_puts_dependencies_before_dependents() {
let members = vec![
member("app", "1.0.0", &["mid"]),
member("mid", "1.0.0", &["core"]),
member("core", "1.0.0", &[]),
];
assert_eq!(topo_order_members(&members), vec!["core", "mid", "app"]);
}
#[test]
fn topo_order_is_deterministic_for_independent_members() {
let members = vec![member("zeta", "1.0.0", &[]), member("alpha", "1.0.0", &[])];
assert_eq!(topo_order_members(&members), vec!["zeta", "alpha"]);
}
#[test]
fn topo_order_appends_a_cycle_deterministically_without_looping() {
let members = vec![member("a", "1.0.0", &["b"]), member("b", "1.0.0", &["a"])];
assert_eq!(topo_order_members(&members), vec!["a", "b"]);
}
fn bump_plan(level: BumpLevel) -> ReleasePlan {
let mut c = rust_contract();
c.targets = vec![target(
Ecosystem::Rust,
"orchestratectl",
Registry::CratesIo,
Adapter::CargoPublish,
)];
let f = lib_bin_workspace_facts("octl-core", "orchestratectl");
build_with_bump(&c, &f, HEAD, "0.1.6", level).expect("0.1.6 is strict semver")
}
#[test]
fn the_no_bump_path_is_unchanged_and_opt_in() {
let (c, f) = (rust_contract(), rust_facts());
let plan = build(&c, &f, HEAD, "0.1.0");
assert!(plan.bump.is_none(), "no --bump ⇒ no bump plan");
assert_eq!(
plan.phases,
PlanPhase::SEQUENCE.to_vec(),
"no leading bump phase"
);
assert!(!plan.phases.contains(&PlanPhase::Bump));
}
#[test]
fn a_bump_plan_prepends_the_bump_phase_and_carries_the_computed_version() {
let plan = bump_plan(BumpLevel::Minor);
assert_eq!(
plan.phases[0],
PlanPhase::Bump,
"bump runs before every barrier"
);
assert_eq!(
&plan.phases[1..],
PlanPhase::SEQUENCE.as_slice(),
"the rest of the pipeline is unchanged after the bump phase"
);
assert_eq!(plan.version, "0.2.0");
let bump = plan.bump.as_ref().expect("a --bump plan carries a bump");
assert_eq!(bump.level, BumpLevel::Minor);
assert_eq!(bump.from_version, "0.1.6");
assert_eq!(bump.to_version, "0.2.0");
}
#[test]
fn the_bump_derives_the_intra_workspace_pin_rewrite() {
let plan = bump_plan(BumpLevel::Minor);
let bump = plan.bump.unwrap();
assert_eq!(
bump.pin_rewrites.len(),
1,
"one lib←bin edge ⇒ one pin rewrite"
);
let r = &bump.pin_rewrites[0];
assert_eq!(r.in_package, "orchestratectl");
assert_eq!(r.dependency, "octl-core");
assert_eq!(r.from, "=0.1.6");
assert_eq!(r.to, "=0.2.0");
}
#[test]
fn build_with_bump_computes_the_version_from_the_level_not_a_literal() {
let mut c = rust_contract();
c.targets = vec![target(
Ecosystem::Rust,
"orchestratectl",
Registry::CratesIo,
Adapter::CargoPublish,
)];
let f = lib_bin_workspace_facts("octl-core", "orchestratectl");
let plan = build_with_bump(&c, &f, HEAD, "0.1.6", BumpLevel::Major).unwrap();
assert_eq!(plan.version, "1.0.0");
assert_eq!(plan.bump.unwrap().to_version, "1.0.0");
}
#[test]
fn build_with_bump_fails_closed_on_a_non_semver_from_version() {
let c = rust_contract();
let f = rust_facts();
let err = build_with_bump(&c, &f, HEAD, "not-semver", BumpLevel::Patch).unwrap_err();
assert_eq!(err.version, "not-semver");
}
#[test]
fn a_single_crate_workspace_has_no_pin_rewrites() {
let c = rust_contract();
let f = rust_facts(); let plan = build_with_bump(&c, &f, HEAD, "0.1.0", BumpLevel::Patch).unwrap();
assert!(plan.bump.unwrap().pin_rewrites.is_empty());
}
#[test]
fn the_bump_finalizes_a_curated_changelog_but_not_an_automated_one() {
let curated = bump_plan(BumpLevel::Patch);
assert!(curated.bump.unwrap().changelog_finalize);
let mut c = rust_contract();
c.changelog.mode = ChangelogMode::Automated;
c.targets = vec![target(
Ecosystem::Rust,
"orchestratectl",
Registry::CratesIo,
Adapter::CargoPublish,
)];
let f = lib_bin_workspace_facts("octl-core", "orchestratectl");
let auto = build_with_bump(&c, &f, HEAD, "0.1.6", BumpLevel::Patch).unwrap();
assert!(!auto.bump.unwrap().changelog_finalize);
}
#[test]
fn a_declared_bump_hook_rides_on_the_bump_plan() {
let mut c = rust_contract();
c.release.bump_hook = Some("cargo insta test --accept".to_string());
c.targets = vec![target(
Ecosystem::Rust,
"orchestratectl",
Registry::CratesIo,
Adapter::CargoPublish,
)];
let f = lib_bin_workspace_facts("octl-core", "orchestratectl");
let plan = build_with_bump(&c, &f, HEAD, "0.1.6", BumpLevel::Minor).unwrap();
assert_eq!(
plan.bump.unwrap().bump_hook.as_deref(),
Some("cargo insta test --accept")
);
}
#[test]
fn an_absent_bump_hook_is_none_on_the_bump_plan() {
let plan = bump_plan(BumpLevel::Patch);
assert!(plan.bump.unwrap().bump_hook.is_none());
}
#[test]
fn a_bump_plan_has_a_different_id_than_the_no_bump_plan() {
let no_bump = {
let mut c = rust_contract();
c.targets = vec![target(
Ecosystem::Rust,
"orchestratectl",
Registry::CratesIo,
Adapter::CargoPublish,
)];
let f = lib_bin_workspace_facts("octl-core", "orchestratectl");
build(&c, &f, HEAD, "0.1.6")
};
let bumped = bump_plan(BumpLevel::Patch);
assert_ne!(no_bump.plan_id, bumped.plan_id);
}
#[test]
fn different_bump_levels_seal_different_ids() {
let minor = bump_plan(BumpLevel::Minor);
let patch = bump_plan(BumpLevel::Patch);
assert_ne!(minor.plan_id, patch.plan_id);
}
#[test]
fn a_bump_plan_is_stable_for_identical_inputs() {
let a = bump_plan(BumpLevel::Minor);
let b = bump_plan(BumpLevel::Minor);
assert_eq!(a.plan_id, b.plan_id, "determinism: same inputs ⇒ same id");
}
#[test]
fn a_declared_bump_hook_changes_the_bump_plan_id() {
let without = bump_plan(BumpLevel::Minor);
let with = {
let mut c = rust_contract();
c.release.bump_hook = Some("cargo insta test --accept".to_string());
c.targets = vec![target(
Ecosystem::Rust,
"orchestratectl",
Registry::CratesIo,
Adapter::CargoPublish,
)];
let f = lib_bin_workspace_facts("octl-core", "orchestratectl");
build_with_bump(&c, &f, HEAD, "0.1.6", BumpLevel::Minor).unwrap()
};
assert_ne!(without.plan_id, with.plan_id);
}