use crate::context::Context;
use crate::{PublishEvidence, PublisherGroup};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PreflightCheck {
Pass,
Warning(String),
Blocker(String),
}
pub trait Publisher: Send + Sync {
fn name(&self) -> &str;
fn run(&self, ctx: &mut Context) -> anyhow::Result<PublishEvidence>;
fn group(&self) -> PublisherGroup;
fn required(&self) -> bool;
fn rollback(&self, _ctx: &mut Context, _evidence: &PublishEvidence) -> anyhow::Result<()> {
Ok(())
}
fn preflight(&self, _ctx: &Context) -> anyhow::Result<PreflightCheck> {
Ok(PreflightCheck::Pass)
}
fn rollback_scope_needed(&self) -> Option<&'static str> {
None
}
fn requirements(&self, _ctx: &Context) -> Vec<crate::env_preflight::EnvRequirement> {
Vec::new()
}
fn skips_on_nightly(&self) -> bool;
fn programmatic_rollback_on_failure(&self, _evidence: &PublishEvidence) -> bool {
false
}
fn retain_on_rollback(&self) -> bool {
false
}
}
pub fn rollback_empty_warning_msg(publisher: &str, target_label: &str) -> String {
format!(
"no {} recorded in {} evidence — verify {} state manually",
target_label, publisher, publisher
)
}
#[cfg(test)]
mod tests {
use super::*;
struct MinimalPublisher;
impl Publisher for MinimalPublisher {
fn name(&self) -> &str {
"minimal"
}
fn run(&self, _ctx: &mut Context) -> anyhow::Result<PublishEvidence> {
Ok(PublishEvidence::new("minimal"))
}
fn group(&self) -> PublisherGroup {
PublisherGroup::Manager
}
fn required(&self) -> bool {
false
}
fn skips_on_nightly(&self) -> bool {
false
}
}
#[test]
fn rollback_default_is_noop_ok() {
let p = MinimalPublisher;
let mut ctx = Context::test_fixture();
let evidence = PublishEvidence::new("minimal");
assert!(p.rollback(&mut ctx, &evidence).is_ok());
}
#[test]
fn preflight_default_is_pass() {
let p = MinimalPublisher;
let ctx = Context::test_fixture();
assert!(matches!(p.preflight(&ctx).unwrap(), PreflightCheck::Pass));
}
#[test]
fn rollback_scope_needed_default_is_none() {
let p = MinimalPublisher;
assert!(p.rollback_scope_needed().is_none());
}
#[test]
fn pending_outcome_round_trips_through_context() {
let mut ctx = Context::test_fixture();
assert!(ctx.take_pending_outcome().is_none());
ctx.record_publisher_outcome(crate::PublisherOutcome::PendingModeration);
assert!(matches!(
ctx.take_pending_outcome(),
Some(crate::PublisherOutcome::PendingModeration)
));
assert!(
ctx.take_pending_outcome().is_none(),
"slot must be empty after take"
);
ctx.record_publisher_outcome(crate::PublisherOutcome::PendingModeration);
ctx.record_publisher_outcome(crate::PublisherOutcome::PendingValidation);
assert!(matches!(
ctx.take_pending_outcome(),
Some(crate::PublisherOutcome::PendingValidation)
));
}
#[test]
fn rollback_empty_warning_msg_interpolates_all_three_slots() {
let msg = rollback_empty_warning_msg("homebrew", "tap commit");
assert_eq!(
msg,
"no tap commit recorded in homebrew evidence — verify homebrew state manually"
);
}
#[test]
fn rollback_empty_warning_msg_distinct_per_publisher() {
let a = rollback_empty_warning_msg("cargo", "crate");
let b = rollback_empty_warning_msg("aur", "commit");
assert_ne!(a, b);
assert!(a.contains("cargo") && a.contains("crate"));
assert!(b.contains("aur") && b.contains("commit"));
}
#[test]
fn programmatic_rollback_on_failure_defaults_false() {
let p = MinimalPublisher;
let evidence = PublishEvidence::new("minimal");
assert!(!p.programmatic_rollback_on_failure(&evidence));
}
#[test]
fn retain_on_rollback_defaults_false() {
assert!(!MinimalPublisher.retain_on_rollback());
}
#[test]
fn requirements_default_is_empty() {
let p = MinimalPublisher;
let ctx = Context::test_fixture();
assert!(p.requirements(&ctx).is_empty());
}
#[test]
fn preflight_check_variants_compare_by_value() {
assert_eq!(PreflightCheck::Pass, PreflightCheck::Pass);
assert_eq!(
PreflightCheck::Warning("dup".into()),
PreflightCheck::Warning("dup".into())
);
assert_ne!(
PreflightCheck::Blocker("a".into()),
PreflightCheck::Blocker("b".into())
);
assert_ne!(
PreflightCheck::Warning("x".into()),
PreflightCheck::Blocker("x".into())
);
}
#[test]
fn minimal_publisher_carries_its_declared_identity() {
let p = MinimalPublisher;
assert_eq!(p.name(), "minimal");
assert_eq!(p.group(), PublisherGroup::Manager);
assert!(!p.required());
assert!(!p.skips_on_nightly());
}
struct OverridingPublisher;
impl Publisher for OverridingPublisher {
fn name(&self) -> &str {
"overriding"
}
fn run(&self, _ctx: &mut Context) -> anyhow::Result<PublishEvidence> {
Ok(PublishEvidence::new("overriding"))
}
fn group(&self) -> PublisherGroup {
PublisherGroup::Assets
}
fn required(&self) -> bool {
true
}
fn skips_on_nightly(&self) -> bool {
true
}
fn preflight(&self, _ctx: &Context) -> anyhow::Result<PreflightCheck> {
Ok(PreflightCheck::Blocker("fork missing".into()))
}
fn rollback_scope_needed(&self) -> Option<&'static str> {
Some("delete_repo")
}
fn programmatic_rollback_on_failure(&self, _evidence: &PublishEvidence) -> bool {
true
}
fn retain_on_rollback(&self) -> bool {
true
}
}
#[test]
fn override_publisher_preflight_returns_blocker() {
let p = OverridingPublisher;
let ctx = Context::test_fixture();
assert_eq!(
p.preflight(&ctx).unwrap(),
PreflightCheck::Blocker("fork missing".into())
);
}
#[test]
fn override_publisher_exposes_rollback_scope_and_flags() {
let p = OverridingPublisher;
let evidence = PublishEvidence::new("overriding");
assert_eq!(p.rollback_scope_needed(), Some("delete_repo"));
assert!(p.programmatic_rollback_on_failure(&evidence));
assert!(p.retain_on_rollback());
assert!(p.required());
assert!(p.skips_on_nightly());
assert_eq!(p.group(), PublisherGroup::Assets);
}
#[test]
fn preflight_check_clone_preserves_payload() {
let warn = PreflightCheck::Warning("dup upload".into());
assert_eq!(warn.clone(), warn);
let blocker = PreflightCheck::Blocker("no tap".into());
let cloned = blocker.clone();
assert_eq!(cloned, PreflightCheck::Blocker("no tap".into()));
assert_ne!(warn, blocker);
}
}