commit ed1e55fb77759dfeb5c2f482e9e927786537a653
parent 8188a96d890912c5787f4b674b83db1326b9f4f1
Author: triesap <tyson@radroots.org>
Date: Mon, 3 Aug 2026 10:50:41 +0000
sdk: replace SDK storage implementation with storage SPIs
- compose memory and SQLite through canonical storage traits
- expose native status and passive integrity reports
- keep SQLite configuration free of backend handles
- verify memory and SQLite lifecycle behavior
Diffstat:
4 files changed, 144 insertions(+), 1 deletion(-)
diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml
@@ -77,6 +77,10 @@ reqwest = { workspace = true, optional = true, default-features = false, feature
"rustls-tls",
] }
+[dev-dependencies]
+tempfile = { workspace = true }
+tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
+
[[test]]
name = "package_boundary"
path = "tests/package_boundary.rs"
diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs
@@ -70,6 +70,18 @@ impl ClientBuilder {
Self::new().storage(Arc::new(MemoryStorage::new(generation)))
}
+ /// Explicitly opens canonical SQLite storage from validated host-owned
+ /// configuration and returns a builder containing only the storage SPI.
+ #[cfg(feature = "sqlite")]
+ pub async fn sqlite(options: crate::storage::SqliteOptions) -> Result<Self> {
+ let storage = radroots_storage_sqlite::SqliteStorage::open(options)
+ .await
+ .map_err(Error::storage_open_failed)?;
+ Ok(Self::new()
+ .storage(Arc::new(storage))
+ .capability_availability(CapabilityId::PERSISTENT_STORAGE, Availability::Available))
+ }
+
/// Injects the canonical storage capability.
#[must_use]
pub fn storage(mut self, storage: Arc<dyn Storage>) -> Self {
@@ -162,6 +174,22 @@ impl Client {
})
}
+ /// Returns canonical backend status without exposing a backend handle.
+ pub async fn storage_status(&self) -> Result<crate::storage::Status> {
+ let storage = self.storage()?;
+ radroots_storage::BackupSource::status(storage)
+ .await
+ .map_err(Error::storage_inspection_failed)
+ }
+
+ /// Runs canonical integrity inspection without exposing backend internals.
+ pub async fn storage_integrity(&self) -> Result<crate::storage::IntegrityStatus> {
+ let storage = self.storage()?;
+ radroots_storage::BackupSource::integrity(storage)
+ .await
+ .map_err(Error::storage_inspection_failed)
+ }
+
/// Returns the injected canonical storage capability.
pub fn storage(&self) -> Result<&dyn Storage> {
self.require_open()?;
@@ -514,6 +542,70 @@ mod tests {
);
}
+ #[test]
+ fn memory_storage_status_integrity_and_lifecycle_use_native_contracts() {
+ use radroots_storage::status::{
+ IntegrityHealth, ShutdownState, StorageBackend, StorageOpenMode, WriterPolicy,
+ };
+
+ let client = ClientBuilder::memory(generation()).build().expect("client");
+ let status = block_on(client.storage_status()).expect("status");
+ assert_eq!(status.backend(), StorageBackend::Memory);
+ assert_eq!(status.open_mode(), StorageOpenMode::Create);
+ assert_eq!(status.writer_policy(), WriterPolicy::NoWriter);
+ assert_eq!(status.shutdown(), ShutdownState::Open);
+ assert_eq!(
+ block_on(client.storage_integrity())
+ .expect("integrity")
+ .health(),
+ IntegrityHealth::Healthy
+ );
+ block_on(client.close()).expect("close");
+ assert_eq!(
+ block_on(client.storage_status())
+ .expect_err("closed")
+ .kind(),
+ crate::error::ErrorKind::ClientClosed
+ );
+ }
+
+ #[cfg(feature = "sqlite")]
+ #[tokio::test]
+ async fn sqlite_builder_exposes_native_status_integrity_and_lifecycle() {
+ use radroots_storage::status::{
+ IntegrityHealth, ShutdownState, StorageBackend, StorageOpenMode, WriterPolicy,
+ };
+
+ let directory = tempfile::tempdir().expect("temporary directory");
+ let paths = crate::storage::SqlitePaths::from_directory(directory.path()).expect("paths");
+ let options =
+ crate::storage::SqliteOptions::new(paths, crate::storage::SqliteOpenMode::Create)
+ .with_source_generation(generation(), 1)
+ .expect("source generation");
+ let client = ClientBuilder::sqlite(options)
+ .await
+ .expect("open builder")
+ .build()
+ .expect("client");
+ let status = client.storage_status().await.expect("status");
+ assert_eq!(status.backend(), StorageBackend::Sqlite);
+ assert_eq!(status.open_mode(), StorageOpenMode::Create);
+ assert_eq!(status.writer_policy(), WriterPolicy::AdvisoryProcessLock);
+ assert_eq!(status.shutdown(), ShutdownState::Open);
+ assert!(status.wal_enabled());
+ assert_ne!(status.busy_timeout_ms(), 0);
+ assert_eq!(
+ client
+ .storage_integrity()
+ .await
+ .expect("integrity")
+ .health(),
+ IntegrityHealth::Unknown
+ );
+ client.close().await.expect("close");
+ assert!(client.is_closed());
+ }
+
struct ThreadWaker;
impl Wake for ThreadWaker {
diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs
@@ -106,6 +106,20 @@ error_catalog! {
message: "SDK storage close failed",
safe_detail_keys: []
},
+ StorageOpenFailed => {
+ code: InternalError,
+ operation: None,
+ capability: Some(CapabilityId::PERSISTENT_STORAGE),
+ message: "SDK persistent storage open failed",
+ safe_detail_keys: []
+ },
+ StorageInspectionFailed => {
+ code: StorageIntegrityFailed,
+ operation: None,
+ capability: Some(CapabilityId::CANONICAL_STORAGE),
+ message: "SDK storage inspection failed",
+ safe_detail_keys: []
+ },
}
/// Stable metadata for one native SDK failure.
@@ -209,6 +223,21 @@ impl Error {
}
}
+ #[cfg(feature = "sqlite")]
+ pub(crate) fn storage_open_failed(source: radroots_storage_sqlite::Error) -> Self {
+ Self {
+ kind: ErrorKind::StorageOpenFailed,
+ source: Some(Box::new(source)),
+ }
+ }
+
+ pub(crate) fn storage_inspection_failed(source: radroots_storage::Error) -> Self {
+ Self {
+ kind: ErrorKind::StorageInspectionFailed,
+ source: Some(Box::new(source)),
+ }
+ }
+
fn without_source(kind: ErrorKind) -> Self {
Self { kind, source: None }
}
diff --git a/crates/sdk/src/storage.rs b/crates/sdk/src/storage.rs
@@ -1 +1,19 @@
-//! Storage capability composition.
+//! Canonical storage capability composition.
+
+/// Backend-neutral storage status owned by `radroots_storage`.
+pub type Status = radroots_storage::StorageStatus;
+
+/// Backend-neutral integrity status owned by `radroots_storage`.
+pub type IntegrityStatus = radroots_storage::status::IntegrityStatus;
+
+/// Validated SQLite open configuration; this contains no connection or pool.
+#[cfg(feature = "sqlite")]
+pub type SqliteOptions = radroots_storage_sqlite::OpenOptions;
+
+/// Explicit SQLite lifecycle mode.
+#[cfg(feature = "sqlite")]
+pub type SqliteOpenMode = radroots_storage_sqlite::OpenMode;
+
+/// Validated SQLite-owned paths; this contains no backend handle.
+#[cfg(feature = "sqlite")]
+pub type SqlitePaths = radroots_storage_sqlite::Paths;