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:
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"));
}
}