sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit 57a750facf767dad9a67a3c7fdf4a0409bdfa53b
parent f27f3092e4e9387316da601fe847fc7aa75ac7a3
Author: triesap <tyson@radroots.org>
Date:   Tue, 23 Jun 2026 01:45:56 +0000

wasm: cover sdk wrapper logic

- remove dummy coverage probes from replica DB and sync WASM crates
- extract replica DB export snapshot policy into native-testable code
- cover replica sync request and event parsing across author and pubkey cases
- narrow the coverage contract to include the newly testable WASM source

Diffstat:
Mcontracts/coverage.toml | 6+++---
Mcrates/replica_db_wasm/src/lib.rs | 23++---------------------
Acrates/replica_db_wasm/src/snapshot.rs | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/replica_db_wasm/src/wasm_impl.rs | 22+++++++++++++---------
Mcrates/replica_sync_wasm/src/lib.rs | 99++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
5 files changed, 176 insertions(+), 61 deletions(-)

diff --git a/contracts/coverage.toml b/contracts/coverage.toml @@ -11,7 +11,7 @@ wasm_target = "wasm32-unknown-unknown" [report] output = "target/sdk-coverage/summary.json" -ignore_filename_regex = "(/target/|/\\.cargo/registry/|/Cellar/rust/|/tools/xtask/|/crates/.+_bindings/|/crates/binding_model/|/crates/replica_(db|sync)_wasm/src/lib.rs|/crates/replica_db_wasm/src/wasm_impl.rs|/crates/replica_sync_wasm/src/lib.rs)" +ignore_filename_regex = "(/target/|/\\.cargo/registry/|/Cellar/rust/|/tools/xtask/|/crates/.+_bindings/|/crates/binding_model/|/crates/replica_db_wasm/src/wasm_impl.rs)" [generated] typescript = "generated TypeScript is excluded from line coverage and verified by generator tests, generated-output reproducibility, package export checks, and pnpm typecheck" @@ -27,8 +27,8 @@ paths = ["crates/*_bindings/**", "crates/binding_model/**"] reason = "binding crates are generator-owned source facades with behavior covered by xtask generator tests" [exclusions.wasm_glue_bootstrap] -paths = ["crates/replica_db_wasm/src/wasm_impl.rs", "crates/replica_sync_wasm/src/lib.rs"] -reason = "temporary bootstrap exclusion until WASM behavior is extracted and covered by the WASM coverage refactor slice" +paths = ["crates/replica_db_wasm/src/wasm_impl.rs"] +reason = "temporary bootstrap exclusion for the generated-style DB wrapper forwarding file while exported snapshot policy and sync parsing behavior are covered natively" [exclusions.xtask_bootstrap] paths = ["tools/xtask/**"] diff --git a/crates/replica_db_wasm/src/lib.rs b/crates/replica_db_wasm/src/lib.rs @@ -1,29 +1,10 @@ -#![cfg(any(target_arch = "wasm32", coverage_nightly))] #![forbid(unsafe_code)] +mod snapshot; #[cfg(target_arch = "wasm32")] mod utils; #[cfg(target_arch = "wasm32")] mod wasm_impl; +pub use snapshot::*; #[cfg(target_arch = "wasm32")] pub use wasm_impl::*; - -#[cfg(coverage_nightly)] -pub fn coverage_branch_probe(input: bool) -> &'static str { - if input { - "replica-db-wasm" - } else { - "replica-db-wasm" - } -} - -#[cfg(all(test, coverage_nightly))] -mod tests { - use super::coverage_branch_probe; - - #[test] - fn coverage_branch_probe_hits_both_paths() { - assert_eq!(coverage_branch_probe(true), "replica-db-wasm"); - assert_eq!(coverage_branch_probe(false), "replica-db-wasm"); - } -} diff --git a/crates/replica_db_wasm/src/snapshot.rs b/crates/replica_db_wasm/src/snapshot.rs @@ -0,0 +1,87 @@ +use radroots_replica_db::ReplicaDbExportManifestRs; + +pub const EXPORT_MANIFEST_FIELD: &str = "manifest_rs"; +pub const EXPORT_DB_BYTES_FIELD: &str = "db_bytes"; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExportManifestSummary { + pub export_version: String, + pub replica_db_version: String, + pub backup_format_version: String, + pub schema_hash: String, + pub schema_table_count: usize, + pub migration_count: usize, + pub table_count_count: usize, +} + +pub fn synced_export_error(pending_count: usize, expected_count: usize) -> Option<String> { + if pending_count == 0 { + None + } else { + Some(format!( + "replica db export requires synced state (pending {pending_count}/{expected_count})" + )) + } +} + +pub fn export_manifest_summary(manifest: &ReplicaDbExportManifestRs) -> ExportManifestSummary { + ExportManifestSummary { + export_version: manifest.export_version.clone(), + replica_db_version: manifest.replica_db_version.clone(), + backup_format_version: manifest.backup_format_version.clone(), + schema_hash: manifest.schema_hash.clone(), + schema_table_count: manifest.schema.len(), + migration_count: manifest.migrations.len(), + table_count_count: manifest.table_counts.len(), + } +} + +#[cfg(test)] +mod tests { + use super::{ + EXPORT_DB_BYTES_FIELD, EXPORT_MANIFEST_FIELD, export_manifest_summary, synced_export_error, + }; + + fn manifest() -> radroots_replica_db::ReplicaDbExportManifestRs { + radroots_replica_db::ReplicaDbExportManifestRs { + export_version: "1".to_owned(), + replica_db_version: "0.1.0".to_owned(), + backup_format_version: "1".to_owned(), + schema_hash: "schema-hash".to_owned(), + schema: Vec::new(), + migrations: Vec::new(), + table_counts: Vec::new(), + } + } + + #[test] + fn export_snapshot_field_names_are_stable() { + assert_eq!(EXPORT_MANIFEST_FIELD, "manifest_rs"); + assert_eq!(EXPORT_DB_BYTES_FIELD, "db_bytes"); + } + + #[test] + fn synced_export_error_allows_empty_pending_queue() { + assert_eq!(synced_export_error(0, 4), None); + } + + #[test] + fn synced_export_error_reports_pending_and_expected_counts() { + assert_eq!( + synced_export_error(2, 4).expect("error"), + "replica db export requires synced state (pending 2/4)" + ); + } + + #[test] + fn export_manifest_summary_preserves_versions_and_counts() { + let summary = export_manifest_summary(&manifest()); + assert_eq!(summary.export_version, "1"); + assert_eq!(summary.replica_db_version, "0.1.0"); + assert_eq!(summary.backup_format_version, "1"); + assert_eq!(summary.schema_hash, "schema-hash"); + assert_eq!(summary.schema_table_count, 0); + assert_eq!(summary.migration_count, 0); + assert_eq!(summary.table_count_count, 0); + } +} diff --git a/crates/replica_db_wasm/src/wasm_impl.rs b/crates/replica_db_wasm/src/wasm_impl.rs @@ -1,4 +1,7 @@ -use crate::utils::value_to_js; +use crate::{ + snapshot::{EXPORT_DB_BYTES_FIELD, EXPORT_MANIFEST_FIELD, synced_export_error}, + utils::value_to_js, +}; use radroots_replica_db::migrations; use radroots_replica_db::{ReplicaDbExportManifestRs, export_manifest}; use radroots_replica_sync::radroots_replica_sync_status; @@ -137,12 +140,9 @@ fn export_snapshot(exec: &WasmSqlExecutor) -> Result<JsValue, JsValue> { err.to_string(), )) })?; - if status.pending_count > 0 { + if let Some(message) = synced_export_error(status.pending_count, status.expected_count) { return Err(err_js(radroots_sql_core::SqlError::InvalidArgument( - format!( - "replica db export requires synced state (pending {}/{})", - status.pending_count, status.expected_count - ), + message, ))); } let manifest = export_manifest(exec).map_err(err_js)?; @@ -164,9 +164,13 @@ fn export_snapshot_value_with_bytes( )) })?; let obj = js_sys::Object::new(); - js_sys::Reflect::set(&obj, &JsValue::from_str("manifest_rs"), &manifest_js) - .map_err(|_| err_js(radroots_sql_core::SqlError::Internal))?; - js_sys::Reflect::set(&obj, &JsValue::from_str("db_bytes"), &bytes_js) + js_sys::Reflect::set( + &obj, + &JsValue::from_str(EXPORT_MANIFEST_FIELD), + &manifest_js, + ) + .map_err(|_| err_js(radroots_sql_core::SqlError::Internal))?; + js_sys::Reflect::set(&obj, &JsValue::from_str(EXPORT_DB_BYTES_FIELD), &bytes_js) .map_err(|_| err_js(radroots_sql_core::SqlError::Internal))?; Ok(JsValue::from(obj)) } diff --git a/crates/replica_sync_wasm/src/lib.rs b/crates/replica_sync_wasm/src/lib.rs @@ -1,20 +1,18 @@ -#![cfg(any(target_arch = "wasm32", coverage_nightly))] #![forbid(unsafe_code)] #[cfg(target_arch = "wasm32")] use base64::Engine; #[cfg(target_arch = "wasm32")] use base64::engine::general_purpose::URL_SAFE_NO_PAD; -#[cfg(target_arch = "wasm32")] use radroots_events::RadrootsNostrEvent; +use radroots_replica_sync::RadrootsReplicaSyncRequest; #[cfg(target_arch = "wasm32")] use radroots_replica_sync::{ - RadrootsReplicaIdFactory, RadrootsReplicaIngestOutcome, RadrootsReplicaSyncRequest, + RadrootsReplicaIdFactory, RadrootsReplicaIngestOutcome, radroots_replica_ingest_event_with_factory, radroots_replica_sync_all, }; #[cfg(target_arch = "wasm32")] use radroots_sdk_sql_wasm_runtime::WasmSqlExecutor; -#[cfg(target_arch = "wasm32")] use serde::Deserialize; #[cfg(target_arch = "wasm32")] use uuid::Uuid; @@ -37,7 +35,6 @@ impl RadrootsReplicaIdFactory for WasmIdFactory { } } -#[cfg(target_arch = "wasm32")] #[derive(Deserialize)] struct NostrEventEnvelope { id: String, @@ -52,21 +49,20 @@ struct NostrEventEnvelope { sig: String, } -#[cfg(target_arch = "wasm32")] -fn parse_request(request_json: &str) -> Result<RadrootsReplicaSyncRequest, JsValue> { - serde_json::from_str(request_json).map_err(err_js) +pub fn parse_request_model(request_json: &str) -> Result<RadrootsReplicaSyncRequest, String> { + serde_json::from_str(request_json).map_err(|error| error.to_string()) } -#[cfg(target_arch = "wasm32")] -fn parse_event(event_json: &str) -> Result<RadrootsNostrEvent, JsValue> { - let envelope: NostrEventEnvelope = serde_json::from_str(event_json).map_err(err_js)?; +pub fn parse_event_model(event_json: &str) -> Result<RadrootsNostrEvent, String> { + let envelope: NostrEventEnvelope = + serde_json::from_str(event_json).map_err(|error| error.to_string())?; let author = match (envelope.author, envelope.pubkey) { (Some(author), Some(pubkey)) if author != pubkey => { - return Err(JsValue::from_str("author/pubkey mismatch")); + return Err("author/pubkey mismatch".to_owned()); } (Some(author), _) => author, (None, Some(pubkey)) => pubkey, - (None, None) => return Err(JsValue::from_str("missing author/pubkey")), + (None, None) => return Err("missing author/pubkey".to_owned()), }; Ok(RadrootsNostrEvent { id: envelope.id, @@ -82,7 +78,7 @@ fn parse_event(event_json: &str) -> Result<RadrootsNostrEvent, JsValue> { #[cfg(target_arch = "wasm32")] #[wasm_bindgen(js_name = replica_sync_sync_all)] pub fn replica_sync_sync_all(request_json: &str) -> Result<JsValue, JsValue> { - let request = parse_request(request_json)?; + let request = parse_request_model(request_json).map_err(err_js)?; let exec = WasmSqlExecutor::new(); let bundle = radroots_replica_sync_all(&exec, &request).map_err(err_js)?; serde_wasm_bindgen::to_value(&bundle).map_err(err_js) @@ -91,7 +87,7 @@ pub fn replica_sync_sync_all(request_json: &str) -> Result<JsValue, JsValue> { #[cfg(target_arch = "wasm32")] #[wasm_bindgen(js_name = replica_sync_ingest_event)] pub fn replica_sync_ingest_event(event_json: &str) -> Result<JsValue, JsValue> { - let event = parse_event(event_json)?; + let event = parse_event_model(event_json).map_err(err_js)?; let exec = WasmSqlExecutor::new(); let factory = WasmIdFactory; let outcome = @@ -103,22 +99,69 @@ pub fn replica_sync_ingest_event(event_json: &str) -> Result<JsValue, JsValue> { Ok(JsValue::from_str(value)) } -#[cfg(coverage_nightly)] -pub fn coverage_branch_probe(input: bool) -> &'static str { - if input { - "replica-sync-wasm" - } else { - "replica-sync-wasm" +#[cfg(test)] +mod tests { + use super::{parse_event_model, parse_request_model}; + + fn event_json(author: Option<&str>, pubkey: Option<&str>) -> String { + let mut fields = vec![ + r#""id":"event-id""#.to_owned(), + r#""created_at":123"#.to_owned(), + r#""kind":30023"#.to_owned(), + r#""tags":[["d","one"]]"#.to_owned(), + r#""content":"content""#.to_owned(), + r#""sig":"sig""#.to_owned(), + ]; + if let Some(author) = author { + fields.push(format!(r#""author":"{author}""#)); + } + if let Some(pubkey) = pubkey { + fields.push(format!(r#""pubkey":"{pubkey}""#)); + } + format!("{{{}}}", fields.join(",")) } -} -#[cfg(all(test, coverage_nightly))] -mod tests { - use super::coverage_branch_probe; + #[test] + fn parse_event_accepts_matching_author_and_pubkey() { + let event = parse_event_model(&event_json(Some("author"), Some("author"))).expect("event"); + assert_eq!(event.author, "author"); + assert_eq!(event.tags, vec![vec!["d".to_owned(), "one".to_owned()]]); + } + + #[test] + fn parse_event_accepts_author_without_pubkey() { + let event = parse_event_model(&event_json(Some("author"), None)).expect("event"); + assert_eq!(event.author, "author"); + } + + #[test] + fn parse_event_accepts_pubkey_without_author() { + let event = parse_event_model(&event_json(None, Some("pubkey"))).expect("event"); + assert_eq!(event.author, "pubkey"); + } + + #[test] + fn parse_event_rejects_author_pubkey_mismatch() { + let error = + parse_event_model(&event_json(Some("author"), Some("pubkey"))).expect_err("error"); + assert_eq!(error, "author/pubkey mismatch"); + } + + #[test] + fn parse_event_rejects_missing_author_and_pubkey() { + let error = parse_event_model(&event_json(None, None)).expect_err("error"); + assert_eq!(error, "missing author/pubkey"); + } + + #[test] + fn parse_event_rejects_malformed_json() { + let error = parse_event_model("{").expect_err("error"); + assert!(error.contains("EOF")); + } #[test] - fn coverage_branch_probe_hits_both_paths() { - assert_eq!(coverage_branch_probe(true), "replica-sync-wasm"); - assert_eq!(coverage_branch_probe(false), "replica-sync-wasm"); + fn parse_request_rejects_malformed_json() { + let error = parse_request_model("{").expect_err("error"); + assert!(error.contains("EOF")); } }