use std::path::PathBuf;
use tempfile::TempDir;
use mati_core::store::db::Store;
use mati_core::store::enforcement::{
canonicalize_file_key, scan_enforcement_events, EnforcementEventType, EnforcementEventWriter,
EnforcementMode, GapCause, GapCertainty, MissedEventCount, SeqAllocator, SubjectKind,
SCHEMA_VERSION,
};
async fn temp_store() -> (TempDir, Store) {
let dir = TempDir::new().expect("tempdir");
let store = Store::open(dir.path()).await.expect("open store");
(dir, store)
}
#[tokio::test]
async fn concurrent_writes_produce_unique_seq_nos() {
let (_dir, store) = temp_store().await;
let mut writer = EnforcementEventWriter::new(&store)
.await
.expect("create writer");
let mut seq_nos = Vec::with_capacity(100);
for _ in 0..100 {
let event = writer
.write(
&store,
EnforcementEventType::Deny,
SubjectKind::File,
"file:src/test.rs".to_string(),
"claude".to_string(),
None,
"gotcha_above_threshold".to_string(),
None,
)
.await
.expect("write event");
seq_nos.push(event.seq_no);
}
let mut deduped = seq_nos.clone();
deduped.sort();
deduped.dedup();
assert_eq!(
deduped.len(),
100,
"all 100 seq_nos must be unique, got {} unique",
deduped.len()
);
for window in seq_nos.windows(2) {
assert!(
window[0] < window[1],
"seq_nos must be strictly ascending: {} >= {}",
window[0],
window[1]
);
}
}
#[tokio::test]
async fn seq_gap_after_simulated_crash() {
let (_dir, store) = temp_store().await;
let mut writer = EnforcementEventWriter::new(&store)
.await
.expect("create writer");
let event1 = writer
.write(
&store,
EnforcementEventType::Deny,
SubjectKind::File,
"file:src/a.rs".to_string(),
"claude".to_string(),
None,
"gotcha_above_threshold".to_string(),
None,
)
.await
.expect("write event 1");
assert_eq!(event1.seq_no, 1);
store
.put_raw("enforcement:seq", &5u64.to_be_bytes())
.await
.expect("advance seq");
let mut writer2 = EnforcementEventWriter::new(&store)
.await
.expect("create writer after crash");
assert_eq!(writer2.current_seq(), 5);
let event2 = writer2
.write(
&store,
EnforcementEventType::Deny,
SubjectKind::File,
"file:src/b.rs".to_string(),
"claude".to_string(),
None,
"gotcha_above_threshold".to_string(),
None,
)
.await
.expect("write event after crash");
assert_eq!(event2.seq_no, 6);
let all_events = scan_enforcement_events(&store, 1, 10).await.expect("scan");
assert_eq!(all_events.len(), 2, "only 2 events should exist");
assert_eq!(all_events[0].seq_no, 1);
assert_eq!(all_events[1].seq_no, 6);
assert_eq!(event2.prev_hash, event1.event_hash);
}
#[tokio::test]
async fn export_snapshot_consistency() {
let (_dir, store) = temp_store().await;
let mut writer = EnforcementEventWriter::new(&store)
.await
.expect("create writer");
for i in 0..50 {
writer
.write(
&store,
EnforcementEventType::Deny,
SubjectKind::File,
format!("file:src/file_{i}.rs"),
"claude".to_string(),
None,
"gotcha_above_threshold".to_string(),
None,
)
.await
.expect("write event");
}
let snapshot = scan_enforcement_events(&store, 1, 50)
.await
.expect("snapshot scan");
assert_eq!(
snapshot.len(),
50,
"snapshot must contain exactly 50 events"
);
for i in 50..60 {
writer
.write(
&store,
EnforcementEventType::Deny,
SubjectKind::File,
format!("file:src/file_{i}.rs"),
"claude".to_string(),
None,
"gotcha_above_threshold".to_string(),
None,
)
.await
.expect("write event");
}
let snapshot_again = scan_enforcement_events(&store, 1, 50)
.await
.expect("snapshot scan again");
assert_eq!(snapshot_again.len(), 50);
let full = scan_enforcement_events(&store, 1, 60)
.await
.expect("full scan");
assert_eq!(full.len(), 60);
}
#[tokio::test]
async fn prune_does_not_corrupt_concurrent_export() {
let (_dir, store) = temp_store().await;
let mut writer = EnforcementEventWriter::new(&store)
.await
.expect("create writer");
for i in 0..100 {
writer
.write(
&store,
EnforcementEventType::Deny,
SubjectKind::File,
format!("file:src/file_{i}.rs"),
"claude".to_string(),
None,
"gotcha_above_threshold".to_string(),
None,
)
.await
.expect("write event");
}
for seq in 1..50 {
let key = format!("enforcement:event:{:020}", seq);
store.delete(&key).await.expect("delete pruned event");
}
let remaining = scan_enforcement_events(&store, 1, 100)
.await
.expect("scan after prune");
assert_eq!(remaining.len(), 51);
assert_eq!(remaining[0].seq_no, 50);
assert_eq!(remaining[50].seq_no, 100);
for event in &remaining {
let recomputed = event.compute_hash();
assert_eq!(
event.event_hash, recomputed,
"event {} has corrupted hash",
event.seq_no
);
}
}
#[tokio::test]
async fn gap_detection_on_recovery() {
let (_dir, store) = temp_store().await;
let mut writer = EnforcementEventWriter::new(&store)
.await
.expect("create writer");
let event1 = writer
.write(
&store,
EnforcementEventType::Deny,
SubjectKind::File,
"file:src/main.rs".to_string(),
"claude".to_string(),
None,
"gotcha_above_threshold".to_string(),
None,
)
.await
.expect("write event 1");
assert_eq!(event1.seq_no, 1);
store
.put_raw("enforcement:seq", &3u64.to_be_bytes())
.await
.expect("advance seq to simulate crash");
let mut writer2 = EnforcementEventWriter::new(&store)
.await
.expect("create writer for recovery");
let gap_event = writer2
.detect_and_record_gap(
&store,
1700000000000,
1700000060000,
GapCause::DaemonUnreachable,
)
.await
.expect("record gap");
assert_eq!(gap_event.seq_no, 4); assert_eq!(gap_event.subject_kind, SubjectKind::System);
assert_eq!(gap_event.decision_reason_code, "recording_gap_detected");
match &gap_event.event_type {
EnforcementEventType::RecordingGap {
gap_start_ms,
gap_end_ms,
cause,
enforcement_mode_during_gap,
missed_event_count,
certainty,
} => {
assert_eq!(*gap_start_ms, 1700000000000);
assert_eq!(*gap_end_ms, 1700000060000);
assert_eq!(*cause, GapCause::DaemonUnreachable);
assert_eq!(*enforcement_mode_during_gap, EnforcementMode::Advisory);
assert_eq!(*missed_event_count, MissedEventCount::Unknown);
assert_eq!(*certainty, GapCertainty::Inferred);
}
other => panic!("expected RecordingGap, got {:?}", other),
}
assert_eq!(gap_event.prev_hash, event1.event_hash);
}
#[tokio::test]
async fn strict_mode_blocks_on_write_failure() {
let dir = TempDir::new().expect("tempdir");
let store = Store::open(dir.path()).await.expect("open store");
let mut writer = EnforcementEventWriter::new(&store)
.await
.expect("create writer");
let event = writer
.write(
&store,
EnforcementEventType::Deny,
SubjectKind::File,
"file:src/test.rs".to_string(),
"claude".to_string(),
None,
"gotcha_above_threshold".to_string(),
None,
)
.await
.expect("first write succeeds");
assert_eq!(event.seq_no, 1);
assert_eq!(event.schema_version, SCHEMA_VERSION);
let loaded = scan_enforcement_events(&store, 1, 1).await.expect("scan");
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].event_hash, event.event_hash);
let seq2 = SeqAllocator::load(&store).await;
assert_eq!(seq2.current(), 1, "seq must be persisted durably");
}
#[test]
fn canonical_path_aliasing_produces_same_key() {
let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let paths = [
"src/store/enforcement.rs",
"./src/store/enforcement.rs",
"src/store/../store/enforcement.rs",
"src/./store/enforcement.rs",
];
let canonical_keys: Vec<String> = paths
.iter()
.map(|p| canonicalize_file_key(p, &repo_root))
.collect();
for (i, key) in canonical_keys.iter().enumerate() {
assert_eq!(
key, &canonical_keys[0],
"Path '{}' produced different key '{}' vs '{}'",
paths[i], key, canonical_keys[0]
);
}
let expected = if cfg!(target_os = "macos") || cfg!(target_os = "windows") {
"src/store/enforcement.rs".to_lowercase()
} else {
"src/store/enforcement.rs".to_string()
};
assert_eq!(canonical_keys[0], expected);
}
#[tokio::test]
async fn full_enforcement_lifecycle_records_all_events() {
use mati_core::store::enforcement::record_event;
let (_dir, store) = temp_store().await;
let deny_result = record_event(
&store,
EnforcementEventType::Deny,
SubjectKind::File,
"file:src/billing/charges.rs".to_string(),
"claude".to_string(),
None,
"gotcha_above_threshold".to_string(),
Some("basis_hash_1".to_string()),
)
.await
.expect("record deny");
assert!(deny_result.is_some());
let deny_event = deny_result.unwrap();
let receipt_result = record_event(
&store,
EnforcementEventType::ReceiptMinted,
SubjectKind::File,
"file:src/billing/charges.rs".to_string(),
"claude".to_string(),
Some("receipt-001".to_string()),
"consultation_requested".to_string(),
None,
)
.await
.expect("record receipt");
assert!(receipt_result.is_some());
let receipt_event = receipt_result.unwrap();
let allow_result = record_event(
&store,
EnforcementEventType::AllowAfterReceipt,
SubjectKind::File,
"file:src/billing/charges.rs".to_string(),
"claude".to_string(),
Some("receipt-001".to_string()),
"receipt_valid".to_string(),
None,
)
.await
.expect("record allow");
assert!(allow_result.is_some());
let allow_event = allow_result.unwrap();
let all = scan_enforcement_events(&store, 1, 10).await.expect("scan");
assert_eq!(all.len(), 3, "expected 3 events");
assert!(matches!(all[0].event_type, EnforcementEventType::Deny));
assert!(matches!(
all[1].event_type,
EnforcementEventType::ReceiptMinted
));
assert!(matches!(
all[2].event_type,
EnforcementEventType::AllowAfterReceipt
));
assert_eq!(deny_event.prev_hash, "", "first event has empty prev_hash");
assert_eq!(
receipt_event.prev_hash, deny_event.event_hash,
"receipt's prev_hash must equal deny's event_hash"
);
assert_eq!(
allow_event.prev_hash, receipt_event.event_hash,
"allow's prev_hash must equal receipt's event_hash"
);
for event in &all {
assert_eq!(event.event_hash, event.compute_hash());
}
}
#[tokio::test]
async fn gotcha_crud_records_control_changed_events() {
use mati_core::store::enforcement::ControlChangeKind;
use mati_core::store::gotcha_ops::{
apply_gotcha_confirm, apply_gotcha_tombstone, apply_gotcha_write,
};
use mati_core::store::record::{
Category, ConfidenceScore, GotchaRecord, Priority, QualityScore, Record, RecordLifecycle,
RecordSource, RecordVersion, StalenessScore,
};
let (_dir, store) = temp_store().await;
let make_gotcha = |key: &str, confirmed: bool| -> Record {
let gotcha = GotchaRecord {
rule: "test rule".into(),
reason: "test reason".into(),
severity: Priority::High,
affected_files: vec!["src/test.rs".into()],
ref_url: None,
discovered_session: 1_000_000,
confirmed,
};
Record {
key: key.to_string(),
value: "test rule because test reason".into(),
payload: serde_json::to_value(&gotcha).ok(),
category: Category::Gotcha,
priority: Priority::High,
tags: vec![],
created_at: 1_000_000,
updated_at: 1_000_000,
ref_url: None,
staleness: StalenessScore::fresh(),
lifecycle: RecordLifecycle::Active,
version: RecordVersion {
device_id: uuid::Uuid::new_v4(),
logical_clock: 1,
wall_clock: 1_000_000,
},
quality: QualityScore::layer0_default(),
access_count: 0,
last_accessed: 0,
source: RecordSource::DeveloperManual,
confidence: ConfidenceScore::for_new_record(&RecordSource::DeveloperManual),
gap_analysis_score: 0.0,
}
};
let record = make_gotcha("gotcha:test-crud", false);
apply_gotcha_write(&store, &record, &[], &["src/test.rs".into()], true)
.await
.expect("create gotcha");
let confirmed_record = make_gotcha("gotcha:test-crud", true);
apply_gotcha_confirm(&store, &confirmed_record, &["src/test.rs".into()])
.await
.expect("confirm gotcha");
let mut record2 = make_gotcha("gotcha:test-crud", true);
record2.value = "updated rule".into();
apply_gotcha_write(
&store,
&record2,
&["src/test.rs".into()],
&["src/test.rs".into()],
false,
)
.await
.expect("update gotcha");
apply_gotcha_tombstone(&store, "gotcha:test-crud", &["src/test.rs".into()])
.await
.expect("delete gotcha");
let events = scan_enforcement_events(&store, 1, 100).await.expect("scan");
let control_events: Vec<_> = events
.iter()
.filter(|e| matches!(e.event_type, EnforcementEventType::ControlChanged { .. }))
.collect();
assert_eq!(
control_events.len(),
4,
"expected exactly 4 ControlChanged events (Created, Confirmed, Updated, Deleted), got {}: {:?}",
control_events.len(),
control_events
.iter()
.map(|e| &e.event_type)
.collect::<Vec<_>>()
);
let kinds: Vec<ControlChangeKind> = control_events
.iter()
.map(|e| match &e.event_type {
EnforcementEventType::ControlChanged { change_kind } => *change_kind,
_ => unreachable!(),
})
.collect();
assert_eq!(
kinds,
vec![
ControlChangeKind::Created,
ControlChangeKind::Confirmed,
ControlChangeKind::Updated,
ControlChangeKind::Deleted,
],
"ControlChanged events must fire in CRUD order"
);
let reason_codes: Vec<&str> = control_events
.iter()
.map(|e| e.decision_reason_code.as_str())
.collect();
assert_eq!(
reason_codes,
vec![
"control_created",
"control_confirmed",
"control_updated",
"control_deleted",
],
);
for e in &control_events {
assert_eq!(e.subject_key, "gotcha:test-crud");
assert_eq!(e.subject_kind, SubjectKind::Control);
assert_eq!(e.agent_type, "developer");
}
for window in control_events.windows(2) {
assert!(
window[1].seq_no > window[0].seq_no,
"seq_no must be strictly increasing: {} → {}",
window[0].seq_no,
window[1].seq_no
);
assert_eq!(
window[1].prev_hash, window[0].event_hash,
"prev_hash must chain from previous event_hash"
);
}
for e in &control_events {
assert_eq!(
e.event_hash,
e.compute_hash(),
"stored event_hash must match canonical re-computation"
);
}
}
#[tokio::test]
async fn retention_prunes_old_events_and_records_prune_event() {
use mati_core::store::enforcement::{enforce_retention, set_retention_days, PruneResult};
let (_dir, store) = temp_store().await;
set_retention_days(&store, 1).await.expect("set retention");
let old_ms = {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
now.saturating_sub(400 * 86_400_000) };
let mut writer = EnforcementEventWriter::new(&store)
.await
.expect("create writer");
for i in 0..5 {
writer
.write(
&store,
EnforcementEventType::Deny,
SubjectKind::File,
format!("file:old_{i}.rs"),
"claude".to_string(),
None,
"gotcha_above_threshold".to_string(),
None,
)
.await
.expect("write old event");
}
for seq in 1..=5u64 {
let key = format!("enforcement:event:{:020}", seq);
if let Ok(Some(bytes)) = store.get_raw_bytes(&key).await {
if let Ok(mut event) =
serde_json::from_slice::<mati_core::store::enforcement::EnforcementEvent>(&bytes)
{
event.recorded_at_ms = old_ms;
let patched = serde_json::to_vec(&event).unwrap();
store.put_raw(&key, &patched).await.unwrap();
}
}
}
for i in 0..3 {
writer
.write(
&store,
EnforcementEventType::AllowAfterReceipt,
SubjectKind::File,
format!("file:new_{i}.rs"),
"claude".to_string(),
None,
"receipt_valid".to_string(),
None,
)
.await
.expect("write new event");
}
let result = enforce_retention(&store).await.expect("enforce retention");
match result {
PruneResult::Pruned {
count,
oldest_seq,
newest_seq,
} => {
assert_eq!(count, 5, "should prune 5 old events");
assert_eq!(oldest_seq, 1);
assert_eq!(newest_seq, 5);
}
PruneResult::NothingToPrune => panic!("expected pruning to happen"),
}
let remaining = scan_enforcement_events(&store, 1, 100).await.expect("scan");
assert_eq!(
remaining.len(),
4,
"expected 4 events (3 recent + 1 prune record)"
);
let prune_events: Vec<_> = remaining
.iter()
.filter(|e| matches!(e.event_type, EnforcementEventType::RetentionPruned { .. }))
.collect();
assert_eq!(prune_events.len(), 1);
if let EnforcementEventType::RetentionPruned {
pruned_count,
oldest_pruned_seq,
newest_pruned_seq,
} = &prune_events[0].event_type
{
assert_eq!(*pruned_count, 5);
assert_eq!(*oldest_pruned_seq, 1);
assert_eq!(*newest_pruned_seq, 5);
}
}
#[tokio::test]
async fn strict_mode_enforcement_config_records_change_event() {
use mati_core::store::enforcement::{get_enforcement_mode, record_event, set_enforcement_mode};
let (_dir, store) = temp_store().await;
let mode = get_enforcement_mode(&store).await;
assert_eq!(mode, EnforcementMode::Advisory);
let old = set_enforcement_mode(&store, EnforcementMode::Strict)
.await
.expect("set strict");
assert_eq!(old, EnforcementMode::Advisory);
let mode = get_enforcement_mode(&store).await;
assert_eq!(mode, EnforcementMode::Strict);
let old = set_enforcement_mode(&store, EnforcementMode::Advisory)
.await
.expect("set advisory");
assert_eq!(old, EnforcementMode::Strict);
let events = scan_enforcement_events(&store, 1, 100).await.expect("scan");
let config_events: Vec<_> = events
.iter()
.filter(|e| {
matches!(
e.event_type,
EnforcementEventType::EnforcementConfigChanged { .. }
)
})
.collect();
assert_eq!(
config_events.len(),
2,
"expected 2 config change events (advisory→strict, strict→advisory)"
);
if let EnforcementEventType::EnforcementConfigChanged {
setting,
old_value,
new_value,
} = &config_events[0].event_type
{
assert_eq!(setting, "audit.write_durability");
assert_eq!(old_value, "best_effort");
assert_eq!(new_value, "strict");
}
if let EnforcementEventType::EnforcementConfigChanged {
setting,
old_value,
new_value,
} = &config_events[1].event_type
{
assert_eq!(setting, "audit.write_durability");
assert_eq!(old_value, "strict");
assert_eq!(new_value, "best_effort");
}
set_enforcement_mode(&store, EnforcementMode::Strict)
.await
.expect("set strict again");
let result = record_event(
&store,
EnforcementEventType::Deny,
SubjectKind::File,
"file:test.rs".to_string(),
"claude".to_string(),
None,
"gotcha_above_threshold".to_string(),
None,
)
.await;
assert!(result.is_ok(), "strict mode write should succeed");
assert!(result.unwrap().is_some());
}
#[tokio::test]
async fn record_event_with_session_attributes_event_v2() {
use mati_core::store::enforcement::record_event_with_session;
let (_dir, store) = temp_store().await;
record_event_with_session(
&store,
EnforcementEventType::Deny,
SubjectKind::File,
"file:src/pay.rs".to_string(),
"claude".to_string(),
None,
"edit_blocked_unconsulted".to_string(),
None,
Some("sess-xyz".to_string()),
)
.await
.expect("record");
let events = scan_enforcement_events(&store, 1, 100).await.expect("scan");
let ev = events
.iter()
.find(|e| matches!(e.event_type, EnforcementEventType::Deny))
.expect("deny event recorded");
assert_eq!(
ev.agent_session.as_deref(),
Some("sess-xyz"),
"the agent session must be recorded on the event"
);
assert_eq!(ev.schema_version, 2, "new events are schema_version 2");
assert_eq!(
ev.event_hash,
ev.compute_hash(),
"v2 event must self-verify"
);
}
#[tokio::test]
async fn config_enforcement_mode_round_trips() {
use mati_core::store::enforcement::{get_enforcement_mode, set_enforcement_mode};
let (_dir, store) = temp_store().await;
assert_eq!(
get_enforcement_mode(&store).await,
EnforcementMode::Advisory
);
set_enforcement_mode(&store, EnforcementMode::Strict)
.await
.expect("set strict");
assert_eq!(get_enforcement_mode(&store).await, EnforcementMode::Strict);
set_enforcement_mode(&store, EnforcementMode::Advisory)
.await
.expect("set advisory");
assert_eq!(
get_enforcement_mode(&store).await,
EnforcementMode::Advisory
);
use mati_core::store::enforcement::{get_retention_days, set_retention_days};
assert_eq!(get_retention_days(&store).await, 365); set_retention_days(&store, 90).await.expect("set 90");
assert_eq!(get_retention_days(&store).await, 90);
}
#[tokio::test]
async fn config_set_enforcement_mode_records_event() {
use mati_core::store::enforcement::set_enforcement_mode;
let (_dir, store) = temp_store().await;
set_enforcement_mode(&store, EnforcementMode::Strict)
.await
.expect("set strict");
let events = scan_enforcement_events(&store, 1, 100).await.expect("scan");
let config_events: Vec<_> = events
.iter()
.filter(|e| {
matches!(
e.event_type,
EnforcementEventType::EnforcementConfigChanged { .. }
)
})
.collect();
assert_eq!(config_events.len(), 1, "expected 1 config change event");
if let EnforcementEventType::EnforcementConfigChanged {
setting,
old_value,
new_value,
} = &config_events[0].event_type
{
assert_eq!(setting, "audit.write_durability");
assert_eq!(old_value, "best_effort");
assert_eq!(new_value, "strict");
} else {
panic!("expected EnforcementConfigChanged");
}
set_enforcement_mode(&store, EnforcementMode::Strict)
.await
.expect("set strict again");
let events2 = scan_enforcement_events(&store, 1, 100).await.expect("scan");
let config_events2: Vec<_> = events2
.iter()
.filter(|e| {
matches!(
e.event_type,
EnforcementEventType::EnforcementConfigChanged { .. }
)
})
.collect();
assert_eq!(
config_events2.len(),
1,
"setting same mode should not record another event"
);
}
#[tokio::test]
async fn derived_metrics_from_real_store_roundtrip() {
use mati_core::store::enforcement::{
aggregate_event_counts, derive_enforcement_metrics, record_event_with_session,
scan_events_since,
};
use mati_core::store::session::CONSULTED_RECENT_TTL_SECS;
let (_dir, store) = temp_store().await;
let denials = [
("file:src/a.rs", "sessA"),
("file:src/b.rs", "sessA"),
("file:src/c.rs", "sessB"),
];
for (subject, session) in denials {
record_event_with_session(
&store,
EnforcementEventType::Deny,
SubjectKind::File,
subject.to_string(),
"claude".to_string(),
None,
"gotcha_above_threshold".to_string(),
None,
Some(session.to_string()),
)
.await
.expect("record sessioned deny");
}
for subject in ["file:src/a.rs", "file:src/b.rs", "file:src/c.rs"] {
record_event_with_session(
&store,
EnforcementEventType::ReceiptMinted,
SubjectKind::File,
subject.to_string(),
"claude".to_string(),
None,
"consultation_requested".to_string(),
None,
None,
)
.await
.expect("record receipt");
}
let events = scan_events_since(&store, 0).await.expect("scan events");
let counts = aggregate_event_counts(&events);
let derived = derive_enforcement_metrics(&events);
assert_eq!(counts.denials, 3, "three denials recorded");
assert_eq!(counts.receipts_minted, 3, "three receipts recorded");
assert_eq!(derived.blocked_sessions, 2, "sessA + sessB");
assert_eq!(derived.attributed_denials, 3);
assert_eq!(
derived.blocks_per_session,
Some(1.5),
"3 denials / 2 sessions"
);
assert_eq!(derived.consult_pairs, 3, "one consult pair per subject");
let median = derived
.median_time_to_consult_ms
.expect("median exists once pairs are found");
assert!(
median <= CONSULTED_RECENT_TTL_SECS * 1_000,
"real-time deltas must fall inside the consult window (got {median}ms)"
);
}
#[tokio::test]
async fn gotcha_add_on_unindexed_file_wires_read_gate() {
use mati_core::store::gotcha_ops::apply_gotcha_write;
use mati_core::store::record::{
Category, ConfidenceScore, FileRecord, GotchaRecord, Priority, QualityScore, Record,
RecordLifecycle, RecordSource, RecordVersion, StalenessScore,
};
let (_dir, store) = temp_store().await;
let file = "src/never_indexed_by_init.rs";
let gkey = "gotcha:never-bypass-the-gate";
let gotcha = GotchaRecord {
rule: "Never bypass the gate".into(),
reason: "It is load-bearing".into(),
severity: Priority::Critical,
affected_files: vec![file.into()],
ref_url: None,
discovered_session: 1_000_000,
confirmed: true,
};
let record = Record {
key: gkey.into(),
value: "Never bypass the gate because it is load-bearing".into(),
payload: serde_json::to_value(&gotcha).ok(),
category: Category::Gotcha,
priority: Priority::Critical,
tags: vec![],
created_at: 1_000_000,
updated_at: 1_000_000,
ref_url: None,
staleness: StalenessScore::fresh(),
lifecycle: RecordLifecycle::Active,
version: RecordVersion {
device_id: uuid::Uuid::new_v4(),
logical_clock: 1,
wall_clock: 1_000_000,
},
quality: QualityScore::developer_entry_default(),
access_count: 0,
last_accessed: 0,
source: RecordSource::DeveloperManual,
confidence: ConfidenceScore::for_new_record(&RecordSource::DeveloperManual),
gap_analysis_score: 0.0,
};
apply_gotcha_write(&store, &record, &[], &[file.to_string()], true)
.await
.expect("write gotcha");
assert!(
store.get(gkey).await.expect("get gotcha").is_some(),
"gotcha record should exist"
);
let file_record = store
.get(&format!("file:{file}"))
.await
.expect("get file record")
.expect("gotcha add must create a file record so the read gate fires");
let fr: FileRecord = file_record
.payload_as()
.expect("file record payload is a FileRecord");
assert!(
fr.gotcha_keys.contains(&gkey.to_string()),
"the created file record must carry the gotcha key in gotcha_keys (got {:?})",
fr.gotcha_keys
);
assert!(matches!(
file_record.lifecycle,
mati_core::store::RecordLifecycle::Active
));
}