commit a752e21babfaf382383b5e1f3b7005d7890d64a0
parent 8983bedf79f8b558dee05e643eff61fa54746271
Author: triesap <tyson@radroots.org>
Date: Mon, 3 Aug 2026 13:36:36 +0000
ffi: wrap shared sdk engine
- Add a private UniFFI crate that directly owns the shared SDK client.
- Expose versioned capability, error, and explicit lifecycle DTOs for mobile hosts.
- Keep keychain, background execution, scheduling, and presentation host-owned.
- Verify locked metadata, all targets, lifecycle tests, Clippy, and architecture policy.
Diffstat:
6 files changed, 545 insertions(+), 3 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -57,6 +57,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
+name = "askama"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7"
+dependencies = [
+ "askama_derive",
+ "itoa",
+ "percent-encoding",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "askama_derive"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac"
+dependencies = [
+ "askama_parser",
+ "basic-toml",
+ "memchr",
+ "proc-macro2",
+ "quote",
+ "rustc-hash",
+ "serde",
+ "serde_derive",
+ "syn",
+]
+
+[[package]]
+name = "askama_parser"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f"
+dependencies = [
+ "memchr",
+ "serde",
+ "serde_derive",
+ "winnow",
+]
+
+[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -144,6 +186,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
+name = "basic-toml"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
+dependencies = [
+ "serde",
+]
+
+[[package]]
name = "bech32"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -214,6 +265,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
+name = "camino"
+version = "1.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -477,7 +560,7 @@ dependencies = [
"serde_json",
"sha2",
"syn",
- "toml",
+ "toml 0.8.23",
]
[[package]]
@@ -631,6 +714,15 @@ dependencies = [
]
[[package]]
+name = "fs-err"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
name = "fs2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -778,6 +870,12 @@ dependencies = [
]
[[package]]
+name = "glob"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
+
+[[package]]
name = "gloo-timers"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -790,6 +888,17 @@ dependencies = [
]
[[package]]
+name = "goblin"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47"
+dependencies = [
+ "log",
+ "plain",
+ "scroll",
+]
+
+[[package]]
name = "group"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1270,6 +1379,12 @@ dependencies = [
]
[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1297,6 +1412,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d"
[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
name = "nostr"
version = "0.44.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1481,6 +1606,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
+name = "plain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+
+[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1902,6 +2033,16 @@ dependencies = [
]
[[package]]
+name = "radroots_sdk_ffi"
+version = "0.1.0-alpha"
+dependencies = [
+ "radroots_sdk",
+ "thiserror 2.0.18",
+ "tokio",
+ "uniffi",
+]
+
+[[package]]
name = "radroots_sdk_sql_wasm_runtime"
version = "0.1.0-alpha"
dependencies = [
@@ -1932,7 +2073,7 @@ dependencies = [
"syn",
"tar",
"tempfile",
- "toml",
+ "toml 0.8.23",
]
[[package]]
@@ -2291,6 +2432,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
+name = "scroll"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6"
+dependencies = [
+ "scroll_derive",
+]
+
+[[package]]
+name = "scroll_derive"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "scrypt"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2340,6 +2501,10 @@ name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+dependencies = [
+ "serde",
+ "serde_core",
+]
[[package]]
name = "serde"
@@ -2451,6 +2616,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
+[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2463,6 +2634,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
+name = "smawk"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100"
+
+[[package]]
name = "socket2"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2604,6 +2781,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2665,6 +2848,15 @@ dependencies = [
]
[[package]]
+name = "textwrap"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
+dependencies = [
+ "smawk",
+]
+
+[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2806,6 +2998,15 @@ dependencies = [
[[package]]
name = "toml"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
@@ -2987,6 +3188,125 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
+name = "uniffi"
+version = "0.29.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3291800a6b06569f7d3e15bdb6dc235e0f0c8bd3eb07177f430057feb076415f"
+dependencies = [
+ "anyhow",
+ "cargo_metadata",
+ "uniffi_bindgen",
+ "uniffi_core",
+ "uniffi_macros",
+ "uniffi_pipeline",
+]
+
+[[package]]
+name = "uniffi_bindgen"
+version = "0.29.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a04b99fa7796eaaa7b87976a0dbdd1178dc1ee702ea00aca2642003aef9b669e"
+dependencies = [
+ "anyhow",
+ "askama",
+ "camino",
+ "cargo_metadata",
+ "fs-err",
+ "glob",
+ "goblin",
+ "heck",
+ "indexmap",
+ "once_cell",
+ "serde",
+ "tempfile",
+ "textwrap",
+ "toml 0.5.11",
+ "uniffi_internal_macros",
+ "uniffi_meta",
+ "uniffi_pipeline",
+ "uniffi_udl",
+]
+
+[[package]]
+name = "uniffi_core"
+version = "0.29.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "once_cell",
+ "static_assertions",
+]
+
+[[package]]
+name = "uniffi_internal_macros"
+version = "0.29.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09acd2ce09c777dd65ee97c251d33c8a972afc04873f1e3b21eb3492ade16933"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "uniffi_macros"
+version = "0.29.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5596f178c4f7aafa1a501c4e0b96236a96bc2ef92bdb453d83e609dad0040152"
+dependencies = [
+ "camino",
+ "fs-err",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn",
+ "toml 0.5.11",
+ "uniffi_meta",
+]
+
+[[package]]
+name = "uniffi_meta"
+version = "0.29.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff"
+dependencies = [
+ "anyhow",
+ "siphasher",
+ "uniffi_internal_macros",
+ "uniffi_pipeline",
+]
+
+[[package]]
+name = "uniffi_pipeline"
+version = "0.29.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "tempfile",
+ "uniffi_internal_macros",
+]
+
+[[package]]
+name = "uniffi_udl"
+version = "0.29.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4319cf905911d70d5b97ce0f46f101619a22e9a189c8c46d797a9955e9233716"
+dependencies = [
+ "anyhow",
+ "textwrap",
+ "uniffi_meta",
+ "weedle2",
+]
+
+[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3273,6 +3593,15 @@ dependencies = [
]
[[package]]
+name = "weedle2"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e"
+dependencies = [
+ "nom",
+]
+
+[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
@@ -4,6 +4,7 @@ members = [
"crates/event_codec_wasm",
"crates/event_bindings",
"crates/event_index_bindings",
+ "crates/ffi",
"crates/identity_bindings",
"crates/replica_store_wasm",
"crates/replica_schema_bindings",
@@ -54,6 +55,7 @@ radroots_event_store = { version = "=0.1.0-alpha", default-features = false }
radroots_event = { package = "radroots_event", version = "=0.1.0-alpha", default-features = false }
radroots_event_codec = { package = "radroots_event_codec", version = "=0.1.0-alpha", default-features = false }
radroots_event_index = { version = "=0.1.0-alpha", default-features = false }
+radroots_sdk_ffi = { path = "crates/ffi", version = "=0.1.0-alpha" }
radroots_geocoder = { version = "=0.1.0-alpha" }
radroots_geonames = { package = "radroots_geonames", version = "=0.1.0-alpha", default-features = false }
radroots_identity = { package = "radroots_identity", version = "=0.1.0-alpha", default-features = false, features = [
@@ -106,8 +108,10 @@ serde-wasm-bindgen = { version = "0.6" }
sha2 = { version = "0.10", default-features = false }
sqlx = { version = "0.9.0", default-features = false }
tempfile = { version = "3" }
+thiserror = { version = "2" }
tokio = { version = "1" }
tokio-tungstenite = "0.26.2"
uuid = { version = "1.22.0", features = ["v4", "v7"] }
+uniffi = { version = "0.29.4" }
wasm-bindgen = { version = "0.2" }
wasm-bindgen-test = { version = "0.3" }
diff --git a/contracts/releases/publication.toml b/contracts/releases/publication.toml
@@ -46,7 +46,7 @@ external_packages = [
]
[workspace_classification]
-private = []
+private = ["radroots_sdk_ffi"]
build_codegen = [
"radroots_core_bindings",
"radroots_event_bindings",
diff --git a/crates/ffi/Cargo.toml b/crates/ffi/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "radroots_sdk_ffi"
+publish = false
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+rust-version.workspace = true
+license.workspace = true
+description = "Private UniFFI wrapper over the shared Radroots SDK engine"
+repository.workspace = true
+homepage.workspace = true
+readme = "README.md"
+
+[lib]
+crate-type = ["lib", "staticlib", "cdylib"]
+
+[dependencies]
+radroots_sdk = { workspace = true, features = ["memory"] }
+thiserror = { workspace = true }
+uniffi = { workspace = true }
+
+[dev-dependencies]
+tokio = { workspace = true, features = ["macros", "rt"] }
+
+[lints]
+workspace = true
diff --git a/crates/ffi/README.md b/crates/ffi/README.md
@@ -0,0 +1,10 @@
+# radroots_sdk_ffi
+
+Private UniFFI bindings over the host-neutral `radroots_sdk` engine. The V1
+surface exposes deterministic memory-client construction, versioned capability
+DTOs, secret-safe errors, and explicit asynchronous close. It does not own
+keychain prompts, background execution, application scheduling, or presentation
+DTOs; those remain mobile-host responsibilities.
+
+This Rust build crate is never published. Generated Swift and Kotlin artifacts
+are qualified independently from the public 19-crate Rust inventory.
diff --git a/crates/ffi/src/lib.rs b/crates/ffi/src/lib.rs
@@ -0,0 +1,173 @@
+//! Private mobile FFI over the shared SDK engine.
+
+#![forbid(unsafe_code)]
+#![doc = include_str!("../README.md")]
+
+use std::sync::Arc;
+
+use radroots_sdk::{Client, ClientBuilder};
+
+uniffi::setup_scaffolding!("radroots_sdk");
+
+/// Generation-1 mobile DTOs.
+pub mod v1 {
+ use super::*;
+
+ /// Stable capability maturity independent of runtime availability.
+ #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
+ pub enum CapabilityMaturity {
+ Stable,
+ Preview,
+ Experimental,
+ }
+
+ /// Current side-effect-free capability availability.
+ #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
+ pub enum CapabilityAvailability {
+ Available,
+ Degraded,
+ Unavailable,
+ Unsupported,
+ }
+
+ /// Versioned, presentation-independent capability observation.
+ #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
+ pub struct CapabilityStatus {
+ pub id: String,
+ pub compiled: bool,
+ pub configured: bool,
+ pub availability: CapabilityAvailability,
+ pub maturity: CapabilityMaturity,
+ }
+
+ /// Secret-safe SDK failure at the language boundary.
+ #[derive(Debug, thiserror::Error, uniffi::Error)]
+ pub enum Error {
+ #[error("SDK {code}: {message}")]
+ Sdk {
+ code: String,
+ message: String,
+ retryable: bool,
+ },
+ }
+
+ impl From<radroots_sdk::Error> for Error {
+ fn from(error: radroots_sdk::Error) -> Self {
+ let descriptor = error.descriptor();
+ Self::Sdk {
+ code: descriptor.code().as_str().to_owned(),
+ message: descriptor.message().to_owned(),
+ retryable: descriptor.retryable(),
+ }
+ }
+ }
+
+ /// Thread-safe mobile handle delegating lifecycle to [`radroots_sdk::Client`].
+ #[derive(uniffi::Object)]
+ pub struct MobileClient {
+ client: Client,
+ }
+
+ #[uniffi::export]
+ impl MobileClient {
+ /// Creates a deterministic, local-only memory client without I/O.
+ #[uniffi::constructor]
+ pub fn memory() -> Result<Arc<Self>, Error> {
+ let client = ClientBuilder::memory_default().build()?;
+ Ok(Arc::new(Self { client }))
+ }
+
+ /// Returns stable capability DTOs without probing host resources.
+ pub fn capabilities(&self) -> Vec<CapabilityStatus> {
+ self.client
+ .capabilities()
+ .iter()
+ .copied()
+ .map(CapabilityStatus::from)
+ .collect()
+ }
+
+ /// Returns whether explicit SDK close completed across all clones.
+ pub fn is_closed(&self) -> bool {
+ self.client.is_closed()
+ }
+
+ /// Explicitly closes SDK-owned resources without installing a runtime.
+ pub async fn close(&self) -> Result<(), Error> {
+ self.client.close().await.map_err(Error::from)
+ }
+ }
+
+ impl From<radroots_sdk::capability::CapabilityStatus> for CapabilityStatus {
+ fn from(status: radroots_sdk::capability::CapabilityStatus) -> Self {
+ Self {
+ id: status.id().as_str().to_owned(),
+ compiled: status.is_compiled(),
+ configured: status.is_configured(),
+ availability: match status.availability() {
+ radroots_sdk::capability::Availability::Available => {
+ CapabilityAvailability::Available
+ }
+ radroots_sdk::capability::Availability::Degraded => {
+ CapabilityAvailability::Degraded
+ }
+ radroots_sdk::capability::Availability::Unavailable => {
+ CapabilityAvailability::Unavailable
+ }
+ radroots_sdk::capability::Availability::Unsupported => {
+ CapabilityAvailability::Unsupported
+ }
+ },
+ maturity: match status.maturity() {
+ radroots_sdk::capability::Maturity::Stable => CapabilityMaturity::Stable,
+ radroots_sdk::capability::Maturity::Preview => CapabilityMaturity::Preview,
+ radroots_sdk::capability::Maturity::Experimental => {
+ CapabilityMaturity::Experimental
+ }
+ },
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::v1::{CapabilityAvailability, Error, MobileClient};
+
+ fn assert_send_sync<T: Send + Sync>() {}
+
+ #[tokio::test(flavor = "current_thread")]
+ async fn memory_client_delegates_capabilities_and_lifecycle() {
+ assert_send_sync::<MobileClient>();
+ let client = MobileClient::memory().expect("memory client");
+ let capabilities = client.capabilities();
+ let storage = capabilities
+ .iter()
+ .find(|status| status.id == "storage.canonical")
+ .expect("canonical storage");
+ assert!(storage.compiled);
+ assert!(storage.configured);
+ assert_eq!(storage.availability, CapabilityAvailability::Available);
+ assert!(!client.is_closed());
+
+ client.close().await.expect("close");
+ assert!(client.is_closed());
+ client.close().await.expect("idempotent close");
+ }
+
+ #[test]
+ fn sdk_error_mapping_is_versioned_and_secret_safe() {
+ let native = radroots_sdk::ClientBuilder::new()
+ .build()
+ .expect_err("missing storage");
+ let error = Error::from(native);
+ let Error::Sdk {
+ code,
+ message,
+ retryable,
+ } = error;
+ assert_eq!(code, "missing_storage");
+ assert_eq!(message, "SDK storage capability is not configured");
+ assert!(!retryable);
+ }
+}