use serde::Serialize;
use crate::vendor_error::VendorErrorClass;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExitClass {
Success,
Failure(i32),
KilledBySignal,
}
pub fn classify_exit(exit_code: Option<i32>) -> ExitClass {
match exit_code {
Some(0) => ExitClass::Success,
Some(code) => ExitClass::Failure(code),
None => ExitClass::KilledBySignal,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergeOutcome {
Merged,
Diverged,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RunEvidence {
pub cancelled: bool,
pub exit: ExitClass,
pub vendor_error: Option<VendorErrorClass>,
pub commit_count: Option<usize>,
pub aftercare_failed: bool,
pub merge: Option<MergeOutcome>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Outcome {
Merged,
Failed,
NeedsReview,
Cancelled,
RateLimited,
Orphaned,
}
impl Outcome {
pub fn as_str(self) -> &'static str {
match self {
Self::Merged => "merged",
Self::Failed => "failed",
Self::NeedsReview => "needs_review",
Self::Cancelled => "cancelled",
Self::RateLimited => "rate_limited",
Self::Orphaned => "orphaned",
}
}
}
pub fn derive_outcome(evidence: &RunEvidence) -> Outcome {
if evidence.cancelled {
return Outcome::Cancelled;
}
if let Some(class) = evidence.vendor_error {
return if class.requires_cooldown() {
Outcome::RateLimited
} else {
Outcome::Failed
};
}
if evidence.merge == Some(MergeOutcome::Merged) {
return Outcome::Merged;
}
if evidence.exit != ExitClass::Success {
return Outcome::Failed;
}
if evidence.aftercare_failed && evidence.commit_count == Some(0) {
return Outcome::Failed;
}
Outcome::NeedsReview
}
#[cfg(test)]
mod tests {
use super::*;
fn evidence() -> RunEvidence {
RunEvidence {
cancelled: false,
exit: ExitClass::Success,
vendor_error: None,
commit_count: Some(0),
aftercare_failed: false,
merge: None,
}
}
#[test]
fn a_successful_exit_without_a_merge_needs_review() {
let outcome = derive_outcome(&evidence());
assert_eq!(outcome, Outcome::NeedsReview);
}
#[test]
fn a_successful_merge_is_the_only_path_to_merged() {
let merged = derive_outcome(&RunEvidence {
commit_count: Some(1),
merge: Some(MergeOutcome::Merged),
..evidence()
});
assert_eq!(merged, Outcome::Merged);
let diverged = derive_outcome(&RunEvidence {
commit_count: Some(1),
merge: Some(MergeOutcome::Diverged),
..evidence()
});
assert_ne!(diverged, Outcome::Merged);
}
#[test]
fn failed_aftercare_with_commits_needs_review() {
let outcome = derive_outcome(&RunEvidence {
commit_count: Some(1),
aftercare_failed: true,
..evidence()
});
assert_eq!(outcome, Outcome::NeedsReview);
}
#[test]
fn failed_aftercare_without_commits_fails() {
let outcome = derive_outcome(&RunEvidence {
aftercare_failed: true,
..evidence()
});
assert_eq!(outcome, Outcome::Failed);
}
#[test]
fn failed_aftercare_with_unknown_commits_needs_review() {
let outcome = derive_outcome(&RunEvidence {
commit_count: None,
aftercare_failed: true,
..evidence()
});
assert_eq!(outcome, Outcome::NeedsReview);
}
#[test]
fn a_crashed_agent_fails_regardless_of_git_history() {
let outcome = derive_outcome(&RunEvidence {
exit: ExitClass::Failure(1),
..evidence()
});
assert_eq!(outcome, Outcome::Failed);
}
#[test]
fn cancellation_wins_over_every_other_reading() {
let outcome = derive_outcome(&RunEvidence {
cancelled: true,
exit: ExitClass::KilledBySignal,
vendor_error: None,
commit_count: Some(5),
aftercare_failed: false,
merge: Some(MergeOutcome::Merged),
});
assert_eq!(outcome, Outcome::Cancelled);
}
#[test]
fn exit_classification_reads_the_wait_status() {
assert_eq!(classify_exit(Some(0)), ExitClass::Success);
assert_eq!(classify_exit(Some(2)), ExitClass::Failure(2));
assert_eq!(classify_exit(None), ExitClass::KilledBySignal);
}
#[test]
fn vendor_rejections_follow_code_owned_policy() {
for class in [
VendorErrorClass::AuthenticationRequired,
VendorErrorClass::InvalidConfiguration,
] {
assert_eq!(
derive_outcome(&RunEvidence {
vendor_error: Some(class),
..evidence()
}),
Outcome::Failed
);
}
for class in [
VendorErrorClass::RateLimited,
VendorErrorClass::UnknownRejection,
] {
assert_eq!(
derive_outcome(&RunEvidence {
vendor_error: Some(class),
..evidence()
}),
Outcome::RateLimited
);
}
}
}