commit 024b3c8d4c738b3f386d5c4ecba78b202afe7614
parent e661ffc5cefcb00ff0d4dbe5c53d7adf7b97aeb4
Author: triesap <tyson@radroots.org>
Date: Mon, 3 Aug 2026 23:54:30 +0000
lifecycle: project exhaustive native routes
- expose actor lifecycle phases through every snapshot DTO
- map opening migration recovery degradation and terminal routes
- gate account controls outside admitted lifecycle states
- cover every non-ready route in Compose UI tests
Diffstat:
3 files changed, 68 insertions(+), 13 deletions(-)
diff --git a/crates/studio_ffi/src/commands.rs b/crates/studio_ffi/src/commands.rs
@@ -172,6 +172,19 @@ pub(crate) struct RuntimeCore {
pub(crate) closed: AtomicBool,
}
+impl RuntimeCore {
+ pub(crate) fn snapshot_dto(&self) -> AppSnapshotDto {
+ AppSnapshotDto::from_runtime(&self.actor.snapshot(), self.actor.lifecycle())
+ }
+
+ pub(crate) fn dto_for(
+ &self,
+ snapshot: &radroots_studio_application::AppSnapshot,
+ ) -> AppSnapshotDto {
+ AppSnapshotDto::from_runtime(snapshot, self.actor.lifecycle())
+ }
+}
+
#[derive(uniffi::Object)]
pub struct StudioAppCore {
pub(crate) inner: Arc<RuntimeCore>,
@@ -204,13 +217,13 @@ impl StudioAppCore {
.actor
.bootstrap()
.await
- .map(|snapshot| (&snapshot).into())
+ .map(|snapshot| self.inner.dto_for(&snapshot))
.map_err(StudioError::from)
}
#[must_use]
pub fn snapshot(&self) -> AppSnapshotDto {
- (&self.inner.actor.snapshot()).into()
+ self.inner.snapshot_dto()
}
/// Begins the exclusive generated-account recovery flow without persistence.
@@ -250,7 +263,7 @@ impl StudioAppCore {
.actor
.acknowledge_generated_key_stage(request.handle.id())
.await
- .map(|snapshot| (&snapshot).into())
+ .map(|snapshot| self.inner.dto_for(&snapshot))
.map_err(StudioError::from)
}
@@ -300,7 +313,7 @@ impl StudioAppCore {
)
.await
.map(|_| {
- let snapshot = AppSnapshotDto::from(&self.inner.actor.snapshot());
+ let snapshot = self.inner.snapshot_dto();
AccountCommandReceiptDto {
request_id: context.request_id.clone(),
committed_revision: snapshot.revision,
@@ -324,7 +337,7 @@ impl StudioAppCore {
.actor
.select_account(public_key)
.await
- .map(|snapshot| (&snapshot).into())
+ .map(|snapshot| self.inner.dto_for(&snapshot))
.map_err(StudioError::from)
}
@@ -342,7 +355,7 @@ impl StudioAppCore {
.actor
.activate_account(public_key)
.await
- .map(|snapshot| (&snapshot).into())
+ .map(|snapshot| self.inner.dto_for(&snapshot))
.map_err(StudioError::from)
}
@@ -356,7 +369,7 @@ impl StudioAppCore {
.actor
.sign_out()
.await
- .map(|snapshot| (&snapshot).into())
+ .map(|snapshot| self.inner.dto_for(&snapshot))
.map_err(StudioError::from)
}
@@ -370,7 +383,7 @@ impl StudioAppCore {
.actor
.refresh_active_profile()
.await
- .map(|snapshot| (&snapshot).into())
+ .map(|snapshot| self.inner.dto_for(&snapshot))
.map_err(StudioError::from)
}
@@ -416,7 +429,7 @@ impl StudioAppCore {
.actor
.confirm_account_removal(token)
.await
- .map(|snapshot| (&snapshot).into())
+ .map(|snapshot| self.inner.dto_for(&snapshot))
.map_err(StudioError::from)
}
}
diff --git a/crates/studio_ffi/src/dto.rs b/crates/studio_ffi/src/dto.rs
@@ -1,6 +1,6 @@
use radroots_studio_application::{
ActiveAccountSnapshot, AppLifecycle, AppSnapshot, ProfileLoadState, RelayConnectionState,
- SessionState,
+ RuntimeLifecycle, SessionState,
};
use radroots_studio_domain::{
AccountSummary, BindingAvailability, ProfileMetadata, SafeError, SafeErrorCode,
@@ -62,8 +62,16 @@ pub struct SafeErrorDto {
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum AppLifecycleDto {
- Booting,
+ Opening,
+ CompatibilityChecking,
+ AcquiringOwnership,
+ Migrating,
+ Recovering,
Ready,
+ Degraded,
+ Blocked,
+ ShuttingDown,
+ Closed,
Fatal,
}
@@ -155,7 +163,7 @@ pub struct AppSnapshotDto {
impl From<&AppSnapshot> for AppSnapshotDto {
fn from(snapshot: &AppSnapshot) -> Self {
let (lifecycle, lifecycle_error) = match snapshot.lifecycle() {
- AppLifecycle::Booting => (AppLifecycleDto::Booting, None),
+ AppLifecycle::Booting => (AppLifecycleDto::Opening, None),
AppLifecycle::Ready => (AppLifecycleDto::Ready, None),
AppLifecycle::Fatal(error) => (AppLifecycleDto::Fatal, Some(error.into())),
};
@@ -191,6 +199,36 @@ impl From<&AppSnapshot> for AppSnapshotDto {
}
}
+impl AppSnapshotDto {
+ pub(crate) fn from_runtime(snapshot: &AppSnapshot, runtime: RuntimeLifecycle) -> Self {
+ let mut dto = Self::from(snapshot);
+ let (lifecycle, problem) = match runtime {
+ RuntimeLifecycle::Opening => (AppLifecycleDto::Opening, None),
+ RuntimeLifecycle::CompatibilityChecking => {
+ (AppLifecycleDto::CompatibilityChecking, None)
+ }
+ RuntimeLifecycle::AcquiringOwnership => (AppLifecycleDto::AcquiringOwnership, None),
+ RuntimeLifecycle::Migrating => (AppLifecycleDto::Migrating, None),
+ RuntimeLifecycle::Recovering => (AppLifecycleDto::Recovering, None),
+ RuntimeLifecycle::Ready => (AppLifecycleDto::Ready, None),
+ RuntimeLifecycle::Degraded(error) => {
+ (AppLifecycleDto::Degraded, Some(SafeErrorDto::from(error)))
+ }
+ RuntimeLifecycle::Blocked(error) => {
+ (AppLifecycleDto::Blocked, Some(SafeErrorDto::from(error)))
+ }
+ RuntimeLifecycle::ShuttingDown => (AppLifecycleDto::ShuttingDown, None),
+ RuntimeLifecycle::Closed => (AppLifecycleDto::Closed, None),
+ RuntimeLifecycle::Fatal(error) => {
+ (AppLifecycleDto::Fatal, Some(SafeErrorDto::from(error)))
+ }
+ };
+ dto.lifecycle = lifecycle;
+ dto.lifecycle_error = problem;
+ dto
+ }
+}
+
impl From<&AccountSummary> for AccountDto {
fn from(account: &AccountSummary) -> Self {
Self {
diff --git a/crates/studio_ffi/src/observer.rs b/crates/studio_ffi/src/observer.rs
@@ -89,10 +89,14 @@ impl StudioAppCore {
.map_err(StudioError::from)?;
let id = subscription.id();
let observer: Arc<dyn StudioChangeObserver> = Arc::from(observer);
+ let runtime_core = Arc::clone(&self.inner);
let task = crate::commands::runtime().spawn(async move {
while let Some(change) = subscription.receive().await {
observer.on_change(SnapshotChangeDto {
- snapshot: change.snapshot().into(),
+ snapshot: AppSnapshotDto::from_runtime(
+ change.snapshot(),
+ runtime_core.actor.lifecycle(),
+ ),
previous_revision: change
.previous_revision()
.map(radroots_studio_application::SnapshotRevision::value),