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