Phase 5C (Index Persistence) implementation: - PersistentVectorIndex with hot/cold architecture - Hot: in-memory HNSW for recent vectors - Cold: memory-mapped HNSW loaded from disk - Background builder for WAL replay and atomic swap - BLAKE3 integrity verification - PersistentVisualIndex with checkpoint persistence - BkTreeSnapshot with rkyv serialization - CRC32C corruption detection - Atomic write pattern (temp → fsync → rename) - Key codec additions for vector index metadata - Split large files into modules (<500 lines each) - battery_pre_sentinel.rs → battery/ directory - visual_index.rs → visual_index/ directory - persistent.rs → persistent/ directory - Refactored ingest worker tests for clarity - Updated roadmap to mark Phase 5 complete Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
377 lines
14 KiB
Rust
377 lines
14 KiB
Rust
//! Battery 2: JWT Conflict Scenario.
|
|
//!
|
|
//! Tests escalation mechanisms and layered consensus with cross-tier disagreement.
|
|
//!
|
|
//! # Test Coverage
|
|
//!
|
|
//! | Test | Feature | Validates |
|
|
//! |------|---------|-----------|
|
|
//! | `test_jwt_conflict_escalation_fires` | Escalation | Conflict score threshold triggers event |
|
|
//! | `test_jwt_escalation_predicate_filter` | Escalation | Predicate pattern filtering |
|
|
//! | `test_jwt_layered_lens_tier_agreement` | Layered lens | Tier-by-tier resolution |
|
|
|
|
#![allow(clippy::expect_used)] // Test code uses expect() for clear failure messages
|
|
|
|
use super::helpers::*;
|
|
|
|
/// Test 2.1: Escalation event fires when conflict score exceeds threshold.
|
|
///
|
|
/// Setup:
|
|
/// - RFC 7519 (Tier 0, confidence 1.0): aud_validation = Boolean(true)
|
|
/// - Approved runbook (Tier 2, confidence 0.95): aud_validation = Boolean(true)
|
|
/// - Internal wiki (Tier 3, confidence 0.8): aud_validation = Boolean(false)
|
|
/// - Stack Overflow (Tier 5, confidence 0.6): aud_validation = Boolean(false)
|
|
///
|
|
/// Escalation policy: min_conflict_score=0.5, level=High, predicate_pattern=None
|
|
///
|
|
/// Proves the escalation mechanism correctly detects cross-tier disagreement
|
|
/// and records an event for external review.
|
|
#[tokio::test]
|
|
async fn test_jwt_conflict_escalation_fires() {
|
|
let dir = tempdir().expect("create temp dir");
|
|
let wal_dir = dir.path().join("wal");
|
|
let db_dir = dir.path().join("db");
|
|
|
|
let base_ts: u64 = 1_000_000;
|
|
|
|
// === Setup: Create 4 conflicting assertions ===
|
|
|
|
// RFC 7519 (Tier 0, confidence 1.0): aud_validation = Boolean(true)
|
|
let rfc_7519 = create_signed_assertion_with_source(
|
|
"JWT_aud_validation",
|
|
"aud_validation",
|
|
ObjectValue::Boolean(true),
|
|
SourceClass::Regulatory,
|
|
1.0,
|
|
base_ts,
|
|
);
|
|
|
|
// Approved runbook (Tier 2, confidence 0.95): aud_validation = Boolean(true)
|
|
let approved_runbook = create_signed_assertion_with_source(
|
|
"JWT_aud_validation",
|
|
"aud_validation",
|
|
ObjectValue::Boolean(true),
|
|
SourceClass::Observational,
|
|
0.95,
|
|
base_ts + 1,
|
|
);
|
|
|
|
// Internal wiki (Tier 3, confidence 0.8): aud_validation = Boolean(false)
|
|
let internal_wiki = create_signed_assertion_with_source(
|
|
"JWT_aud_validation",
|
|
"aud_validation",
|
|
ObjectValue::Boolean(false),
|
|
SourceClass::Expert,
|
|
0.8,
|
|
base_ts + 2,
|
|
);
|
|
|
|
// Stack Overflow (Tier 5, confidence 0.6): aud_validation = Boolean(false)
|
|
let stack_overflow = create_signed_assertion_with_source(
|
|
"JWT_aud_validation",
|
|
"aud_validation",
|
|
ObjectValue::Boolean(false),
|
|
SourceClass::Anecdotal,
|
|
0.6,
|
|
base_ts + 3,
|
|
);
|
|
|
|
// === Step 1: Write all 4 to WAL ===
|
|
let mut journal = Journal::open(&wal_dir).expect("open journal");
|
|
journal.append(serialize_assertion(&rfc_7519).expect("ser")).expect("append rfc");
|
|
journal.append(serialize_assertion(&approved_runbook).expect("ser")).expect("append runbook");
|
|
journal.append(serialize_assertion(&internal_wiki).expect("ser")).expect("append wiki");
|
|
journal
|
|
.append(serialize_assertion(&stack_overflow).expect("ser"))
|
|
.expect("append stackoverflow");
|
|
|
|
// === Step 2: Ingest all 4 via IngestWorker ===
|
|
let journal = Arc::new(Mutex::new(journal));
|
|
let store = Arc::new(HybridStore::open(&db_dir).expect("open store"));
|
|
|
|
let mut worker =
|
|
IngestWorker::new(journal.clone(), store.clone()).await.expect("create worker");
|
|
|
|
for _ in 0..4 {
|
|
let bytes = worker.step().await.expect("ingest step");
|
|
assert!(bytes > 0, "should process data from WAL");
|
|
}
|
|
|
|
// === Step 3: Configure escalation policy and materialize ===
|
|
let policy = EscalationPolicy {
|
|
name: "security-config".to_string(),
|
|
min_conflict_score: 0.5,
|
|
level: EscalationLevel::High,
|
|
predicate_pattern: None,
|
|
};
|
|
|
|
let escalation_store = Arc::new(GenericEscalationStore::new(store.clone()));
|
|
let lens = SyncLensWrapper(LayeredConsensusLens::new());
|
|
let materializer = Materializer::new(store.clone(), Box::new(lens))
|
|
.with_escalation(escalation_store.clone() as Arc<dyn EscalationStore>, vec![policy]);
|
|
|
|
let report = materializer.step().await.expect("materialize");
|
|
assert_eq!(report.views_updated, 1, "should update one view");
|
|
assert_eq!(report.escalations_triggered, 1, "should trigger one escalation");
|
|
|
|
// === Step 4: Verify escalation event ===
|
|
let pending = escalation_store.get_pending_escalations().await.expect("get pending");
|
|
assert_eq!(pending.len(), 1, "should have one pending escalation");
|
|
|
|
let event = &pending[0];
|
|
assert_eq!(event.subject, "JWT_aud_validation");
|
|
assert_eq!(event.predicate, "aud_validation");
|
|
assert_eq!(event.level, EscalationLevel::High);
|
|
assert!(
|
|
event.conflict_score >= 0.5,
|
|
"conflict_score should be >= 0.5, got {}",
|
|
event.conflict_score
|
|
);
|
|
assert!(!event.resolved, "escalation should not be resolved");
|
|
}
|
|
|
|
/// Test 2.2: Escalation predicate pattern filtering works correctly.
|
|
///
|
|
/// Two policies:
|
|
/// - Policy A: predicate_pattern=Some("aud"), triggers on "aud_validation"
|
|
/// - Policy B: predicate_pattern=Some("revenue"), does NOT trigger on "aud_validation"
|
|
///
|
|
/// Only Policy A should fire, creating a Critical-level escalation.
|
|
#[tokio::test]
|
|
async fn test_jwt_escalation_predicate_filter() {
|
|
let dir = tempdir().expect("create temp dir");
|
|
let wal_dir = dir.path().join("wal");
|
|
let db_dir = dir.path().join("db");
|
|
|
|
let base_ts: u64 = 1_000_000;
|
|
|
|
// Same four assertions as 2.1
|
|
let rfc_7519 = create_signed_assertion_with_source(
|
|
"JWT_aud_validation",
|
|
"aud_validation",
|
|
ObjectValue::Boolean(true),
|
|
SourceClass::Regulatory,
|
|
1.0,
|
|
base_ts,
|
|
);
|
|
|
|
let approved_runbook = create_signed_assertion_with_source(
|
|
"JWT_aud_validation",
|
|
"aud_validation",
|
|
ObjectValue::Boolean(true),
|
|
SourceClass::Observational,
|
|
0.95,
|
|
base_ts + 1,
|
|
);
|
|
|
|
let internal_wiki = create_signed_assertion_with_source(
|
|
"JWT_aud_validation",
|
|
"aud_validation",
|
|
ObjectValue::Boolean(false),
|
|
SourceClass::Expert,
|
|
0.8,
|
|
base_ts + 2,
|
|
);
|
|
|
|
let stack_overflow = create_signed_assertion_with_source(
|
|
"JWT_aud_validation",
|
|
"aud_validation",
|
|
ObjectValue::Boolean(false),
|
|
SourceClass::Anecdotal,
|
|
0.6,
|
|
base_ts + 3,
|
|
);
|
|
|
|
// Write to WAL and ingest
|
|
let mut journal = Journal::open(&wal_dir).expect("open journal");
|
|
journal.append(serialize_assertion(&rfc_7519).expect("ser")).expect("append");
|
|
journal.append(serialize_assertion(&approved_runbook).expect("ser")).expect("append");
|
|
journal.append(serialize_assertion(&internal_wiki).expect("ser")).expect("append");
|
|
journal.append(serialize_assertion(&stack_overflow).expect("ser")).expect("append");
|
|
|
|
let journal = Arc::new(Mutex::new(journal));
|
|
let store = Arc::new(HybridStore::open(&db_dir).expect("open store"));
|
|
|
|
let mut worker =
|
|
IngestWorker::new(journal.clone(), store.clone()).await.expect("create worker");
|
|
|
|
for _ in 0..4 {
|
|
worker.step().await.expect("ingest step");
|
|
}
|
|
|
|
// === Configure two policies ===
|
|
let policy_a = EscalationPolicy {
|
|
name: "policy-aud".to_string(),
|
|
min_conflict_score: 0.3,
|
|
level: EscalationLevel::Critical,
|
|
predicate_pattern: Some("aud".to_string()),
|
|
};
|
|
|
|
let policy_b = EscalationPolicy {
|
|
name: "policy-revenue".to_string(),
|
|
min_conflict_score: 0.3,
|
|
level: EscalationLevel::Medium,
|
|
predicate_pattern: Some("revenue".to_string()),
|
|
};
|
|
|
|
let escalation_store = Arc::new(GenericEscalationStore::new(store.clone()));
|
|
let lens = SyncLensWrapper(LayeredConsensusLens::new());
|
|
let materializer = Materializer::new(store.clone(), Box::new(lens)).with_escalation(
|
|
escalation_store.clone() as Arc<dyn EscalationStore>,
|
|
vec![policy_a, policy_b],
|
|
);
|
|
|
|
let report = materializer.step().await.expect("materialize");
|
|
assert_eq!(report.escalations_triggered, 1, "should trigger exactly one escalation");
|
|
|
|
// === Verify only Policy A fired ===
|
|
let pending = escalation_store.get_pending_escalations().await.expect("get pending");
|
|
assert_eq!(pending.len(), 1, "should have exactly one pending escalation");
|
|
|
|
let event = &pending[0];
|
|
assert_eq!(event.level, EscalationLevel::Critical, "should be Critical (Policy A)");
|
|
assert_eq!(event.predicate, "aud_validation");
|
|
assert!(event.reason.contains("policy-aud"), "reason should reference Policy A");
|
|
}
|
|
|
|
/// Test 2.3: Layered Consensus Lens shows tier-by-tier resolution.
|
|
///
|
|
/// With the JWT assertions:
|
|
/// - Tier 0 (Regulatory): Boolean(true) wins
|
|
/// - Tier 2 (Observational): Boolean(true) wins
|
|
/// - Tier 3 (Expert): Boolean(false) wins
|
|
/// - Tier 5 (Anecdotal): Boolean(false) wins
|
|
///
|
|
/// Cross-tier conflict should be high (tiers 0/2 vs 3/5 disagree).
|
|
/// Overall winner should come from Tier 0 (highest authority).
|
|
#[tokio::test]
|
|
async fn test_jwt_layered_lens_tier_agreement() {
|
|
use stemedb_lens::{LayeredConsensusLens, LayeredLens};
|
|
|
|
let store = Arc::new(HybridStore::open_temp().expect("store"));
|
|
let index_store = GenericIndexStore::new(store.clone());
|
|
|
|
let base_ts: u64 = 1_000_000;
|
|
|
|
// RFC 7519 (Tier 0): Boolean(true)
|
|
let rfc_7519 = AssertionBuilder::new()
|
|
.subject("JWT_aud_validation")
|
|
.predicate("aud_validation")
|
|
.object(ObjectValue::Boolean(true))
|
|
.source_class(SourceClass::Regulatory)
|
|
.confidence(1.0)
|
|
.agent_id([1u8; 32])
|
|
.timestamp(base_ts)
|
|
.build();
|
|
|
|
// Approved runbook (Tier 2): Boolean(true)
|
|
let approved_runbook = AssertionBuilder::new()
|
|
.subject("JWT_aud_validation")
|
|
.predicate("aud_validation")
|
|
.object(ObjectValue::Boolean(true))
|
|
.source_class(SourceClass::Observational)
|
|
.confidence(0.95)
|
|
.agent_id([2u8; 32])
|
|
.timestamp(base_ts + 1)
|
|
.build();
|
|
|
|
// Internal wiki (Tier 3): Boolean(false)
|
|
let internal_wiki = AssertionBuilder::new()
|
|
.subject("JWT_aud_validation")
|
|
.predicate("aud_validation")
|
|
.object(ObjectValue::Boolean(false))
|
|
.source_class(SourceClass::Expert)
|
|
.confidence(0.8)
|
|
.agent_id([3u8; 32])
|
|
.timestamp(base_ts + 2)
|
|
.build();
|
|
|
|
// Stack Overflow (Tier 5): Boolean(false)
|
|
let stack_overflow = AssertionBuilder::new()
|
|
.subject("JWT_aud_validation")
|
|
.predicate("aud_validation")
|
|
.object(ObjectValue::Boolean(false))
|
|
.source_class(SourceClass::Anecdotal)
|
|
.confidence(0.6)
|
|
.agent_id([4u8; 32])
|
|
.timestamp(base_ts + 3)
|
|
.build();
|
|
|
|
store_assertion_direct(&store, &index_store, &rfc_7519).await;
|
|
store_assertion_direct(&store, &index_store, &approved_runbook).await;
|
|
store_assertion_direct(&store, &index_store, &internal_wiki).await;
|
|
store_assertion_direct(&store, &index_store, &stack_overflow).await;
|
|
|
|
// === Resolve with LayeredConsensusLens ===
|
|
let lens = LayeredConsensusLens::new();
|
|
let assertions = vec![rfc_7519, approved_runbook, internal_wiki, stack_overflow];
|
|
let result = lens.resolve_layered(&assertions);
|
|
|
|
// === Assert tier-specific results ===
|
|
|
|
// Should have 4 tiers (0, 2, 3, 5)
|
|
assert_eq!(result.tiers.len(), 4, "should have 4 tiers");
|
|
|
|
// Tier 0 (Regulatory): Boolean(true)
|
|
let tier_0 = result.tiers.iter().find(|t| t.tier == 0).expect("tier 0 should exist");
|
|
assert_eq!(tier_0.candidates_count, 1);
|
|
assert!(tier_0.winner.is_some(), "tier 0 should have a winner");
|
|
assert_eq!(
|
|
tier_0.winner.as_ref().expect("tier 0 winner").object,
|
|
ObjectValue::Boolean(true),
|
|
"Tier 0 should say Boolean(true)"
|
|
);
|
|
|
|
// Tier 2 (Observational): Boolean(true)
|
|
let tier_2 = result.tiers.iter().find(|t| t.tier == 2).expect("tier 2 should exist");
|
|
assert_eq!(tier_2.candidates_count, 1);
|
|
assert!(tier_2.winner.is_some(), "tier 2 should have a winner");
|
|
assert_eq!(
|
|
tier_2.winner.as_ref().expect("tier 2 winner").object,
|
|
ObjectValue::Boolean(true),
|
|
"Tier 2 should say Boolean(true)"
|
|
);
|
|
|
|
// Tier 3 (Expert): Boolean(false)
|
|
let tier_3 = result.tiers.iter().find(|t| t.tier == 3).expect("tier 3 should exist");
|
|
assert_eq!(tier_3.candidates_count, 1);
|
|
assert!(tier_3.winner.is_some(), "tier 3 should have a winner");
|
|
assert_eq!(
|
|
tier_3.winner.as_ref().expect("tier 3 winner").object,
|
|
ObjectValue::Boolean(false),
|
|
"Tier 3 should say Boolean(false)"
|
|
);
|
|
|
|
// Tier 5 (Anecdotal): Boolean(false)
|
|
let tier_5 = result.tiers.iter().find(|t| t.tier == 5).expect("tier 5 should exist");
|
|
assert_eq!(tier_5.candidates_count, 1);
|
|
assert!(tier_5.winner.is_some(), "tier 5 should have a winner");
|
|
assert_eq!(
|
|
tier_5.winner.as_ref().expect("tier 5 winner").object,
|
|
ObjectValue::Boolean(false),
|
|
"Tier 5 should say Boolean(false)"
|
|
);
|
|
|
|
// === Assert overall results ===
|
|
|
|
// Overall winner should be from Tier 0 (highest authority)
|
|
assert!(result.overall_winner.is_some(), "should have overall winner");
|
|
assert_eq!(
|
|
result.overall_winner.as_ref().expect("overall winner").object,
|
|
ObjectValue::Boolean(true),
|
|
"Overall winner should be Boolean(true) from Tier 0"
|
|
);
|
|
assert_eq!(
|
|
result.overall_winner.as_ref().expect("overall winner").source_class,
|
|
SourceClass::Regulatory,
|
|
"Overall winner should be from Tier 0 (Regulatory)"
|
|
);
|
|
|
|
// Cross-tier conflict should be high (tiers 0/2 vs 3/5 disagree)
|
|
assert!(
|
|
result.overall_conflict_score > 0.5,
|
|
"overall_conflict_score should be > 0.5 (cross-tier disagreement), got {}",
|
|
result.overall_conflict_score
|
|
);
|
|
}
|