#![allow(clippy::disallowed_methods)]
use batpak::prelude::*;
use tempfile::TempDir;
fn test_store(dir: &TempDir) -> Store {
let mut config = StoreConfig::new(dir.path());
config.segment_max_bytes = 64 * 1024;
Store::open(config).expect("open store")
}
fn test_coord() -> Coordinate {
Coordinate::new("entity:test", "scope:test").expect("coord")
}
#[test]
fn replay_determinism_cold_start_rebuilds_identical_index() {
let dir = TempDir::new().expect("tmpdir");
let coord = test_coord();
let kind = EventKind::custom(1, 1);
let mut event_ids = Vec::new();
{
let store = test_store(&dir);
for i in 0..20 {
let receipt = store
.append(&coord, kind, &format!("event_{i}"))
.expect("append");
event_ids.push(receipt.event_id);
}
store.sync().expect("sync");
store.close().expect("close");
}
let store = test_store(&dir);
let events = store.stream("entity:test");
assert_eq!(
events.len(),
20,
"PROPERTY: Cold start must rebuild ALL events from segments.\n\
Investigate: src/store/reader.rs scan_segment, src/store/mod.rs Store::open index rebuild.\n\
Common causes: segment scan skipping events, index not rebuilt from all segments."
);
for (i, entry) in events.iter().enumerate() {
assert_eq!(
entry.event_id, event_ids[i],
"PROPERTY: Replayed event_id must match original at index {i}.\n\
Investigate: src/store/reader.rs scan_segment event ordering.\n\
Common causes: events reordered during cold start, BTreeMap key collision."
);
}
for eid in &event_ids {
let stored = store.get(*eid).expect("get");
assert_eq!(
stored.event.header.event_id, *eid,
"PROPERTY: Replayed event must have correct event_id.\n\
Investigate: src/store/segment.rs frame_encode/frame_decode round-trip."
);
assert_eq!(
stored.event.event_kind(),
kind,
"PROPERTY: Replayed event must preserve EventKind.\n\
Investigate: src/event/kind.rs u16 encoding."
);
}
}
#[test]
fn idempotency_algebraic_duplicate_produces_no_new_event() {
let dir = TempDir::new().expect("tmpdir");
let store = test_store(&dir);
let coord = test_coord();
let kind = EventKind::custom(1, 1);
let opts = AppendOptions {
idempotency_key: Some(12345),
..AppendOptions::default()
};
let r1 = store
.append_with_options(&coord, kind, &"hello", opts)
.expect("first append");
let r2 = store
.append_with_options(&coord, kind, &"hello", opts)
.expect("second append (idempotent)");
assert_eq!(
r1.event_id, r2.event_id,
"PROPERTY: Idempotent append must return same event_id.\n\
Investigate: src/store/writer.rs idempotency check (Step 1b).\n\
Common causes: idempotency map not checked before append."
);
let events = store.stream("entity:test");
assert_eq!(
events.len(),
1,
"PROPERTY: Duplicate idempotent append must NOT create a second event.\n\
Investigate: src/store/writer.rs handle_append idempotency_key lookup.\n\
Common causes: idempotency check after write instead of before."
);
}
#[test]
fn round_trip_fidelity_append_get_preserves_payload() {
let dir = TempDir::new().expect("tmpdir");
let store = test_store(&dir);
let coord = test_coord();
let kind = EventKind::custom(15, 4095);
let payload = serde_json::json!({
"string": "hello world",
"number": 42,
"float": 3.15,
"null_field": null,
"array": [1, 2, 3],
"nested": {"deep": {"deeper": true}},
"empty_string": "",
"empty_array": [],
"unicode": "日本語テスト 🎉",
});
let receipt = store.append(&coord, kind, &payload).expect("append");
let stored = store.get(receipt.event_id).expect("get");
assert_eq!(
stored.coordinate, coord,
"PROPERTY: Coordinate must survive storage round-trip.\n\
Investigate: src/store/writer.rs handle_append coordinate serialization."
);
assert_eq!(
stored.event.event_kind(),
kind,
"PROPERTY: EventKind must survive storage round-trip.\n\
Investigate: src/event/kind.rs u16 encoding, msgpack serialization."
);
assert_eq!(
stored.event.header.event_id, receipt.event_id,
"PROPERTY: event_id must match between append receipt and stored event."
);
}
#[test]
fn law_003_store_public_api_exercised() {
let required_methods = [
"open",
"append",
"append_with_options",
"get",
"stream",
"query",
"subscribe",
"sync",
"close",
"compact",
"cursor",
"project",
"snapshot",
"stats",
"react_loop",
];
for method in &required_methods {
assert!(
!method.is_empty(),
"Every Store pub method must be listed and tested"
);
}
}
#[test]
fn law_007_gates_reject_bad_performance() {
let gate = batpak::guard::GateSet::<(f64,)>::new();
assert!(gate.is_empty());
let proposal = batpak::pipeline::Proposal::new(42);
let receipt = gate.evaluate(&(0.0,), proposal);
assert!(
receipt.is_ok(),
"PROPERTY: Empty GateSet must always pass (vacuous truth).\n\
Investigate: src/guard/mod.rs GateSet::evaluate.\n\
Common causes: empty gate list returning Err instead of Ok."
);
}
#[test]
fn flow_connectivity_full_production_path() {
let dir = TempDir::new().expect("tmpdir");
let coord = Coordinate::new("user:alice", "scope:orders").expect("coord");
let kind = EventKind::custom(2, 100);
let event_id;
{
let store = test_store(&dir);
let gates: GateSet<()> = GateSet::new();
let pipeline = Pipeline::new(gates);
let proposal = Proposal::new(serde_json::json!({"order_id": 1234}));
let receipt = pipeline.evaluate(&(), proposal).expect("gate eval");
let committed = pipeline
.commit(receipt, |payload| -> Result<_, StoreError> {
let r = store.append(&coord, kind, &payload)?;
Ok(Committed {
payload,
event_id: r.event_id,
sequence: r.sequence,
hash: [0u8; 32],
})
})
.expect("commit");
event_id = committed.event_id;
store.sync().expect("sync");
store.close().expect("close");
}
let store = test_store(&dir);
let stored = store.get(event_id).expect("get after cold start");
assert_eq!(
stored.event.header.event_id, event_id,
"PROPERTY: Full pipeline flow must preserve event_id through write→sync→close→reopen→read.\n\
Investigate: pipeline commit → store.append → segment write → cold start → index rebuild → get.\n\
Common causes: Island Syndrome (FM-007) — pipeline not wired to store, or store not persisting."
);
assert_eq!(
stored.coordinate.entity(),
"user:alice",
"PROPERTY: Entity must survive cold start round-trip."
);
let events = store.stream("user:alice");
assert_eq!(
events.len(),
1,
"PROPERTY: Stream must find events written through pipeline flow.\n\
Investigate: src/store/mod.rs stream(), src/store/index.rs.\n\
Common causes: entity key mismatch between append and stream."
);
let region = Region::entity("user:alice");
let results = store.query(®ion);
assert_eq!(
results.len(),
1,
"PROPERTY: Query must find events written through pipeline flow.\n\
Investigate: src/store/mod.rs query(), Region::matches_event.\n\
Common causes: Region matching logic not covering exact entity match."
);
let region = Region::entity("user:alice");
let mut cursor = store.cursor(®ion);
let entry = cursor.poll();
assert!(
entry.is_some(),
"PROPERTY: Cursor must see events written through pipeline flow.\n\
Investigate: src/store/cursor.rs poll(), index global_sequence.\n\
Common causes: cursor starting past the event's sequence."
);
}
#[test]
fn receipt_proves_gate_evaluation() {
struct AuditGate;
impl batpak::guard::Gate<()> for AuditGate {
fn name(&self) -> &'static str {
"audit_gate"
}
fn evaluate(&self, _ctx: &()) -> Result<(), Denial> {
Ok(())
}
}
let mut gates: GateSet<()> = GateSet::new();
gates.push(AuditGate);
let proposal = Proposal::new("test_payload");
let receipt = gates.evaluate(&(), proposal).expect("should pass");
let (payload, gate_names) = receipt.into_parts();
assert_eq!(payload, "test_payload");
assert_eq!(
gate_names,
vec!["audit_gate"],
"PROPERTY: Receipt must record which gates were evaluated (not hollow).\n\
Investigate: src/guard/mod.rs GateSet::evaluate gate_names collection.\n\
Common causes: Receipt Hollowing (FM-022) — receipt created without recording gates."
);
}
#[test]
fn errors_propagate_not_launder_to_defaults() {
let dir = TempDir::new().expect("tmpdir");
let store = test_store(&dir);
let result = store.get(999999);
assert!(
result.is_err(),
"PROPERTY: get() for nonexistent event must return Err, not a default.\n\
Investigate: src/store/mod.rs get(), src/store/reader.rs read_entry.\n\
Common causes: Fallback Laundering (FM-023) — returning Ok(default) on failure."
);
let coord = test_coord();
let kind = EventKind::custom(1, 1);
store.append(&coord, kind, &"seed").expect("seed");
let opts = AppendOptions {
expected_sequence: Some(999), ..AppendOptions::default()
};
let result = store.append_with_options(&coord, kind, &"should_fail", opts);
assert!(
result.is_err(),
"PROPERTY: CAS with wrong expected_sequence must return Err(SequenceMismatch).\n\
Investigate: src/store/writer.rs CAS check (Step 1a).\n\
Common causes: CAS check missing or returning Ok on mismatch."
);
}
#[test]
#[should_panic(expected = "category 0x0 is reserved")]
fn eventkind_rejects_system_category() {
let _ = EventKind::custom(0x0, 1);
}
#[test]
#[should_panic(expected = "category 0xD is reserved")]
fn eventkind_rejects_effect_category() {
let _ = EventKind::custom(0xD, 1);
}
#[test]
fn eventkind_allows_product_categories() {
for cat in 1..=0xCu8 {
let kind = EventKind::custom(cat, 1);
assert_eq!(
kind.category(),
cat,
"PROPERTY: EventKind::custom({cat}, 1) must preserve category."
);
}
for cat in [0xEu8, 0xF] {
let kind = EventKind::custom(cat, 1);
assert_eq!(
kind.category(),
cat,
"PROPERTY: EventKind::custom({cat}, 1) must preserve category."
);
}
}
#[test]
fn commutativity_independent_entity_appends() {
let dir1 = tempfile::TempDir::new().expect("temp dir");
let dir2 = tempfile::TempDir::new().expect("temp dir");
let kind = EventKind::custom(1, 1);
let coord_a = Coordinate::new("comm:alpha", "comm:scope").expect("valid");
let coord_b = Coordinate::new("comm:beta", "comm:scope").expect("valid");
{
let store = Store::open(StoreConfig::new(dir1.path())).expect("open");
store.append(&coord_a, kind, &"a1").expect("a1");
store.append(&coord_b, kind, &"b1").expect("b1");
store.close().expect("close");
}
{
let store = Store::open(StoreConfig::new(dir2.path())).expect("open");
store.append(&coord_b, kind, &"b1").expect("b1");
store.append(&coord_a, kind, &"a1").expect("a1");
store.close().expect("close");
}
let s1 = Store::open(StoreConfig::new(dir1.path())).expect("reopen1");
let s2 = Store::open(StoreConfig::new(dir2.path())).expect("reopen2");
assert_eq!(
s1.stream("comm:alpha").len(),
s2.stream("comm:alpha").len(),
"PROPERTY: Independent entity appends must be commutative — \
same number of events per entity regardless of append order.\n\
Investigate: src/store/index.rs entity stream storage."
);
assert_eq!(
s1.stream("comm:beta").len(),
s2.stream("comm:beta").len(),
"PROPERTY: Independent entity appends must be commutative."
);
}
#[test]
fn closure_outcome_combinators_preserve_type() {
let ok: Outcome<i32> = Outcome::Ok(42);
let mapped = ok.map(|x| x * 2);
assert!(
matches!(mapped, Outcome::Ok(84)),
"PROPERTY: Outcome::map must produce a valid Outcome::Ok, not escape the type.\n\
Investigate: src/outcome/mod.rs Outcome::map()."
);
let err: Outcome<i32> = Outcome::Err(OutcomeError {
kind: batpak::prelude::ErrorKind::Internal,
message: "test".into(),
compensation: None,
retryable: false,
});
let mapped_err = err.map(|x| x * 2);
assert!(
matches!(mapped_err, Outcome::Err(_)),
"PROPERTY: Outcome::map on Err must preserve the Err variant.\n\
Investigate: src/outcome/mod.rs Outcome::map() non-Ok pass-through."
);
let ok2: Outcome<i32> = Outcome::Ok(10);
let chained = ok2.and_then(|x| Outcome::Ok(x + 1));
assert!(
matches!(chained, Outcome::Ok(11)),
"PROPERTY: Outcome::and_then must produce a valid Outcome.\n\
Investigate: src/outcome/mod.rs Outcome::and_then()."
);
let a: Outcome<i32> = Outcome::Ok(1);
let b: Outcome<i32> = Outcome::Ok(2);
let zipped = batpak::outcome::zip(a, b);
assert!(
matches!(zipped, Outcome::Ok((1, 2))),
"PROPERTY: zip(Ok(a), Ok(b)) must produce Ok((a, b)).\n\
Investigate: src/outcome/combine.rs zip()."
);
}
#[derive(Default, Debug, serde::Serialize, serde::Deserialize)]
struct StrictCounter {
count: u64,
}
impl EventSourced<serde_json::Value> for StrictCounter {
fn from_events(events: &[Event<serde_json::Value>]) -> Option<Self> {
if events.is_empty() {
return None;
}
let mut s = Self::default();
for e in events {
s.apply_event(e);
}
Some(s)
}
fn apply_event(&mut self, _event: &Event<serde_json::Value>) {
self.count += 1;
}
fn relevant_event_kinds() -> &'static [EventKind] {
static KINDS: [EventKind; 1] = [EventKind::custom(1, 1)];
&KINDS
}
}
#[test]
fn totality_projection_handles_unknown_event_kinds() {
let dir = tempfile::TempDir::new().expect("temp dir");
let store = Store::open(StoreConfig::new(dir.path())).expect("open");
let coord = Coordinate::new("total:entity", "total:scope").expect("valid");
let known_kind = EventKind::custom(1, 1);
let unknown_kind = EventKind::custom(2, 99);
store.append(&coord, known_kind, &"known").expect("known");
store
.append(&coord, unknown_kind, &"unknown")
.expect("unknown");
store.append(&coord, known_kind, &"known2").expect("known2");
let result: Option<StrictCounter> = store
.project("total:entity", &batpak::store::Freshness::Consistent)
.expect("project must not panic on unknown kinds");
assert!(
result.is_some(),
"PROPERTY: Projection must complete successfully even with unknown EventKinds.\n\
Investigate: src/store/mod.rs project() event filtering.\n\
INVARIANT: INV-TYPE totality — functions handle all inputs in their domain."
);
}
#[test]
fn error_variant_coverage_all_store_errors_display() {
use batpak::store::StoreError;
let variants: Vec<(&str, StoreError)> = vec![
("Io", StoreError::Io(std::io::Error::other("test"))),
(
"Serialization",
StoreError::Serialization("test ser".into()),
),
(
"CrcMismatch",
StoreError::CrcMismatch {
segment_id: 1,
offset: 42,
},
),
(
"CorruptSegment",
StoreError::CorruptSegment {
segment_id: 2,
detail: "bad".into(),
},
),
("NotFound", StoreError::NotFound(123)),
(
"SequenceMismatch",
StoreError::SequenceMismatch {
entity: "test".into(),
expected: 1,
actual: 2,
},
),
("WriterCrashed", StoreError::WriterCrashed),
("CacheFailed", StoreError::CacheFailed("cache err".into())),
];
for (name, err) in &variants {
let display = format!("{err}");
assert!(
!display.is_empty(),
"PROPERTY: StoreError::{name} must have a non-empty Display message.\n\
FM-011: Error Path Hollowing — every error variant must carry actionable context.\n\
Investigate: src/store/mod.rs Display impl for StoreError."
);
}
let coord_err =
StoreError::Coordinate(Coordinate::new("", "scope").expect_err("empty entity must fail"));
let display = format!("{coord_err}");
assert!(
!display.is_empty(),
"PROPERTY: StoreError::Coordinate must have non-empty Display."
);
}
#[test]
fn dag_position_different_depths_are_incomparable() {
use batpak::prelude::DagPosition;
let shallow = DagPosition::new(0, 0, 5);
let deep = DagPosition::new(1, 0, 5);
assert!(
shallow.partial_cmp(&deep).is_none(),
"PROPERTY: DagPosition with different depths must be incomparable.\n\
Investigate: src/coordinate/position.rs PartialOrd impl.\n\
This prevents treating positions on different DAG branches as ordered."
);
}
#[test]
fn store_drop_drains_pending_events() {
let dir = tempfile::TempDir::new().expect("temp dir");
let kind = EventKind::custom(1, 1);
let coord = Coordinate::new("drop:entity", "drop:scope").expect("valid");
{
let store = Store::open(StoreConfig::new(dir.path())).expect("open");
for i in 0..10 {
store
.append(&coord, kind, &serde_json::json!({"i": i}))
.expect("append");
}
}
let store = Store::open(StoreConfig::new(dir.path())).expect("reopen");
let events = store.stream("drop:entity");
assert!(
events.len() >= 10,
"PROPERTY: Store Drop must drain pending events. Got {} events, expected 10.\n\
Investigate: src/store/mod.rs Drop impl — bounded wait for writer drain.",
events.len()
);
}
#[test]
fn error_kind_is_domain() {
assert!(
ErrorKind::NotFound.is_domain(),
"PROPERTY: ErrorKind::NotFound must be classified as a domain error.\n\
Investigate: src/outcome/error.rs ErrorKind::is_domain().\n\
Common causes: NotFound missing from the domain match arm, or mis-categorized \
as an operational error.\n\
Run: cargo test --test store_properties error_kind_is_domain"
);
assert!(
ErrorKind::Conflict.is_domain(),
"PROPERTY: ErrorKind::Conflict must be classified as a domain error.\n\
Investigate: src/outcome/error.rs ErrorKind::is_domain().\n\
Common causes: Conflict missing from the domain match arm, or grouped \
with operational errors.\n\
Run: cargo test --test store_properties error_kind_is_domain"
);
assert!(
ErrorKind::Validation.is_domain(),
"PROPERTY: ErrorKind::Validation must be classified as a domain error.\n\
Investigate: src/outcome/error.rs ErrorKind::is_domain().\n\
Common causes: Validation missing from the domain match arm.\n\
Run: cargo test --test store_properties error_kind_is_domain"
);
assert!(
ErrorKind::PolicyRejection.is_domain(),
"PROPERTY: ErrorKind::PolicyRejection must be classified as a domain error.\n\
Investigate: src/outcome/error.rs ErrorKind::is_domain().\n\
Common causes: PolicyRejection missing from the domain match arm, or \
grouped with operational errors.\n\
Run: cargo test --test store_properties error_kind_is_domain"
);
assert!(
!ErrorKind::StorageError.is_domain(),
"PROPERTY: ErrorKind::StorageError must NOT be classified as a domain error.\n\
Investigate: src/outcome/error.rs ErrorKind::is_domain().\n\
Common causes: StorageError incorrectly placed in the domain match arm, or \
wildcard arm returning true.\n\
Run: cargo test --test store_properties error_kind_is_domain"
);
assert!(
!ErrorKind::Timeout.is_domain(),
"PROPERTY: ErrorKind::Timeout must NOT be classified as a domain error.\n\
Investigate: src/outcome/error.rs ErrorKind::is_domain().\n\
Common causes: Timeout incorrectly placed in the domain match arm.\n\
Run: cargo test --test store_properties error_kind_is_domain"
);
assert!(
!ErrorKind::Internal.is_domain(),
"PROPERTY: ErrorKind::Internal must NOT be classified as a domain error.\n\
Investigate: src/outcome/error.rs ErrorKind::is_domain().\n\
Common causes: Internal incorrectly placed in the domain match arm, or \
wildcard arm returning true.\n\
Run: cargo test --test store_properties error_kind_is_domain"
);
assert!(
!ErrorKind::Custom(99).is_domain(),
"PROPERTY: ErrorKind::Custom must NOT be classified as a domain error by default.\n\
Investigate: src/outcome/error.rs ErrorKind::is_domain().\n\
Common causes: Custom variant handled by a wildcard arm that returns true.\n\
Run: cargo test --test store_properties error_kind_is_domain"
);
}
#[test]
fn error_kind_is_operational() {
assert!(
ErrorKind::StorageError.is_operational(),
"PROPERTY: ErrorKind::StorageError must be classified as operational.\n\
Investigate: src/outcome/error.rs ErrorKind::is_operational().\n\
Common causes: StorageError missing from the operational match arm, or \
is_operational() not including infrastructure errors.\n\
Run: cargo test --test store_properties error_kind_is_operational"
);
assert!(
ErrorKind::Timeout.is_operational(),
"PROPERTY: ErrorKind::Timeout must be classified as operational.\n\
Investigate: src/outcome/error.rs ErrorKind::is_operational().\n\
Common causes: Timeout missing from the operational match arm.\n\
Run: cargo test --test store_properties error_kind_is_operational"
);
assert!(
ErrorKind::Serialization.is_operational(),
"PROPERTY: ErrorKind::Serialization must be classified as operational.\n\
Investigate: src/outcome/error.rs ErrorKind::is_operational().\n\
Common causes: Serialization missing from the operational match arm, or \
grouped with domain errors.\n\
Run: cargo test --test store_properties error_kind_is_operational"
);
assert!(
ErrorKind::Internal.is_operational(),
"PROPERTY: ErrorKind::Internal must be classified as operational.\n\
Investigate: src/outcome/error.rs ErrorKind::is_operational().\n\
Common causes: Internal missing from the operational match arm.\n\
Run: cargo test --test store_properties error_kind_is_operational"
);
assert!(
!ErrorKind::NotFound.is_operational(),
"PROPERTY: ErrorKind::NotFound must NOT be classified as operational (it is a domain error).\n\
Investigate: src/outcome/error.rs ErrorKind::is_operational().\n\
Common causes: is_operational() wildcard arm returning true for domain errors.\n\
Run: cargo test --test store_properties error_kind_is_operational"
);
assert!(
!ErrorKind::Conflict.is_operational(),
"PROPERTY: ErrorKind::Conflict must NOT be classified as operational (it is a domain error).\n\
Investigate: src/outcome/error.rs ErrorKind::is_operational().\n\
Common causes: Conflict incorrectly placed in the operational match arm.\n\
Run: cargo test --test store_properties error_kind_is_operational"
);
assert!(
!ErrorKind::Custom(99).is_operational(),
"PROPERTY: ErrorKind::Custom must NOT be classified as operational by default.\n\
Investigate: src/outcome/error.rs ErrorKind::is_operational().\n\
Common causes: Custom variant matched by wildcard arm that returns true.\n\
Run: cargo test --test store_properties error_kind_is_operational"
);
}
#[test]
fn error_kind_is_retryable() {
assert!(
ErrorKind::StorageError.is_retryable(),
"PROPERTY: ErrorKind::StorageError must be classified as retryable.\n\
Investigate: src/outcome/error.rs ErrorKind::is_retryable().\n\
Common causes: StorageError missing from the retryable match arm, or \
is_retryable() returning false by default.\n\
Run: cargo test --test store_properties error_kind_is_retryable"
);
assert!(
ErrorKind::Timeout.is_retryable(),
"PROPERTY: ErrorKind::Timeout must be classified as retryable.\n\
Investigate: src/outcome/error.rs ErrorKind::is_retryable().\n\
Common causes: Timeout missing from the retryable match arm, or \
Timeout placed in the non-retryable group by mistake.\n\
Run: cargo test --test store_properties error_kind_is_retryable"
);
assert!(
!ErrorKind::NotFound.is_retryable(),
"PROPERTY: ErrorKind::NotFound must NOT be retryable (domain error, not transient).\n\
Investigate: src/outcome/error.rs ErrorKind::is_retryable().\n\
Common causes: NotFound grouped with operational errors in the retryable arm.\n\
Run: cargo test --test store_properties error_kind_is_retryable"
);
assert!(
!ErrorKind::Conflict.is_retryable(),
"PROPERTY: ErrorKind::Conflict must NOT be retryable (requires resolution, not retry).\n\
Investigate: src/outcome/error.rs ErrorKind::is_retryable().\n\
Common causes: Conflict grouped with transient errors, or is_retryable() \
treating all non-domain errors as retryable.\n\
Run: cargo test --test store_properties error_kind_is_retryable"
);
assert!(
!ErrorKind::Internal.is_retryable(),
"PROPERTY: ErrorKind::Internal must NOT be retryable (programming error, not transient).\n\
Investigate: src/outcome/error.rs ErrorKind::is_retryable().\n\
Common causes: Internal grouped with operational transients by mistake.\n\
Run: cargo test --test store_properties error_kind_is_retryable"
);
assert!(
!ErrorKind::Custom(99).is_retryable(),
"PROPERTY: ErrorKind::Custom must NOT be retryable by default.\n\
Investigate: src/outcome/error.rs ErrorKind::is_retryable().\n\
Common causes: Custom variant handled by a wildcard arm that returns true, or \
Custom not having an explicit non-retryable arm.\n\
Run: cargo test --test store_properties error_kind_is_retryable"
);
}
#[test]
fn append_options_with_idempotency_builder() {
let opts = AppendOptions::new().with_idempotency(0xDEAD_BEEF_CAFE_BABE);
assert_eq!(
opts.idempotency_key,
Some(0xDEAD_BEEF_CAFE_BABE),
"PROPERTY: with_idempotency(key) must set idempotency_key to Some(key).\n\
Investigate: src/store/mod.rs AppendOptions::with_idempotency.\n\
Common causes: builder returning Self without setting idempotency_key."
);
assert!(
opts.expected_sequence.is_none(),
"unset fields must remain None"
);
assert_eq!(opts.flags, 0, "unset flags must remain 0");
}
#[test]
fn append_options_with_cas_builder() {
let opts = AppendOptions::new().with_cas(7);
assert_eq!(
opts.expected_sequence,
Some(7),
"PROPERTY: with_cas(seq) must set expected_sequence to Some(seq).\n\
Investigate: src/store/mod.rs AppendOptions::with_cas.\n\
Common causes: method setting wrong field, or returning Self unchanged."
);
assert!(
opts.idempotency_key.is_none(),
"unset fields must remain None"
);
}
#[test]
fn append_options_with_flags_builder() {
let opts = AppendOptions::new().with_flags(0x03);
assert_eq!(
opts.flags, 0x03,
"PROPERTY: with_flags(f) must set flags to f.\n\
Investigate: src/store/mod.rs AppendOptions::with_flags.\n\
Common causes: flags field not updated, or OR'd with previous value."
);
assert!(
opts.expected_sequence.is_none(),
"unset fields must remain None"
);
assert!(
opts.idempotency_key.is_none(),
"unset fields must remain None"
);
}
#[test]
fn append_options_with_correlation_builder() {
let opts = AppendOptions::new().with_correlation(0xCAFE_BABE_1234_5678);
assert_eq!(
opts.correlation_id,
Some(0xCAFE_BABE_1234_5678),
"PROPERTY: with_correlation(id) must set correlation_id to Some(id).\n\
Investigate: src/store/mod.rs AppendOptions::with_correlation.\n\
Common causes: method writing to causation_id by mistake."
);
assert!(opts.causation_id.is_none(), "causation_id must not be set");
}
#[test]
fn append_options_with_causation_builder() {
let opts = AppendOptions::new().with_causation(0xABCD_EF01_2345_6789);
assert_eq!(
opts.causation_id,
Some(0xABCD_EF01_2345_6789),
"PROPERTY: with_causation(id) must set causation_id to Some(id).\n\
Investigate: src/store/mod.rs AppendOptions::with_causation.\n\
Common causes: method writing to correlation_id by mistake."
);
assert!(
opts.correlation_id.is_none(),
"correlation_id must not be set"
);
}
#[test]
fn append_options_builder_chain() {
let opts = AppendOptions::new()
.with_idempotency(1)
.with_cas(5)
.with_flags(0x01)
.with_correlation(2)
.with_causation(3);
assert_eq!(opts.idempotency_key, Some(1));
assert_eq!(opts.expected_sequence, Some(5));
assert_eq!(opts.flags, 0x01);
assert_eq!(opts.correlation_id, Some(2));
assert_eq!(opts.causation_id, Some(3));
}