From 489360d0e5d92495af49b495e5c443f0245b688f Mon Sep 17 00:00:00 2001 From: Adrian Duke Date: Fri, 22 May 2026 14:15:03 +0100 Subject: [PATCH] radicle/cob/identity: Add concurrent terminal state test The new identity evaluation logic was updated to ignore `Accept` or `Reject` actions if the revision has already reached a terminal state (`Accepted` or `Rejected`), however this handling was not explicitly covered by the test suite. This test ensures that the state machine correctly short-circuits the late-arriving vote, proving that the new logic succesfully ensures terminal states remain immutable under concurrent network conditions. --- crates/radicle/src/cob/identity.rs | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/crates/radicle/src/cob/identity.rs b/crates/radicle/src/cob/identity.rs index d081c180..e91bc8d2 100644 --- a/crates/radicle/src/cob/identity.rs +++ b/crates/radicle/src/cob/identity.rs @@ -1831,6 +1831,82 @@ mod test { ); } + #[test] + fn terminal_states_concurrent() { + let network = Network::default(); + let alice = &network.alice; + let bob = &network.bob; + let eve = &network.eve; + + let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap(); + let mut alice_doc = alice_identity.doc().clone().edit(); + alice_doc.delegate(bob.signer.public_key().into()); + alice_doc.delegate(eve.signer.public_key().into()); + let a1 = alice_identity + .update( + cob::Title::new("Add Bob and Eve").unwrap(), + "", + &alice_doc.verified().unwrap(), + ) + .unwrap(); + + bob.repo.fetch(alice); + eve.repo.fetch(alice); + + let mut bob_identity = Identity::load_mut(&*bob.repo, &bob.signer).unwrap(); + let mut eve_identity = Identity::load_mut(&*eve.repo, &eve.signer).unwrap(); + + bob_identity.accept(&a1).unwrap(); + eve_identity.accept(&a1).unwrap(); + + alice.repo.fetch(bob); + alice_identity.reload().unwrap(); + assert_eq!(alice_identity.revision(&a1).unwrap().state, State::Accepted); + + alice.repo.fetch(eve); + alice_identity.reload().unwrap(); + assert_eq!(alice_identity.revision(&a1).unwrap().state, State::Accepted); + + let mut alice_doc2 = alice_identity.doc().clone().edit(); + alice_doc2.visibility = Visibility::private([]); + let a2 = alice_identity + .update( + cob::Title::new("A2").unwrap(), + "", + &alice_doc2.verified().unwrap(), + ) + .unwrap(); + + bob.repo.fetch(alice); + eve.repo.fetch(alice); + bob_identity.reload().unwrap(); + eve_identity.reload().unwrap(); + + bob_identity.reject(a2).unwrap(); + eve_identity.reject(a2).unwrap(); + + alice.repo.fetch(bob); + alice.repo.fetch(eve); + alice_identity.reload().unwrap(); + assert_eq!( + alice_identity.revision(&a2).unwrap().state, + State::Rejected(RejectedBy::Vote) + ); + + // a2 (Propose "A2") 1/3 (Rejected by Bob and Eve) + // | + // a1 (Add Bob and Eve) 3/3 (Accepted by Alice, Bob, Eve) + // | + // a0 + + // Alice tries to accept the rejected revision + alice_identity.accept(&a2).unwrap(); + assert_eq!( + alice_identity.revision(&a2).unwrap().state, + State::Rejected(RejectedBy::Vote) + ); + } + #[test] fn test_valid_identity() { let tempdir = tempfile::tempdir().unwrap();