use super::test_support::*;
use super::*;
use std::cell::RefCell;
#[test]
fn ok_first_attempt_returns_outcome() {
let p = ClosureProtocol::new(always_ok(), 3);
let r = Negotiation::new(p, FailurePolicy::Required).run().unwrap();
assert_eq!(r, ok_default("pushed"));
}
#[test]
fn conflict_then_ok_retries_and_succeeds() {
let calls = RefCell::new(0);
let propose: PropFn = Box::new(|| {
let mut c = calls.borrow_mut();
*c += 1;
if *c == 1 { Ok(AttemptClass::Conflict) } else { Ok(AttemptClass::Ok) }
});
let p = ClosureProtocol::new(propose, 5);
let r = Negotiation::new(p, FailurePolicy::Required).run().unwrap();
assert_eq!(r, ok_default("pushed"));
assert_eq!(*calls.borrow(), 2);
}
#[test]
fn post_merge_short_circuits_to_outcome() {
let p = ClosureProtocol::new(Box::new(|| Ok(AttemptClass::Conflict)), 5)
.with_post_merge(Box::new(|| Ok(Some("lost"))));
let r = Negotiation::new(p, FailurePolicy::Required).run().unwrap();
assert_eq!(r, ok_default("lost"));
}
#[test]
fn ok_carries_protocol_chosen_commit_policy() {
let p = ClosureProtocol::new(always_ok(), 1).with_policy(CommitPolicy::Suppress);
let r = Negotiation::new(p, FailurePolicy::Required).run().unwrap();
assert_eq!(
r,
NegotiationResult::Ok(Accepted {
outcome: "pushed",
commit_policy: CommitPolicy::Suppress,
})
);
}
#[test]
fn post_merge_short_circuit_carries_commit_policy() {
let policy = CommitPolicy::Batch { tag: "audit".into() };
let p = ClosureProtocol::new(Box::new(|| Ok(AttemptClass::Conflict)), 5)
.with_post_merge(Box::new(|| Ok(Some("lost"))))
.with_policy(policy.clone());
let r = Negotiation::new(p, FailurePolicy::Required).run().unwrap();
assert_eq!(
r,
NegotiationResult::Ok(Accepted { outcome: "lost", commit_policy: policy })
);
}
#[test]
fn unreachable_required_errors() {
let p = ClosureProtocol::new(
Box::new(|| Ok(AttemptClass::Unreachable("net down".into()))),
3,
);
let err = Negotiation::new(p, FailurePolicy::Required).run().unwrap_err();
assert!(format!("{err}").contains("net down"));
}
#[test]
fn unreachable_best_effort_skips() {
let p = ClosureProtocol::new(
Box::new(|| Ok(AttemptClass::Unreachable("offline".into()))),
3,
);
let r = Negotiation::new(p, FailurePolicy::BestEffort).run().unwrap();
assert!(matches!(r, NegotiationResult::Skipped(s) if s.contains("offline")));
}
#[test]
fn unreachable_gating_stages() {
let p = ClosureProtocol::new(
Box::new(|| Ok(AttemptClass::Unreachable("offline".into()))),
3,
);
let r = Negotiation::new(p, FailurePolicy::Gating).run().unwrap();
assert!(matches!(r, NegotiationResult::Staged(s) if s.contains("offline")));
}
#[test]
fn other_failure_required_errors() {
let p = ClosureProtocol::new(
Box::new(|| Ok(AttemptClass::Other("weird".into()))),
3,
);
let err = Negotiation::new(p, FailurePolicy::Required).run().unwrap_err();
assert!(format!("{err}").contains("weird"));
}
#[test]
fn exhaustion_required_errors() {
let calls = RefCell::new(0);
let propose: PropFn = Box::new(|| {
*calls.borrow_mut() += 1;
Ok(AttemptClass::Conflict)
});
let p = ClosureProtocol::new(propose, 3);
let err = Negotiation::new(p, FailurePolicy::Required).run().unwrap_err();
assert!(format!("{err}").contains("gave up"));
assert_eq!(*calls.borrow(), 3);
}
#[test]
fn exhaustion_best_effort_skips() {
let p = ClosureProtocol::new(Box::new(|| Ok(AttemptClass::Conflict)), 2);
let r = Negotiation::new(p, FailurePolicy::BestEffort).run().unwrap();
assert!(matches!(r, NegotiationResult::Skipped(s) if s.contains("gave up")));
}
#[test]
fn exhaustion_gating_stages() {
let p = ClosureProtocol::new(Box::new(|| Ok(AttemptClass::Conflict)), 2);
let r = Negotiation::new(p, FailurePolicy::Gating).run().unwrap();
assert!(matches!(r, NegotiationResult::Staged(s) if s.contains("gave up")));
}
#[test]
fn propagates_propose_error() {
let p = ClosureProtocol::new(
Box::new(|| Err(BallError::Other("spawn failed".into()))),
3,
);
let err = Negotiation::new(p, FailurePolicy::Required).run().unwrap_err();
assert!(format!("{err}").contains("spawn"));
}
#[test]
fn propagates_fetch_error() {
let p = ClosureProtocol::new(Box::new(|| Ok(AttemptClass::Conflict)), 3)
.with_fetch(Box::new(|| Err(BallError::Conflict("unresolvable".into()))));
let err = Negotiation::new(p, FailurePolicy::Required).run().unwrap_err();
assert!(format!("{err}").contains("unresolvable"));
}
#[test]
fn propagates_post_merge_error() {
let p = ClosureProtocol::new(Box::new(|| Ok(AttemptClass::Conflict)), 3)
.with_post_merge(Box::new(|| Err(BallError::Other("post-merge boom".into()))));
let err = Negotiation::new(p, FailurePolicy::Required).run().unwrap_err();
assert!(format!("{err}").contains("post-merge"));
}
#[test]
fn run_strict_unwraps_ok() {
let p = ClosureProtocol::new(always_ok(), 3);
let outcome = Negotiation::new(p, FailurePolicy::Required).run_strict().unwrap();
assert_eq!(outcome, "pushed");
}
#[test]
fn run_strict_propagates_skipped_as_err() {
let p = ClosureProtocol::new(
Box::new(|| Ok(AttemptClass::Unreachable("offline".into()))),
2,
);
let err = Negotiation::new(p, FailurePolicy::BestEffort).run_strict().unwrap_err();
assert!(format!("{err}").contains("offline"));
}
#[test]
fn run_strict_propagates_staged_as_err() {
let p = ClosureProtocol::new(Box::new(|| Ok(AttemptClass::Conflict)), 2);
let err = Negotiation::new(p, FailurePolicy::Gating).run_strict().unwrap_err();
assert!(format!("{err}").contains("gave up"));
}
struct DefaultPostMergeProtocol {
conflicts_first: usize,
calls: usize,
budget: usize,
}
impl Protocol for DefaultPostMergeProtocol {
type Outcome = &'static str;
fn propose(&mut self) -> Result<AttemptClass> {
self.calls += 1;
if self.calls <= self.conflicts_first {
Ok(AttemptClass::Conflict)
} else {
Ok(AttemptClass::Ok)
}
}
fn fetch_remote_view(&mut self) -> Result<()> {
Ok(())
}
fn pushed(&mut self) -> Self::Outcome {
"default-pushed"
}
fn retry_budget(&self) -> usize {
self.budget
}
}
#[test]
fn protocol_default_post_merge_keeps_loop_retrying_until_ok() {
let p = DefaultPostMergeProtocol { conflicts_first: 1, calls: 0, budget: 3 };
let r = Negotiation::new(p, FailurePolicy::Required).run().unwrap();
assert_eq!(r, ok_default("default-pushed"));
}