use effectfence::fence::{
Admission, EffectFence, EffectRequest, FenceConfig, VectorClock, prepare_effect_fence,
};
use std::time::Duration;
fn req(intent: &str, agent: &str) -> EffectRequest {
EffectRequest {
intent: intent.to_string(),
parent: None,
domain: "order:9".to_string(),
tool: "charge".to_string(),
args: serde_json::json!({"amount": 4900}),
read_set: vec![],
agent: agent.to_string(),
known_clock: VectorClock::new(),
}
}
fn short_lease() -> EffectFence {
EffectFence::with_config(FenceConfig {
lease_ttl: Duration::from_millis(50),
..FenceConfig::default()
})
}
#[test]
fn a_slow_holder_that_cannot_signal_life_loses_its_claim() {
let fence = short_lease();
let _still_running =
prepare_effect_fence(&fence, req("charge:slow-1", "agent-a")).expect("first admission");
std::thread::sleep(Duration::from_millis(120));
let second = prepare_effect_fence(&fence, req("charge:slow-1", "agent-b"));
assert!(
matches!(second, Ok(Admission::Fresh(_))),
"documents the hazard: with no way to signal life, the slow holder's \
intent is admitted a second time"
);
}
#[test]
fn heartbeat_keeps_a_slow_holder_s_claim() {
let fence = short_lease();
let _running =
prepare_effect_fence(&fence, req("charge:slow-2", "agent-a")).expect("first admission");
for _ in 0..3 {
std::thread::sleep(Duration::from_millis(30));
assert!(
fence.heartbeat("charge:slow-2"),
"an in-flight intent must accept a heartbeat"
);
}
let second = prepare_effect_fence(&fence, req("charge:slow-2", "agent-b"));
assert!(
matches!(
second,
Err(effectfence::fence::FenceError::IntentInFlight { .. })
),
"a heartbeating holder keeps its claim; got {second:?}"
);
}
#[test]
fn heartbeat_refuses_anything_that_is_not_in_flight() {
let fence = short_lease();
assert!(!fence.heartbeat("charge:never-existed"), "unknown intent");
let prepared = match prepare_effect_fence(&fence, req("charge:slow-3", "agent-a")).unwrap() {
Admission::Fresh(p) => p,
other => panic!("expected a fresh admission, got {other:?}"),
};
effectfence::fence::commit_effect_cert(&fence, prepared, serde_json::json!({"ok": true}))
.expect("commit");
assert!(
!fence.heartbeat("charge:slow-3"),
"a committed intent is finished, not in flight — extending it would \
reopen a settled outcome"
);
let _lost = prepare_effect_fence(&fence, req("charge:slow-4", "agent-a")).unwrap();
std::thread::sleep(Duration::from_millis(120));
let _taken = prepare_effect_fence(&fence, req("charge:slow-4", "agent-b")).unwrap();
assert!(
fence.heartbeat("charge:slow-4"),
"the CURRENT holder may beat (the fence tracks the lease, not who holds it)"
);
}