facett-releaseview/tests/derive.rs
//! **RED-when-broken** unit tests for the convergence-status kernel — the pure
//! [`derive_status`] fn across all four version-derived states, plus the fifth
//! ([`ConvergeStatus::Publishing`]) reached through the realtime [`apply`] seam, and
//! the whole-model roll-ups. If a status rule regresses, exactly one of these goes
//! red and names the case.
//!
//! [`apply`]: facett_releaseview::model::ReleaseConvergence::apply
use facett_releaseview::model::{
ConvergeStatus, CrateConvergence, DepEdge, ReleaseConvergence, ReleaseEvent, derive_status,
};
#[test]
fn all_five_convergence_states_are_reachable() {
// 1. Converged — the three graphs agree.
assert_eq!(derive_status("0.5.4", "0.5.4", "0.5.4"), ConvergeStatus::Converged);
// 2. NeedsRelease — crates.io behind local (must publish).
assert_eq!(derive_status("0.5.4", "0.5.4", "0.5.3"), ConvergeStatus::NeedsRelease);
// ...even when sparring also leads (still a release, red dominates).
assert_eq!(derive_status("0.6.0", "0.6.0", "0.5.9"), ConvergeStatus::NeedsRelease);
// 3. LockDesync — sparring (resolved) disagrees with cratesio (published), but
// crates.io is NOT behind local (so not a plain needs-release).
assert_eq!(derive_status("0.5.4", "0.5.5", "0.5.4"), ConvergeStatus::LockDesync);
// 4. BumpDrift — sparring differs from local, while sparring == cratesio.
assert_eq!(derive_status("0.5.3", "0.5.4", "0.5.4"), ConvergeStatus::BumpDrift);
// 5. Publishing — NOT version-derived; it is the transient live-event state.
let mut m = ReleaseConvergence::new("t").with_node(CrateConvergence::new("a", "0.5.4", "0.5.4", "0.5.3"));
assert_eq!(m.nodes[0].status, ConvergeStatus::NeedsRelease);
assert!(m.apply(ReleaseEvent::PublishStart { krate: "a".into() }));
assert_eq!(m.nodes[0].status, ConvergeStatus::Publishing, "PublishStart flips the crate to Publishing");
}
#[test]
fn derive_status_never_returns_publishing() {
// Publishing is a live-event state, never a function of the versions alone.
for triple in [
("0.5.4", "0.5.4", "0.5.4"),
("0.5.4", "0.5.4", "0.5.3"),
("0.5.4", "0.5.5", "0.5.4"),
("0.5.3", "0.5.4", "0.5.4"),
("1.0.0", "0.9.0", "0.8.0"),
] {
assert_ne!(derive_status(triple.0, triple.1, triple.2), ConvergeStatus::Publishing);
}
}
#[test]
fn published_event_converges_the_node() {
let mut m = ReleaseConvergence::new("release")
.with_node(CrateConvergence::new("nornir", "0.5.4", "0.5.4", "0.5.3"));
assert_eq!(m.nodes[0].status, ConvergeStatus::NeedsRelease);
assert!(!m.is_converged());
// Live cascade: start publishing, then it lands at the local/sparring version.
m.apply(ReleaseEvent::PublishStart { krate: "nornir".into() });
assert_eq!(m.nodes[0].status, ConvergeStatus::Publishing);
m.apply(ReleaseEvent::Published { krate: "nornir".into(), version: "0.5.4".into() });
assert_eq!(m.nodes[0].status, ConvergeStatus::Converged, "a publish that matches local+sparring converges");
assert!(m.is_converged());
}
#[test]
fn sparring_and_local_events_reconcile_drift() {
// BumpDrift: sparring hoisted 0.5.4, local still 0.5.3, cratesio 0.5.4.
let mut m = ReleaseConvergence::new("r")
.with_node(CrateConvergence::new("skade", "0.5.3", "0.5.4", "0.5.4"));
assert_eq!(m.nodes[0].status, ConvergeStatus::BumpDrift);
// Adopting the bump into the working tree converges it.
m.apply(ReleaseEvent::LocalBumped { krate: "skade".into(), version: "0.5.4".into() });
assert_eq!(m.nodes[0].status, ConvergeStatus::Converged);
}
#[test]
fn status_counts_and_converged_fraction_roll_up() {
let m = ReleaseConvergence::new("nordisk")
.with_node(CrateConvergence::new("a", "0.1.0", "0.1.0", "0.1.0")) // converged
.with_node(CrateConvergence::new("b", "0.2.0", "0.2.0", "0.1.9")) // needs-release
.with_node(CrateConvergence::new("c", "0.3.0", "0.3.1", "0.3.0")) // lock-desync
.with_node(CrateConvergence::new("d", "0.4.0", "0.4.1", "0.4.1")) // bump-drift
.with_edge(DepEdge::new(1, 0));
// (converged, needs_release, lock_desync, bump_drift, publishing)
assert_eq!(m.status_counts(), (1, 1, 1, 1, 0));
assert!(!m.is_converged());
assert!((m.converged_fraction() - 0.25).abs() < 1e-6, "1 of 4 converged");
}
#[test]
fn apply_to_unknown_crate_is_a_noop() {
let mut m = ReleaseConvergence::new("r").with_node(CrateConvergence::new("a", "1.0.0", "1.0.0", "1.0.0"));
assert!(!m.apply(ReleaseEvent::PublishStart { krate: "ghost".into() }), "unknown crate → no change");
assert_eq!(m.nodes[0].status, ConvergeStatus::Converged);
}