app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 9287b509039a6de36453164c750e7777a03a83a6
parent fb1dd87a07c5c8879b86614133be429fbbc2e2ae
Author: triesap <tyson@radroots.org>
Date:   Fri, 26 Jun 2026 08:23:47 +0000

desktop: align SDK client workflow status

- Switch studio runtime construction to RadrootsClient and trades().
- Keep linked buyer and seller guards aligned with pending RHI workflow state.
- Seed relay-ingest listing fixture locality with deterministic public geohash data.
- Validate with full studio workspace all-feature tests.

Diffstat:
Mcrates/desktop/src/runtime.rs | 72+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mcrates/desktop/src/source_guards.rs | 2+-
Mcrates/runtime/src/sdk.rs | 44++++++++++++++++++++++----------------------
3 files changed, 82 insertions(+), 36 deletions(-)

diff --git a/crates/desktop/src/runtime.rs b/crates/desktop/src/runtime.rs @@ -2585,6 +2585,16 @@ impl DesktopAppRuntimeState { reason: "seller order decision listing address is outside seller authority", }); } + let lifecycle = self.resolve_order_lifecycle_evidence(&request)?; + if lifecycle.decision.is_some() + || lifecycle.status != RadrootsTradeWorkflowState::Requested + || lifecycle.cancellation_event_id.is_some() + || active_order_pending_revision_proposal(&lifecycle).is_some() + { + return Err(AppSqliteError::InvalidProjection { + reason: "seller order decision requires an undecided order", + }); + } let Some(order_export) = sqlite_store.load_seller_order_decision_export(farm_id, order_id)? else { @@ -2869,7 +2879,12 @@ impl DesktopAppRuntimeState { }); } let lifecycle = self.resolve_order_lifecycle_evidence(&request)?; - if lifecycle.decision.is_some() || lifecycle.status != RadrootsTradeWorkflowState::Requested + if lifecycle.decision.is_some() + || !matches!( + lifecycle.status, + RadrootsTradeWorkflowState::Requested + | RadrootsTradeWorkflowState::RevisionProposed + ) { return Err(AppSqliteError::InvalidProjection { reason: "buyer order revision requires active pre-agreement negotiation", @@ -3003,15 +3018,13 @@ impl DesktopAppRuntimeState { reason: "buyer order cancellation requires an open pre-agreement order", }); } + if active_order_pending_revision_proposal(&lifecycle).is_some() { + return Err(AppSqliteError::InvalidProjection { + reason: "buyer order cancellation requires no pending seller proposal", + }); + } let prev_event_id = match lifecycle.status { - RadrootsTradeWorkflowState::Requested => { - if active_order_pending_revision_proposal(&lifecycle).is_some() { - return Err(AppSqliteError::InvalidProjection { - reason: "buyer order cancellation requires no pending seller proposal", - }); - } - request.request_event_id.clone() - } + RadrootsTradeWorkflowState::Requested => request.request_event_id.clone(), RadrootsTradeWorkflowState::RevisionProposed | RadrootsTradeWorkflowState::AgreedPendingRhi | RadrootsTradeWorkflowState::Committed => { @@ -9828,6 +9841,7 @@ mod tests { ProductsFilter, ProductsSort, ReminderDeliveryState, ReminderFeedProjection, ReminderKind, SelectedAccountProjection, SelectedSurfaceProjection, SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary, + TradeAgreementStatus, }; use serde_json::json; use tokio::net::TcpListener; @@ -10758,8 +10772,17 @@ mod tests { fulfillment_location: Some("Relay barn".to_owned()), status: ProductStatus::Published, }; - let listing = super::listing_publish_payload_to_sdk_listing(&listing_payload) + let mut listing = super::listing_publish_payload_to_sdk_listing(&listing_payload) .expect("listing payload should convert to SDK listing"); + listing.location = Some( + radroots_sdk::protocol::listing::RadrootsListingPublicLocation { + primary: "Relay barn".to_owned(), + city: Some("San Francisco".to_owned()), + region: Some("CA".to_owned()), + country: Some("US".to_owned()), + geohash: "9q8yy".to_owned(), + }, + ); let parts = radroots_sdk::protocol::listing::build_draft(&listing) .expect("listing draft should build") .into_wire_parts(); @@ -15380,7 +15403,11 @@ mod tests { reason: "seller order decision requires an undecided order" } )); - assert_eq!(persisted_order_status(&runtime, order_id), "scheduled"); + assert_eq!(persisted_order_status(&runtime, order_id), "needs_action"); + assert_eq!( + persisted_order_workflow_agreement(&runtime, order_id), + "pending_rhi" + ); assert_eq!(relay.event_count(), 1); cleanup_bootstrapped_runtime_paths(&paths); @@ -16106,9 +16133,10 @@ mod tests { .detail .as_ref() .expect("linked buyer order detail should exist"); - assert_eq!(row.status, BuyerOrderStatus::Scheduled); + assert_eq!(row.status, BuyerOrderStatus::Placed); assert_eq!(detail.order_id, fixture.order_id); - assert_eq!(detail.status, BuyerOrderStatus::Scheduled); + assert_eq!(detail.status, BuyerOrderStatus::Placed); + assert_eq!(detail.workflow.agreement, TradeAgreementStatus::PendingRhi); assert_eq!( detail.workflow.provenance.last_event_id.as_deref(), Some(fixture.decision_event_id.as_str()) @@ -20744,6 +20772,24 @@ mod tests { .expect("order status should load") } + fn persisted_order_workflow_agreement( + runtime: &DesktopAppRuntime, + order_id: OrderId, + ) -> String { + runtime + .lock_state() + .sqlite_store + .as_ref() + .expect("sqlite store") + .connection() + .query_row( + "select workflow_agreement from orders where id = ?1 limit 1", + [order_id.to_string()], + |row| row.get::<_, String>(0), + ) + .expect("order workflow agreement should load") + } + fn pending_order_sync_payloads( runtime: &DesktopAppRuntime, account_id: &str, diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs @@ -1126,7 +1126,7 @@ const STRICT_SDK_BOUNDARY_FORBIDDEN_PATTERNS: &[SdkBoundaryForbiddenPattern] = & }, SdkBoundaryForbiddenPattern { pattern: "RadrootsSdkClient", - reason: "app production sources must use the long-lived RadrootsSdk runtime boundary", + reason: "app production sources must use the long-lived RadrootsClient runtime boundary", }, SdkBoundaryForbiddenPattern { pattern: "RadrootsSdkConfig", diff --git a/crates/runtime/src/sdk.rs b/crates/runtime/src/sdk.rs @@ -31,7 +31,7 @@ use radroots_sdk::{ OrderDecisionEnqueueRequest, OrderDecisionReceipt, OrderEvidenceIngestRequest, OrderRequestEvidenceIngestRequest, OrderRevisionDecisionEnqueueRequest, OrderRevisionDecisionReceipt, OrderRevisionProposalEnqueueRequest, - OrderRevisionProposalReceipt, OrderSubmitEnqueueRequest, OrderSubmitReceipt, RadrootsSdk, + OrderRevisionProposalReceipt, OrderSubmitEnqueueRequest, OrderSubmitReceipt, RadrootsClient, RadrootsSdkError, RadrootsSdkStoragePaths, RestoreReceipt, RestoreRequest, SdkBackupVerification, SdkRelayUrlPolicy as SdkRuntimeRelayUrlPolicy, StorageStatusReceipt, StorageStatusRequest, SyncStatusReceipt, SyncStatusRequest, @@ -1302,8 +1302,8 @@ fn run_degraded_worker( ); } -async fn build_sdk_runtime(config: &AppSdkConfig) -> Result<RadrootsSdk, RadrootsSdkError> { - let mut builder = RadrootsSdk::builder() +async fn build_sdk_runtime(config: &AppSdkConfig) -> Result<RadrootsClient, RadrootsSdkError> { + let mut builder = RadrootsClient::builder() .directory_storage(config.storage_root.clone()) .relay_url_policy(config.relay_url_policy.into()); for relay_url in &config.relay_urls { @@ -1330,7 +1330,7 @@ fn run_restore_preflight( .with_overwrite(request.overwrite_existing_sdk_storage) .dry_run(); let result = runtime - .block_on(RadrootsSdk::restore(restore_request)) + .block_on(RadrootsClient::restore(restore_request)) .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error)) .map(|receipt| { let projection_lifecycle = mark_projections_stale( @@ -1351,7 +1351,7 @@ fn run_restore_preflight( } async fn collect_sdk_diagnostics( - sdk: &RadrootsSdk, + sdk: &RadrootsClient, runtime: AppSdkRuntimeStatus, ) -> Result<AppSdkDiagnostics, RadrootsSdkError> { let storage = sdk.storage_status(StorageStatusRequest::new()).await?; @@ -1367,7 +1367,7 @@ async fn collect_sdk_diagnostics( fn enqueue_farm_publish_with_sdk( runtime: &tokio::runtime::Runtime, - sdk: &RadrootsSdk, + sdk: &RadrootsClient, request: AppSdkFarmPublishRequest, ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { let actor = sdk_actor_context( @@ -1394,7 +1394,7 @@ fn enqueue_farm_publish_with_sdk( fn enqueue_listing_publish_with_sdk( runtime: &tokio::runtime::Runtime, - sdk: &RadrootsSdk, + sdk: &RadrootsClient, request: AppSdkListingPublishRequest, ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { let actor = sdk_actor_context( @@ -1421,7 +1421,7 @@ fn enqueue_listing_publish_with_sdk( fn enqueue_order_submit_with_sdk( runtime: &tokio::runtime::Runtime, - sdk: &RadrootsSdk, + sdk: &RadrootsClient, request: AppSdkOrderSubmitRequest, ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { let actor = sdk_actor_context( @@ -1440,7 +1440,7 @@ fn enqueue_order_submit_with_sdk( } let receipt = runtime .block_on( - sdk.orders() + sdk.trades() .enqueue_submit_with_explicit_signer(enqueue, &signer), ) .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; @@ -1449,7 +1449,7 @@ fn enqueue_order_submit_with_sdk( fn enqueue_order_decision_with_sdk( runtime: &tokio::runtime::Runtime, - sdk: &RadrootsSdk, + sdk: &RadrootsClient, request: AppSdkOrderDecisionRequest, ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { let actor = sdk_actor_context( @@ -1461,7 +1461,7 @@ fn enqueue_order_decision_with_sdk( let target_relays = sdk_relay_targets(request.target_relays, request.relay_url_policy)?; runtime .block_on( - sdk.orders() + sdk.trades() .ingest_request_evidence(OrderRequestEvidenceIngestRequest::new( request.request_event, )), @@ -1480,7 +1480,7 @@ fn enqueue_order_decision_with_sdk( } let receipt = runtime .block_on( - sdk.orders() + sdk.trades() .enqueue_decision_with_explicit_signer(enqueue, &signer), ) .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; @@ -1492,7 +1492,7 @@ fn enqueue_order_decision_with_sdk( fn enqueue_order_revision_proposal_with_sdk( runtime: &tokio::runtime::Runtime, - sdk: &RadrootsSdk, + sdk: &RadrootsClient, request: AppSdkOrderRevisionProposalRequest, ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { let actor = sdk_actor_context( @@ -1517,7 +1517,7 @@ fn enqueue_order_revision_proposal_with_sdk( } let receipt = runtime .block_on( - sdk.orders() + sdk.trades() .enqueue_revision_proposal_with_explicit_signer(enqueue, &signer), ) .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; @@ -1529,7 +1529,7 @@ fn enqueue_order_revision_proposal_with_sdk( fn enqueue_order_revision_decision_with_sdk( runtime: &tokio::runtime::Runtime, - sdk: &RadrootsSdk, + sdk: &RadrootsClient, request: AppSdkOrderRevisionDecisionRequest, ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { let actor = sdk_actor_context( @@ -1554,7 +1554,7 @@ fn enqueue_order_revision_decision_with_sdk( } let receipt = runtime .block_on( - sdk.orders() + sdk.trades() .enqueue_revision_decision_with_explicit_signer(enqueue, &signer), ) .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; @@ -1566,7 +1566,7 @@ fn enqueue_order_revision_decision_with_sdk( fn enqueue_order_cancellation_with_sdk( runtime: &tokio::runtime::Runtime, - sdk: &RadrootsSdk, + sdk: &RadrootsClient, request: AppSdkOrderCancellationRequest, ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { let actor = sdk_actor_context( @@ -1591,7 +1591,7 @@ fn enqueue_order_cancellation_with_sdk( } let receipt = runtime .block_on( - sdk.orders() + sdk.trades() .enqueue_cancellation_with_explicit_signer(enqueue, &signer), ) .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; @@ -1603,13 +1603,13 @@ fn enqueue_order_cancellation_with_sdk( fn ingest_order_evidence_with_sdk( runtime: &tokio::runtime::Runtime, - sdk: &RadrootsSdk, + sdk: &RadrootsClient, evidence_events: Vec<RadrootsNostrEvent>, ) -> Result<(), AppSdkRuntimeIssue> { for event in evidence_events { runtime .block_on( - sdk.orders() + sdk.trades() .ingest_evidence(OrderEvidenceIngestRequest::new(event)), ) .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; @@ -1911,7 +1911,7 @@ mod tests { }; use radroots_nostr::prelude::{RadrootsNostrKeys, RadrootsNostrSecretKey}; use radroots_sdk::{ - BackupRequest, LISTING_PUBLISH_OPERATION_KIND, RadrootsSdk, + BackupRequest, LISTING_PUBLISH_OPERATION_KIND, RadrootsClient, SdkRelayUrlPolicy as SdkRuntimeRelayUrlPolicy, }; @@ -2172,7 +2172,7 @@ mod tests { .expect("tokio runtime"); let sdk = tokio .block_on( - RadrootsSdk::builder() + RadrootsClient::builder() .directory_storage(backup_source_root.clone()) .relay_url_policy(SdkRuntimeRelayUrlPolicy::Localhost) .relay_url("ws://127.0.0.1:8080")