lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 961fd6b18e61cba6d4ed4be7b8576b5dcfecc4db
parent 2db097f9cbddac08fba681521dc552002052f8c2
Author: triesap <tyson@radroots.org>
Date:   Tue, 28 Jul 2026 06:34:05 +0000

event_store: cover bounded validator failures

- exercise typed capacity diagnostics and transition storage parsing
- exhaust suppression evidence and bounded query conversions
- reject registry mutations and persisted capacity drift
- preserve production AST hashes while improving governed coverage

Diffstat:
Mcrates/event_store/src/error.rs | 46++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/event_store/src/migrations.rs | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/event_store/src/model.rs | 8++++++++
Mcrates/event_store/src/model/addressable_transition_feed_v1.rs | 43+++++++++++++++++++++++++++++++++++++++++++
Mcrates/event_store/src/model/current_visibility_v1.rs | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/event_store/src/model/food_availability_projection_v1.rs | 7+++++++
Mcrates/event_store/src/source_maintenance_v1.rs | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 543 insertions(+), 0 deletions(-)

diff --git a/crates/event_store/src/error.rs b/crates/event_store/src/error.rs @@ -556,4 +556,50 @@ mod tests { RadrootsEventStoreError::Transport(RadrootsTransportError::InvalidTargetUri) )); } + + #[test] + fn capacity_resources_and_inbound_foreign_keys_have_stable_diagnostics() { + for (resource, expected) in [ + ( + RadrootsEventStoreSourceCapacityResourceV1::RawEvents, + "raw event count", + ), + ( + RadrootsEventStoreSourceCapacityResourceV1::RawTags, + "raw tag count", + ), + ( + RadrootsEventStoreSourceCapacityResourceV1::RawEventBytes, + "total retained raw-source event row text bytes", + ), + ( + RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes, + "total retained raw-source tag row text bytes", + ), + ] { + assert_eq!(resource.as_str(), expected); + assert_eq!(resource.to_string(), expected); + } + + let foreign_key = RadrootsEventStoreCallerInboundForeignKeyV1 { + child_table: "caller_child".to_owned(), + foreign_key_id: 2, + foreign_key_sequence: 1, + child_column: "event_id".to_owned(), + parent_table: "event_envelopes".to_owned(), + parent_column: Some("event_id".to_owned()), + on_update: "CASCADE".to_owned(), + on_delete: "RESTRICT".to_owned(), + match_clause: "NONE".to_owned(), + }; + assert!(foreign_key.to_string().contains("`event_id`")); + assert!( + RadrootsEventStoreCallerInboundForeignKeyV1 { + parent_column: None, + ..foreign_key + } + .to_string() + .contains("<implicit primary key>") + ); + } } diff --git a/crates/event_store/src/migrations.rs b/crates/event_store/src/migrations.rs @@ -1394,6 +1394,20 @@ mod migration_framework { "5b1f92779640f1a2dbd75e37a96996bda6c8be58883190f69eb3eced22a48f03"; const FROZEN_V1_OBJECT_COUNT: usize = 46; + fn assert_registry_defect( + registry: &[EventStoreMigration], + minimum: u32, + current: u32, + expected: &str, + ) { + let error = validate_migration_registry(registry, minimum, current) + .expect_err("migration registry defect"); + assert!( + matches!(&error, RadrootsEventStoreError::MigrationRegistryDefect { reason } if reason.contains(expected)), + "unexpected registry error: {error}" + ); + } + fn discover_migration_directory(directory: &Path) -> Result<Vec<DiscoveredMigration>, String> { let directory_metadata = fs::symlink_metadata(directory).map_err(|error| { format!( @@ -1585,6 +1599,140 @@ mod migration_framework { } #[test] + fn registry_validator_rejects_each_structural_mutation() { + const ZERO_SHA256: &str = + "0000000000000000000000000000000000000000000000000000000000000000"; + const RESERVED_OBJECT: &[&str] = &["radroots_event_store_fixture"]; + const OTHER_RESERVED_OBJECT: &[&str] = &["radroots_event_store_other_fixture"]; + const DUPLICATE_OBJECTS: &[&str] = &[ + "radroots_event_store_fixture", + "radroots_event_store_fixture", + ]; + const ORDINARY_REPLACEMENT: &[&str] = &["event_envelope_kind_created_idx"]; + + let baseline = EVENT_STORE_MIGRATIONS[0]; + assert_registry_defect(&[baseline], 0, 1, "non-empty positive registry"); + assert_registry_defect(&[baseline], 2, 1, "non-empty positive registry"); + assert_registry_defect(&[], 1, 1, "non-empty positive registry"); + + let mut mutated = baseline; + mutated.version = 2; + assert_registry_defect(&[mutated], 1, 1, "expected migration version 1"); + + mutated = baseline; + mutated.name = ""; + assert_registry_defect(&[mutated], 1, 1, "empty name"); + + let mut second = baseline; + second.version = 2; + second.owned_object_names = RESERVED_OBJECT; + second.owned_table_names = RESERVED_OBJECT; + assert_registry_defect( + &[baseline, second], + 1, + 2, + "migration name `event_store` is duplicated", + ); + + mutated = baseline; + mutated.owned_object_names = &[]; + mutated.owned_table_names = &[]; + mutated.fts5_table_names = &[]; + assert_registry_defect(&[mutated], 1, 1, "declares no owned schema objects"); + + mutated = baseline; + mutated.owned_object_names = DUPLICATE_OBJECTS; + mutated.owned_table_names = &[]; + mutated.fts5_table_names = &[]; + assert_registry_defect(&[mutated], 1, 1, "owned schema object"); + + mutated = baseline; + mutated.owned_table_names = &["event_envelopes", "event_envelopes"]; + mutated.fts5_table_names = &[]; + assert_registry_defect(&[mutated], 1, 1, "owned schema table"); + + second.name = "fixture"; + second.owned_object_names = &["ordinary_fixture"]; + second.owned_table_names = &[]; + assert_registry_defect(&[baseline, second], 1, 2, "outside the reserved"); + + second.owned_object_names = RESERVED_OBJECT; + second.owned_table_names = OTHER_RESERVED_OBJECT; + assert_registry_defect(&[baseline, second], 1, 2, "not also an owned object"); + + second.owned_object_names = &[ + "radroots_event_store_fixture", + "radroots_event_store_fixture_fts", + ]; + second.owned_table_names = RESERVED_OBJECT; + second.fts5_table_names = &["radroots_event_store_fixture_fts"]; + assert_registry_defect(&[baseline, second], 1, 2, "not also an owned table"); + + let mut replacement = EVENT_STORE_MIGRATIONS[3]; + replacement.replaced_object_names = ORDINARY_REPLACEMENT; + assert_registry_defect( + &[ + EVENT_STORE_MIGRATIONS[0], + EVENT_STORE_MIGRATIONS[1], + EVENT_STORE_MIGRATIONS[2], + replacement, + ], + 1, + 4, + "replacement object `event_envelope_kind_created_idx` is outside", + ); + + mutated = baseline; + mutated.up_len += 1; + assert!(matches!( + validate_migration_registry(&[mutated], 1, 1), + Err(RadrootsEventStoreError::EmbeddedMigrationLengthMismatch { .. }) + )); + + mutated = baseline; + mutated.up_sha256 = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + assert_registry_defect(&[mutated], 1, 1, "invalid up SHA-256 literal"); + + mutated = baseline; + mutated.up_sha256 = ZERO_SHA256; + assert!(matches!( + validate_migration_registry(&[mutated], 1, 1), + Err(RadrootsEventStoreError::EmbeddedMigrationChecksumMismatch { .. }) + )); + + mutated = baseline; + mutated.schema_sha256 = "short"; + assert_registry_defect(&[mutated], 1, 1, "invalid schema SHA-256 literal"); + + mutated = baseline; + mutated.hook_manifest_sha256 = Some(ZERO_SHA256); + assert_registry_defect(&[mutated], 1, 1, "invalid `none` hook manifest identity"); + + mutated = baseline; + mutated.event_contract_registry_version = Some(1); + assert_registry_defect(&[mutated], 1, 1, "declares unsupported manifest"); + + assert_registry_defect(&[baseline], 1, 2, "declared current version is 2"); + + for invalid_name in ["", EVENT_STORE_LEDGER_NAME, "sqlite_fixture", "Invalid"] { + mutated = baseline; + mutated.owned_object_names = match invalid_name { + "" => &[""], + value if value == EVENT_STORE_LEDGER_NAME => &[EVENT_STORE_LEDGER_NAME], + "sqlite_fixture" => &["sqlite_fixture"], + _ => &["Invalid"], + }; + mutated.owned_table_names = &[]; + mutated.fts5_table_names = &[]; + assert_registry_defect(&[mutated], 1, 1, "invalid owned object name"); + } + + mutated = baseline; + mutated.version = u32::MAX; + assert_registry_defect(&[mutated], u32::MAX, u32::MAX, "migration version overflow"); + } + + #[test] fn governed_name_matching_uses_sqlite_ascii_identifier_semantics() { for name in [ "event_envelopes", diff --git a/crates/event_store/src/model.rs b/crates/event_store/src/model.rs @@ -710,6 +710,14 @@ mod tests { .expect("caller-redacted message"); assert_eq!(observation.caller_redacted_message(), Some("seen")); assert_eq!( + observation + .caller_redacted_message + .as_ref() + .expect("message") + .as_ref(), + "seen" + ); + assert_eq!( observation.endpoint_uri().as_str(), "wss://relay.example.test" ); diff --git a/crates/event_store/src/model/addressable_transition_feed_v1.rs b/crates/event_store/src/model/addressable_transition_feed_v1.rs @@ -644,4 +644,47 @@ mod tests { }) if actual == RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1 + 1 )); } + + #[test] + fn transition_storage_enums_round_trip_and_reject_unknown_values() { + for origin in [ + RadrootsAddressableTransitionOriginV1::Baseline, + RadrootsAddressableTransitionOriginV1::Incremental, + ] { + assert_eq!( + RadrootsAddressableTransitionOriginV1::parse(origin.as_str()).expect("origin"), + origin + ); + } + assert!(RadrootsAddressableTransitionOriginV1::parse("unknown").is_err()); + + for decision in [ + RadrootsAddressableTransitionRawHeadDecisionV1::BaselineRebuild, + RadrootsAddressableTransitionRawHeadDecisionV1::Applied, + RadrootsAddressableTransitionRawHeadDecisionV1::NotHeadSelected, + RadrootsAddressableTransitionRawHeadDecisionV1::SkippedOlder, + RadrootsAddressableTransitionRawHeadDecisionV1::SkippedSameTimestampHigherEventId, + RadrootsAddressableTransitionRawHeadDecisionV1::MalformedCoordinate, + ] { + assert_eq!( + RadrootsAddressableTransitionRawHeadDecisionV1::parse(decision.as_str()) + .expect("raw-head decision"), + decision + ); + } + assert!(RadrootsAddressableTransitionRawHeadDecisionV1::parse("unknown").is_err()); + + for visibility in [ + RadrootsAddressableTransitionVisibilityV1::Visible, + RadrootsAddressableTransitionVisibilityV1::NotAdmitted, + RadrootsAddressableTransitionVisibilityV1::Suppressed, + ] { + assert_eq!( + RadrootsAddressableTransitionVisibilityV1::parse(visibility.as_str()) + .expect("visibility"), + visibility + ); + } + assert!(RadrootsAddressableTransitionVisibilityV1::parse("unknown").is_err()); + } } diff --git a/crates/event_store/src/model/current_visibility_v1.rs b/crates/event_store/src/model/current_visibility_v1.rs @@ -152,3 +152,143 @@ impl RadrootsCurrentEventVisibilityV1 { self.decision } } + +#[cfg(test)] +mod tests { + use super::*; + + fn event_id(byte: char) -> RadrootsEventId { + RadrootsEventId::parse(core::iter::repeat_n(byte, 64).collect::<String>()) + .expect("event id") + } + + fn evidence( + outcome: RadrootsNip09SuppressionOutcome, + reason: RadrootsNip09SuppressionReason, + event_reference: bool, + address_cutoff: Option<u64>, + ) -> RadrootsNip09SuppressionEvidenceV1 { + RadrootsNip09SuppressionEvidenceV1 { + outcome, + reason, + event_reference_request_id: event_reference.then(|| event_id('a')), + address_reference_request_id: address_cutoff.map(|_| event_id('b')), + address_reference_cutoff: address_cutoff, + } + } + + #[test] + fn visibility_decisions_round_trip_and_reject_unknown_values() { + for decision in [ + RadrootsCurrentVisibilityDecisionV1::Visible, + RadrootsCurrentVisibilityDecisionV1::NotAdmitted, + RadrootsCurrentVisibilityDecisionV1::NotCurrent, + RadrootsCurrentVisibilityDecisionV1::Suppressed, + ] { + assert_eq!( + RadrootsCurrentVisibilityDecisionV1::parse(decision.as_str()).expect("decision"), + decision + ); + } + assert!(RadrootsCurrentVisibilityDecisionV1::parse("unknown").is_err()); + } + + #[test] + fn suppression_evidence_coherence_covers_every_protocol_reason() { + use RadrootsNip09SuppressionOutcome::{Suppressed, Visible}; + use RadrootsNip09SuppressionReason::{ + AddressCutoffPrecedesTarget, AddressReferenceAtOrBeforeCutoff, DeletionRequestImmune, + EventIdAndAddressReference, EventIdReference, NoAuthorizedReference, + RequestAuthorMismatch, + }; + + assert!(evidence(Visible, DeletionRequestImmune, false, None).is_coherent_for_event(5, 10)); + assert!( + !evidence(Suppressed, DeletionRequestImmune, false, None).is_coherent_for_event(5, 10) + ); + assert!(!evidence(Visible, DeletionRequestImmune, true, None).is_coherent_for_event(5, 10)); + assert!( + !evidence(Visible, DeletionRequestImmune, false, None).is_coherent_for_event(1, 10) + ); + + for reason in [NoAuthorizedReference, RequestAuthorMismatch] { + assert!(evidence(Visible, reason, false, None).is_coherent_for_event(1, 10)); + assert!(!evidence(Suppressed, reason, false, None).is_coherent_for_event(1, 10)); + assert!(!evidence(Visible, reason, true, None).is_coherent_for_event(1, 10)); + assert!(!evidence(Visible, reason, false, Some(9)).is_coherent_for_event(1, 10)); + } + + assert!( + evidence(Visible, AddressCutoffPrecedesTarget, false, Some(9)) + .is_coherent_for_event(1, 10) + ); + assert!( + !evidence(Visible, AddressCutoffPrecedesTarget, false, Some(10)) + .is_coherent_for_event(1, 10) + ); + assert!( + !evidence(Suppressed, AddressCutoffPrecedesTarget, false, Some(9)) + .is_coherent_for_event(1, 10) + ); + assert!( + !evidence(Visible, AddressCutoffPrecedesTarget, true, Some(9)) + .is_coherent_for_event(1, 10) + ); + + assert!(evidence(Suppressed, EventIdReference, true, None).is_coherent_for_event(1, 10)); + assert!(evidence(Suppressed, EventIdReference, true, Some(9)).is_coherent_for_event(1, 10)); + assert!( + !evidence(Suppressed, EventIdReference, true, Some(10)).is_coherent_for_event(1, 10) + ); + assert!(!evidence(Visible, EventIdReference, true, None).is_coherent_for_event(1, 10)); + assert!(!evidence(Suppressed, EventIdReference, false, None).is_coherent_for_event(1, 10)); + + assert!( + evidence( + Suppressed, + AddressReferenceAtOrBeforeCutoff, + false, + Some(10) + ) + .is_coherent_for_event(1, 10) + ); + assert!( + !evidence(Suppressed, AddressReferenceAtOrBeforeCutoff, false, Some(9)) + .is_coherent_for_event(1, 10) + ); + assert!( + !evidence(Visible, AddressReferenceAtOrBeforeCutoff, false, Some(10)) + .is_coherent_for_event(1, 10) + ); + assert!( + !evidence(Suppressed, AddressReferenceAtOrBeforeCutoff, true, Some(10)) + .is_coherent_for_event(1, 10) + ); + + assert!( + evidence(Suppressed, EventIdAndAddressReference, true, Some(10)) + .is_coherent_for_event(1, 10) + ); + assert!( + !evidence(Visible, EventIdAndAddressReference, true, Some(10)) + .is_coherent_for_event(1, 10) + ); + assert!( + !evidence(Suppressed, EventIdAndAddressReference, false, Some(10)) + .is_coherent_for_event(1, 10) + ); + assert!( + !evidence(Suppressed, EventIdAndAddressReference, true, Some(9)) + .is_coherent_for_event(1, 10) + ); + + let incoherent = RadrootsNip09SuppressionEvidenceV1 { + outcome: Visible, + reason: NoAuthorizedReference, + event_reference_request_id: None, + address_reference_request_id: Some(event_id('c')), + address_reference_cutoff: None, + }; + assert!(!incoherent.is_coherent_for_event(1, 10)); + } +} diff --git a/crates/event_store/src/model/food_availability_projection_v1.rs b/crates/event_store/src/model/food_availability_projection_v1.rs @@ -444,6 +444,13 @@ mod tests { query.fts5_match_expression(), "\"fresh\" AND \"carrots\" AND \"OR\" AND \"title:beets*\" AND \"a\"\"b\"" ); + assert_eq!(query.to_string(), query.as_str()); + assert_eq!( + RadrootsFoodAvailabilitySearchQueryV1::try_from("fresh carrots") + .expect("TryFrom query") + .as_str(), + "fresh carrots" + ); } #[test] diff --git a/crates/event_store/src/source_maintenance_v1.rs b/crates/event_store/src/source_maintenance_v1.rs @@ -855,6 +855,157 @@ mod tests { )); } + #[test] + fn capacity_arithmetic_and_storage_conversions_fail_closed() { + let resource = RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes; + assert_eq!( + raw_tag_row_bytes_v1("e", "t", None, "[]").expect("tag bytes"), + 4 + ); + assert!(matches!( + checked_capacity_add(resource, u64::MAX, 1), + Err(RadrootsEventStoreError::SourceCapacityExceeded { + resource: RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes, + current: u64::MAX, + requested: 1, + .. + }) + )); + assert!(matches!( + validate_prospective_capacity( + capacity_with(resource, u64::MAX), + delta_with(resource, 1) + ), + Err(RadrootsEventStoreError::SourceCapacityExceeded { + resource: RadrootsEventStoreSourceCapacityResourceV1::RawTagBytes, + current: u64::MAX, + requested: 1, + .. + }) + )); + + assert_eq!( + sqlite_nonnegative_capacity(resource, 7).expect("positive"), + 7 + ); + assert!(sqlite_nonnegative_capacity(resource, -1).is_err()); + assert_eq!( + sqlite_capacity_value(7, "fixture").expect("SQLite value"), + 7 + ); + assert!(sqlite_capacity_value(u64::MAX, "fixture").is_err()); + assert_eq!(generation_count(1).expect("generation count"), 1); + assert!(generation_count(0).is_err()); + assert!(generation_count(-1).is_err()); + assert!(generation_count(i64::from(u32::MAX) + 1).is_err()); + assert_eq!( + source_generation_bytes(vec![0x42; 32]).expect("generation"), + [0x42; 32] + ); + assert!(source_generation_bytes(vec![0x42; 31]).is_err()); + assert!(matches!( + source_capacity_drift::<()>("fixture drift".to_owned()), + Err(RadrootsEventStoreError::SourceCapacityStateDrift { reason }) + if reason == "fixture drift" + )); + } + + #[tokio::test] + async fn direct_capacity_validation_rejects_seal_and_row_count_drift() { + for assignment in [ + "raw_event_count = raw_event_count + 1", + "raw_tag_count = raw_tag_count + 1", + "raw_high_water_seq = raw_high_water_seq + 1", + "retained_generation_count = retained_generation_count + 1", + ] { + let store = RadrootsEventStore::open_memory().await.expect("store"); + let mut transaction = store.begin_write_transaction().await.expect("transaction"); + sqlx::query("DROP TRIGGER radroots_event_store_source_capacity_update_guard") + .execute(&mut *transaction) + .await + .expect("drop update guard"); + sqlx::query(sqlx::AssertSqlSafe(format!( + "UPDATE radroots_event_store_source_capacity_v1 SET {assignment} WHERE singleton = 1" + ))) + .execute(&mut *transaction) + .await + .expect("corrupt capacity seal"); + assert!(matches!( + validate_source_capacity_authority_fast_v1(&mut transaction).await, + Err(RadrootsEventStoreError::SourceCapacityStateDrift { .. }) + )); + transaction.rollback().await.expect("rollback fixture"); + } + + let store = RadrootsEventStore::open_memory().await.expect("store"); + let mut transaction = store.begin_write_transaction().await.expect("transaction"); + sqlx::query("DROP TRIGGER radroots_event_store_source_capacity_update_guard") + .execute(&mut *transaction) + .await + .expect("drop update guard"); + sqlx::query( + "UPDATE radroots_event_store_source_capacity_v1 SET raw_event_bytes = raw_event_bytes + 1 WHERE singleton = 1", + ) + .execute(&mut *transaction) + .await + .expect("corrupt measured byte seal"); + assert!(matches!( + validate_source_capacity_authority_full_v1(&mut transaction).await, + Err(RadrootsEventStoreError::SourceCapacityStateDrift { .. }) + )); + transaction.rollback().await.expect("rollback fixture"); + + let store = RadrootsEventStore::open_memory().await.expect("store"); + let mut transaction = store.begin_write_transaction().await.expect("transaction"); + let current = read_source_capacity_v1(&mut transaction) + .await + .expect("current capacity"); + assert!(matches!( + bind_source_capacity_to_generation_v1(&mut transaction, current.source_generation) + .await, + Err(RadrootsEventStoreError::SourceCapacityStateDrift { .. }) + )); + transaction.rollback().await.expect("rollback fixture"); + + let store = RadrootsEventStore::open_memory().await.expect("store"); + let mut transaction = store.begin_write_transaction().await.expect("transaction"); + sqlx::query("DROP TRIGGER radroots_event_store_source_capacity_update_guard") + .execute(&mut *transaction) + .await + .expect("drop update guard"); + sqlx::query( + "CREATE TEMP TRIGGER ignore_capacity_bind BEFORE UPDATE ON radroots_event_store_source_capacity_v1 BEGIN SELECT RAISE(IGNORE); END", + ) + .execute(&mut *transaction) + .await + .expect("install ignored update"); + assert!(matches!( + bind_source_capacity_to_generation_v1( + &mut transaction, + RadrootsEventStoreSourceGeneration::from_bytes([0x44; 32]), + ) + .await, + Err(RadrootsEventStoreError::SourceCapacityStateDrift { .. }) + )); + transaction.rollback().await.expect("rollback fixture"); + + let store = RadrootsEventStore::open_memory().await.expect("store"); + let mut transaction = store.begin_write_transaction().await.expect("transaction"); + sqlx::query("DROP TRIGGER radroots_event_store_source_capacity_delete_guard") + .execute(&mut *transaction) + .await + .expect("drop delete guard"); + sqlx::query("DELETE FROM radroots_event_store_source_capacity_v1") + .execute(&mut *transaction) + .await + .expect("delete capacity row"); + assert!(matches!( + read_source_capacity_v1(&mut transaction).await, + Err(RadrootsEventStoreError::SourceCapacityStateDrift { .. }) + )); + transaction.rollback().await.expect("rollback fixture"); + } + #[tokio::test] async fn generation_sql_backstop_allows_exact_append_and_is_conflict_safe_one_over() { let store = RadrootsEventStore::open_memory().await.expect("open store");