lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 15f0de693d2b4d22a33349e47a2d5e170cb666d5
parent 04e546e8045fe4a27f64db966b2d507236429402
Author: triesap <tyson@radroots.org>
Date:   Thu, 16 Jul 2026 08:26:41 +0000

runtime: add Phase 1 outbox retry decisions

- expose shared retry eligibility decisions for outbox items
- re-check retry state, visibility, and authority
- cover retry state, visibility, and authority failures in tests

Diffstat:
Mcrates/mobile_core/src/runtime/product_surface.rs | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 104 insertions(+), 0 deletions(-)

diff --git a/crates/mobile_core/src/runtime/product_surface.rs b/crates/mobile_core/src/runtime/product_surface.rs @@ -383,6 +383,15 @@ pub struct OutboxItem { pub last_error: Option<String>, } +#[derive(uniffi::Record, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OutboxRetryDecision { + pub item_id: String, + pub is_retryable: bool, + pub authority_gate: AuthorityGate, + pub reason: Option<String>, +} + pub const CANONICAL_CONTEXT_TYPES: [ContextType; 10] = [ ContextType::Regional, ContextType::Network, @@ -1109,6 +1118,51 @@ pub fn fixture_outbox_items() -> Vec<OutboxItem> { .collect() } +fn outbox_state_allows_retry(state: OutboxState) -> bool { + matches!(state, OutboxState::Failed | OutboxState::Conflict) +} + +fn outbox_visibility_allows_retry(visibility: VisibilityClass) -> bool { + !matches!( + visibility, + VisibilityClass::LocalDraft | VisibilityClass::SecretNeverShared + ) +} + +pub fn fixture_outbox_retry_decision(item: OutboxItem) -> OutboxRetryDecision { + let authority_gate = fixture_authority_gate( + item.context.actor, + item.context.clone(), + item.authority_gate.domain, + AuthorityAction::Retry, + ); + let state_allows_retry = outbox_state_allows_retry(item.outbox_state); + let visibility_allows_retry = outbox_visibility_allows_retry(item.visibility); + let is_retryable = state_allows_retry && visibility_allows_retry && authority_gate.is_allowed; + let reason = if is_retryable { + None + } else if !state_allows_retry { + Some(format!( + "{:?} is not a retryable outbox state", + item.outbox_state + )) + } else if !visibility_allows_retry { + Some(format!( + "{:?} visibility cannot be retried", + item.visibility + )) + } else { + authority_gate.reason.clone() + }; + + OutboxRetryDecision { + item_id: item.id, + is_retryable, + authority_gate, + reason, + } +} + #[cfg_attr(not(coverage_nightly), uniffi::export)] impl RadrootsRuntime { pub fn phase1_active_contexts(&self) -> Vec<ActiveContext> { @@ -1139,6 +1193,11 @@ impl RadrootsRuntime { fixture_outbox_items() } + pub fn phase1_outbox_retry_decision(&self, item: OutboxItem) -> OutboxRetryDecision { + let _ = self; + fixture_outbox_retry_decision(item) + } + pub fn phase1_check_authority( &self, actor: WorkflowActor, @@ -1384,6 +1443,51 @@ mod tests { } #[test] + fn outbox_retry_decision_rechecks_state_visibility_and_authority() { + let failed = fixture_outbox_items() + .into_iter() + .find(|item| item.outbox_state == OutboxState::Failed) + .expect("failed outbox fixture"); + + let retryable = fixture_outbox_retry_decision(failed.clone()); + assert!(retryable.is_retryable); + assert_eq!(retryable.item_id, failed.id); + assert_eq!(retryable.authority_gate.action, AuthorityAction::Retry); + assert!(retryable.reason.is_none()); + + let mut queued = failed.clone(); + queued.outbox_state = OutboxState::Queued; + let queued_decision = fixture_outbox_retry_decision(queued); + assert!(!queued_decision.is_retryable); + assert!( + queued_decision + .reason + .as_deref() + .expect("queued reason") + .contains("not a retryable outbox state") + ); + + let mut secret = failed.clone(); + secret.visibility = VisibilityClass::SecretNeverShared; + let secret_decision = fixture_outbox_retry_decision(secret); + assert!(!secret_decision.is_retryable); + assert!( + secret_decision + .reason + .as_deref() + .expect("secret reason") + .contains("visibility cannot be retried") + ); + + let mut denied = failed; + denied.authority_gate.domain = AuthorityDomain::BuyerWorkspace; + let denied_decision = fixture_outbox_retry_decision(denied); + assert!(!denied_decision.is_retryable); + assert!(!denied_decision.authority_gate.is_allowed); + assert!(denied_decision.reason.is_some()); + } + + #[test] fn compatibility_note_quarantines_low_level_roles() { assert!(WORKFLOW_ACTOR_COMPATIBILITY_NOTE.contains("Farmer")); assert!(WORKFLOW_ACTOR_COMPATIBILITY_NOTE.contains("Buyer"));