use crate::coordinate::{Coordinate, Region};
use crate::event::EventKind;
use crate::store::file_classification::KEYSET_FILENAME;
use crate::store::keyscope::KeyScopeGranularity;
use crate::store::sim::fs::{CrashOp, SimFs};
use crate::store::{AppendOptions, BatchAppendItem, CausationRef, Store, StoreConfig, StoreError};
use std::sync::Arc;
const KIND: EventKind = EventKind::custom(0xE, 0x22);
fn minting_batch(coord: &Coordinate, count: usize) -> Vec<BatchAppendItem> {
(0..count)
.map(|i| {
BatchAppendItem::new(
coord.clone(),
KIND,
&serde_json::json!({ "i": i }),
AppendOptions::default(),
CausationRef::None,
)
.expect("construct batch item")
})
.collect()
}
fn encrypted_config(dir: &std::path::Path, sim_fs: &Arc<SimFs>) -> StoreConfig {
StoreConfig::new(dir)
.with_payload_encryption(KeyScopeGranularity::PerEntity)
.with_fs(Arc::clone(sim_fs) as Arc<dyn crate::store::platform::fs::StoreFs>)
}
fn visible_user_events(store: &Store) -> usize {
store
.query(&Region::all())
.into_iter()
.filter(|entry| !entry.event_kind().is_reserved())
.count()
}
#[test]
fn a_faulted_keyset_fence_flush_fails_a_minting_batch_closed() {
{
let dir = tempfile::tempdir().expect("tmpdir");
let sim_fs = Arc::new(SimFs::new(0xBA7C_0001, 0));
let store =
Store::open(encrypted_config(dir.path(), &sim_fs)).expect("open encrypted store");
let coord = Coordinate::new("entity:batch-fence", "scope:mk2").expect("coord");
let receipts = store
.append_batch(minting_batch(&coord, 2))
.expect("an unfaulted minting batch commits");
assert_eq!(receipts.len(), 2, "both batch items are acked");
assert!(
dir.path().join(KEYSET_FILENAME).exists(),
"PROPERTY: the fence published the minted key's keyset BEFORE the batch acked"
);
assert_eq!(
visible_user_events(&store),
2,
"both committed batch events are visible"
);
store.close().expect("close control store");
}
let dir = tempfile::tempdir().expect("tmpdir");
let sim_fs = Arc::new(SimFs::new(0xBA7C_0002, 0));
let store = Store::open(encrypted_config(dir.path(), &sim_fs)).expect("open encrypted store");
let coord = Coordinate::new("entity:batch-fence", "scope:mk2").expect("coord");
sim_fs.arm_fault_on(CrashOp::PersistTemp, 1);
let err = store
.append_batch(minting_batch(&coord, 2))
.expect_err("PROPERTY: a minting batch whose fence-flush tears must fail closed");
assert!(
matches!(err, StoreError::Io(_)),
"PROPERTY: the torn fence publish must surface as StoreError::Io, got {err:?}"
);
assert_eq!(
visible_user_events(&store),
0,
"PROPERTY: the failed batch published NOTHING — no partial visibility"
);
assert!(
!dir.path().join(KEYSET_FILENAME).exists(),
"PROPERTY: the torn publish left no keyset file (the temp never landed)"
);
let receipts = store
.append_batch(minting_batch(&coord, 2))
.expect("PROPERTY: the retried batch re-raises the fence and commits");
assert_eq!(receipts.len(), 2, "the retried batch acks both items");
assert!(
dir.path().join(KEYSET_FILENAME).exists(),
"PROPERTY: the retried fence published the keyset durably"
);
store.close().expect("close faulted-then-recovered store");
}