commit fd6a0ef6368d7010f69d7cabfff22c2e05c8697d
parent f70c151433a82cda12e6060df7c981991ebd9630
Author: triesap <tyson@radroots.org>
Date: Sun, 2 Aug 2026 01:05:35 +0000
storage: complete package conformance coverage
- enforce the exact root export and feature contracts
- expose the aggregate storage and backup capabilities
- qualify no-default individual and all-feature builds
- prove dependency leakage docs tests and strict lint gates
Diffstat:
12 files changed, 181 insertions(+), 30 deletions(-)
diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs
@@ -14,11 +14,33 @@ pub mod private_artifact;
pub mod projection;
pub mod status;
-pub use atomic::AtomicStorage;
-pub use backup::StorageReliability;
+pub use backup::StorageReliability as BackupSource;
pub use error::Error;
pub use event::EventStore;
pub use journal::Journal;
pub use outbox::Outbox;
-pub use private_artifact::PrivateArtifactStore;
pub use projection::ProjectionStore;
+pub use status::StorageStatus;
+
+/// Complete backend-neutral storage capability implemented by concrete stores.
+pub trait Storage:
+ EventStore
+ + Journal
+ + Outbox
+ + ProjectionStore
+ + private_artifact::PrivateArtifactStore
+ + backup::StorageReliability
+ + atomic::AtomicStorage
+{
+}
+
+impl<T> Storage for T where
+ T: EventStore
+ + Journal
+ + Outbox
+ + ProjectionStore
+ + private_artifact::PrivateArtifactStore
+ + backup::StorageReliability
+ + atomic::AtomicStorage
+{
+}
diff --git a/crates/storage/src/memory.rs b/crates/storage/src/memory.rs
@@ -6,15 +6,14 @@ use radroots_transport::{BoxFuture, source::EventProvenance};
use std::sync::{Mutex, MutexGuard};
use crate::{
- AtomicStorage, Error, EventStore, Journal, Outbox, PrivateArtifactStore, ProjectionStore,
- StorageReliability,
+ Error, EventStore, Journal, Outbox, ProjectionStore,
atomic::{
AtomicCommit, AtomicCommitDisposition, AtomicCommitId, AtomicCommitOutcome,
- AtomicCommitReceipt, AtomicWorkflow,
+ AtomicCommitReceipt, AtomicStorage, AtomicWorkflow,
},
backup::{
BackupId, BackupOperation, BackupPlan, BackupTransition, ReliabilityRevision,
- RestoreOperation, RestorePlan, RestoreTransition,
+ RestoreOperation, RestorePlan, RestoreTransition, StorageReliability,
},
event::{
AdmissionDisposition, AdmissionReceipt, AdmissionStage, EventAdmission, EventPage,
@@ -33,7 +32,7 @@ use crate::{
private_artifact::{
DeletionReason, EXPIRED_ARTIFACT_QUERY_LIMIT_MAX, PrivateArtifactId,
PrivateArtifactMetadata, PrivateArtifactRevision, PrivateArtifactStage,
- PrivateArtifactStatus,
+ PrivateArtifactStatus, PrivateArtifactStore,
},
projection::{
EventIndexCheckpoint, EventIndexManifest, ProjectionCheckpoint, ProjectionGeneration,
diff --git a/crates/storage/tests/atomic.rs b/crates/storage/tests/atomic.rs
@@ -1,10 +1,10 @@
use futures_executor::block_on;
use radroots_protocol::runtime::v1::OperationId;
use radroots_storage::{
- AtomicStorage, Error,
+ Error,
atomic::{
AtomicCommit, AtomicCommitDigest, AtomicCommitDisposition, AtomicCommitId,
- AtomicCommitOutcome, AtomicCommitReceipt, AtomicWorkflow,
+ AtomicCommitOutcome, AtomicCommitReceipt, AtomicStorage, AtomicWorkflow,
},
journal::{IdempotencyDigest, IdempotencyKey, OperationInstanceId, PrepareOperation},
};
diff --git a/crates/storage/tests/backup.rs b/crates/storage/tests/backup.rs
@@ -1,10 +1,10 @@
use radroots_storage::{
- Error, StorageReliability,
+ Error,
backup::{
BackupFormatVersion, BackupId, BackupManifest, BackupMember, BackupMemberKind,
BackupOperation, BackupPlan, BackupSecretPolicy, BackupStage, BackupTransition,
MemberDigest, MemberVerification, ReliabilityRevision, RestoreMemberStatus,
- RestoreOperation, RestorePlan, RestoreStage, RestoreTransition,
+ RestoreOperation, RestorePlan, RestoreStage, RestoreTransition, StorageReliability,
},
status::{
IntegrityHealth, IntegrityStatus, ShutdownState, StorageBackend, StorageOpenMode,
diff --git a/crates/storage/tests/conformance.rs b/crates/storage/tests/conformance.rs
@@ -1,9 +1,11 @@
+#![cfg(feature = "memory")]
+
#[path = "conformance/suite.rs"]
mod suite;
use radroots_storage::{
- AtomicStorage, EventStore, Journal, Outbox, PrivateArtifactStore, ProjectionStore,
- StorageReliability, memory::MemoryStorage,
+ EventStore, Journal, Outbox, ProjectionStore, atomic::AtomicStorage,
+ backup::StorageReliability, memory::MemoryStorage, private_artifact::PrivateArtifactStore,
};
use suite::StorageConformanceHarness;
diff --git a/crates/storage/tests/conformance/suite.rs b/crates/storage/tests/conformance/suite.rs
@@ -2,16 +2,15 @@ use futures_executor::block_on;
use radroots_event::{SignedEvent, wire::Nip01EventWire};
use radroots_protocol::runtime::v1::OperationId;
use radroots_storage::{
- AtomicStorage, EventStore, Journal, Outbox, PrivateArtifactStore, ProjectionStore,
- StorageReliability,
+ EventStore, Journal, Outbox, ProjectionStore,
atomic::{
- AtomicCommit, AtomicCommitDigest, AtomicCommitDisposition, AtomicCommitId, AtomicWorkflow,
- CommitEnqueued, CommitIngested, CommitSigned,
+ AtomicCommit, AtomicCommitDigest, AtomicCommitDisposition, AtomicCommitId, AtomicStorage,
+ AtomicWorkflow, CommitEnqueued, CommitIngested, CommitSigned,
},
backup::{
BackupFormatVersion, BackupId, BackupManifest, BackupMember, BackupMemberKind, BackupPlan,
BackupSecretPolicy, BackupStage, BackupTransition, MemberDigest, MemberVerification,
- RestoreMemberStatus, RestorePlan, RestoreStage, RestoreTransition,
+ RestoreMemberStatus, RestorePlan, RestoreStage, RestoreTransition, StorageReliability,
},
event::{EventAdmission, EventQuery, EventQueryBounds},
journal::{
@@ -24,7 +23,7 @@ use radroots_storage::{
},
private_artifact::{
ArtifactCommitment, ArtifactKind, ArtifactSchemaId, DurableSecretReference,
- PrivateArtifactId, PrivateArtifactMetadata, RetentionPolicy,
+ PrivateArtifactId, PrivateArtifactMetadata, PrivateArtifactStore, RetentionPolicy,
},
projection::{ProjectionCheckpoint, ProjectionGeneration, ProjectionId},
status::{ShutdownState, StorageBackend},
@@ -435,7 +434,16 @@ fn signed_event(content: &str) -> SignedEvent {
extra: Default::default(),
};
wire.id = wire.computed_event_id().expect("event id").to_hex();
- let raw = serde_json::to_string(&wire).expect("event JSON");
+ let raw = serde_json::json!({
+ "id": &wire.id,
+ "pubkey": &wire.pubkey,
+ "created_at": wire.created_at,
+ "kind": wire.kind,
+ "tags": &wire.tags,
+ "content": &wire.content,
+ "sig": &wire.sig,
+ })
+ .to_string();
SignedEvent::from_wire_verified_id(wire, raw).expect("signed event")
}
diff --git a/crates/storage/tests/event_store.rs b/crates/storage/tests/event_store.rs
@@ -274,7 +274,16 @@ fn signed_event_with_signature(signature_byte: &str) -> SignedEvent {
.computed_event_id()
.expect("canonical event id")
.to_hex();
- let raw_json = serde_json::to_string(&wire).expect("event JSON");
+ let raw_json = serde_json::json!({
+ "id": &wire.id,
+ "pubkey": &wire.pubkey,
+ "created_at": wire.created_at,
+ "kind": wire.kind,
+ "tags": &wire.tags,
+ "content": &wire.content,
+ "sig": &wire.sig,
+ })
+ .to_string();
SignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event")
}
diff --git a/crates/storage/tests/feature_contract.rs b/crates/storage/tests/feature_contract.rs
@@ -0,0 +1,27 @@
+#[cfg(feature = "memory")]
+use radroots_storage::Storage;
+
+#[cfg(feature = "memory")]
+#[test]
+fn memory_feature_provides_the_complete_storage_capability() {
+ fn accepts(_: &dyn Storage) {}
+ accepts(&radroots_storage::memory::MemoryStorage::default());
+}
+
+#[cfg(not(feature = "memory"))]
+#[test]
+fn storage_spi_builds_without_the_memory_backend() {
+ fn accepts_spi(_: Option<&dyn radroots_storage::EventStore>) {}
+ accepts_spi(None);
+}
+
+#[cfg(feature = "serde")]
+#[test]
+fn serde_feature_round_trips_backend_status_discriminants() {
+ let encoded = serde_json::to_string(&radroots_storage::status::StorageBackend::Memory)
+ .expect("serialize backend");
+ assert_eq!(encoded, "\"memory\"");
+ let decoded: radroots_storage::status::StorageBackend =
+ serde_json::from_str(&encoded).expect("deserialize backend");
+ assert_eq!(decoded, radroots_storage::status::StorageBackend::Memory);
+}
diff --git a/crates/storage/tests/memory.rs b/crates/storage/tests/memory.rs
@@ -1,11 +1,13 @@
+#![cfg(feature = "memory")]
+
use futures_executor::block_on;
use radroots_event::{SignedEvent, wire::Nip01EventWire};
use radroots_protocol::runtime::v1::OperationId;
use radroots_storage::{
- AtomicStorage, EventStore, Journal, Outbox, PrivateArtifactStore, ProjectionStore,
+ EventStore, Journal, Outbox, ProjectionStore,
atomic::{
- AtomicCommit, AtomicCommitDigest, AtomicCommitDisposition, AtomicCommitId, AtomicWorkflow,
- CommitIngested, CommitSigned,
+ AtomicCommit, AtomicCommitDigest, AtomicCommitDisposition, AtomicCommitId, AtomicStorage,
+ AtomicWorkflow, CommitIngested, CommitSigned,
},
event::{EventAdmission, EventQuery, EventQueryBounds, SourceGeneration},
journal::{
@@ -19,7 +21,8 @@ use radroots_storage::{
},
private_artifact::{
ArtifactCommitment, ArtifactKind, ArtifactSchemaId, DurableSecretReference,
- PrivateArtifactId, PrivateArtifactMetadata, PrivateArtifactRevision, RetentionPolicy,
+ PrivateArtifactId, PrivateArtifactMetadata, PrivateArtifactRevision, PrivateArtifactStore,
+ RetentionPolicy,
},
projection::{
InvalidationReason, ProjectionCheckpoint, ProjectionGeneration, ProjectionHealth,
@@ -46,7 +49,16 @@ fn signed_event() -> SignedEvent {
extra: Default::default(),
};
wire.id = wire.computed_event_id().expect("event id").to_hex();
- let raw = serde_json::to_string(&wire).expect("event JSON");
+ let raw = serde_json::json!({
+ "id": &wire.id,
+ "pubkey": &wire.pubkey,
+ "created_at": wire.created_at,
+ "kind": wire.kind,
+ "tags": &wire.tags,
+ "content": &wire.content,
+ "sig": &wire.sig,
+ })
+ .to_string();
SignedEvent::from_wire_verified_id(wire, raw).expect("signed event")
}
diff --git a/crates/storage/tests/outbox.rs b/crates/storage/tests/outbox.rs
@@ -171,7 +171,16 @@ fn signed_event() -> SignedEvent {
.computed_event_id()
.expect("canonical event id")
.to_hex();
- let raw_json = serde_json::to_string(&wire).expect("event JSON");
+ let raw_json = serde_json::json!({
+ "id": &wire.id,
+ "pubkey": &wire.pubkey,
+ "created_at": wire.created_at,
+ "kind": wire.kind,
+ "tags": &wire.tags,
+ "content": &wire.content,
+ "sig": &wire.sig,
+ })
+ .to_string();
SignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event")
}
diff --git a/crates/storage/tests/package_boundary.rs b/crates/storage/tests/package_boundary.rs
@@ -24,6 +24,20 @@ fn manifest_matches_the_release_v1_package_boundary() {
"storage SPI must not depend on {forbidden}"
);
}
+
+ for feature in [
+ "default = [\"memory\", \"serde\"]",
+ "memory = []",
+ "serde = [",
+ ] {
+ assert!(manifest.contains(feature), "missing feature law: {feature}");
+ }
+ for forbidden_feature in ["sqlite =", "tokio =", "nostr =", "runtime ="] {
+ assert!(
+ !manifest.contains(forbidden_feature),
+ "storage SPI declares forbidden feature {forbidden_feature}"
+ );
+ }
}
#[test]
@@ -46,3 +60,51 @@ fn release_v1_public_module_skeleton_is_declared() {
);
}
}
+
+#[test]
+fn root_exports_are_exact_and_implementation_types_do_not_leak() {
+ let root = include_str!("../src/lib.rs");
+ for required in [
+ "pub trait Storage:",
+ "pub use backup::StorageReliability as BackupSource;",
+ "pub use error::Error;",
+ "pub use event::EventStore;",
+ "pub use journal::Journal;",
+ "pub use outbox::Outbox;",
+ "pub use projection::ProjectionStore;",
+ "pub use status::StorageStatus;",
+ ] {
+ assert!(root.contains(required), "missing root contract: {required}");
+ }
+ for forbidden in [
+ "pub use atomic::AtomicStorage",
+ "pub use backup::StorageReliability;",
+ "pub use private_artifact::PrivateArtifactStore",
+ ] {
+ assert!(
+ !root.contains(forbidden),
+ "forbidden root export: {forbidden}"
+ );
+ }
+
+ let source_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
+ let mut source = String::new();
+ for entry in fs::read_dir(source_root).expect("source directory") {
+ let path = entry.expect("source entry").path();
+ if path.extension().and_then(|value| value.to_str()) == Some("rs") {
+ source.push_str(&fs::read_to_string(path).expect("source file"));
+ }
+ }
+ for forbidden in [
+ "sqlx::",
+ "rusqlite::",
+ "nostr_sdk::",
+ "std::path::Path",
+ "tokio::runtime",
+ ] {
+ assert!(
+ !source.contains(forbidden),
+ "public storage source leaks implementation path {forbidden}"
+ );
+ }
+}
diff --git a/crates/storage/tests/private_artifact.rs b/crates/storage/tests/private_artifact.rs
@@ -1,9 +1,9 @@
use radroots_storage::{
- Error, PrivateArtifactStore,
+ Error,
private_artifact::{
ArtifactCommitment, ArtifactKind, ArtifactSchemaId, DeletionReason, DurableSecretReference,
PrivateArtifactId, PrivateArtifactMetadata, PrivateArtifactRevision, PrivateArtifactStage,
- RetentionPolicy,
+ PrivateArtifactStore, RetentionPolicy,
},
};
@@ -77,6 +77,7 @@ fn tombstones_preserve_commitment_and_enforce_retention() {
}
#[test]
+#[cfg(feature = "serde")]
fn secret_references_are_bounded_redacted_and_round_trip() {
let reference =
DurableSecretReference::new("keyring", "opaque-key-token", 3).expect("secret reference");