Files
vm-core/docs/VAULTMESH-TESTING-FRAMEWORK.md
2025-12-27 00:10:32 +00:00

621 lines
17 KiB
Markdown

# VAULTMESH-TESTING-FRAMEWORK.md
**Property-Based Testing for the Civilization Ledger**
> *What is not tested cannot be trusted.*
---
## 1. Testing Philosophy
VaultMesh uses a layered testing approach:
| Layer | What It Tests | Framework |
|-------|---------------|-----------|
| Unit | Individual functions | Rust: `#[test]`, Python: `pytest` |
| Property | Invariants that must always hold | `proptest`, `hypothesis` |
| Integration | Component interactions | `testcontainers` |
| Contract | API compatibility | OpenAPI validation |
| Chaos | Resilience under failure | `chaos-mesh`, custom |
| Acceptance | End-to-end scenarios | `cucumber-rs` |
---
## 2. Core Invariants
These properties must ALWAYS hold:
```rust
// vaultmesh-core/src/invariants.rs
/// Core invariants that must never be violated
pub trait Invariant {
fn check(&self) -> Result<(), InvariantViolation>;
}
/// Receipts are append-only (AXIOM-001)
pub struct AppendOnlyReceipts;
impl Invariant for AppendOnlyReceipts {
fn check(&self) -> Result<(), InvariantViolation> {
// Verify no receipts have been modified or deleted
// by comparing sequential hashes
Ok(())
}
}
/// Merkle roots are consistent with receipts (AXIOM-002)
pub struct ConsistentMerkleRoots;
impl Invariant for ConsistentMerkleRoots {
fn check(&self) -> Result<(), InvariantViolation> {
// Recompute Merkle root from receipts
// Compare with stored root
Ok(())
}
}
/// All significant operations produce receipts (AXIOM-003)
pub struct UniversalReceipting;
impl Invariant for UniversalReceipting {
fn check(&self) -> Result<(), InvariantViolation> {
// Check that tracked operations have corresponding receipts
Ok(())
}
}
/// Hash chains are unbroken
pub struct UnbrokenHashChains;
impl Invariant for UnbrokenHashChains {
fn check(&self) -> Result<(), InvariantViolation> {
// Verify each receipt's previous_hash matches the prior receipt
Ok(())
}
}
```
---
## 3. Property-Based Tests
### 3.1 Receipt Properties
```rust
// vaultmesh-core/tests/receipt_properties.rs
use proptest::prelude::*;
use vaultmesh_core::{Receipt, Scroll, VmHash};
proptest! {
/// Any valid receipt can be serialized and deserialized without loss
#[test]
fn receipt_roundtrip(receipt in arb_receipt()) {
let json = serde_json::to_string(&receipt)?;
let restored: Receipt = serde_json::from_str(&json)?;
prop_assert_eq!(receipt, restored);
}
/// Receipt hash is deterministic
#[test]
fn receipt_hash_deterministic(receipt in arb_receipt()) {
let hash1 = VmHash::from_json(&receipt)?;
let hash2 = VmHash::from_json(&receipt)?;
prop_assert_eq!(hash1, hash2);
}
/// Different receipts produce different hashes
#[test]
fn different_receipts_different_hashes(
receipt1 in arb_receipt(),
receipt2 in arb_receipt()
) {
prop_assume!(receipt1 != receipt2);
let hash1 = VmHash::from_json(&receipt1)?;
let hash2 = VmHash::from_json(&receipt2)?;
prop_assert_ne!(hash1, hash2);
}
/// Merkle root of N receipts is consistent regardless of computation order
#[test]
fn merkle_root_order_independent(receipts in prop::collection::vec(arb_receipt(), 1..100)) {
let hashes: Vec<VmHash> = receipts.iter()
.map(|r| VmHash::from_json(r).unwrap())
.collect();
let root1 = merkle_root(&hashes);
// Shuffle but keep same hashes
let mut shuffled = hashes.clone();
shuffled.sort_by(|a, b| a.hex().cmp(b.hex()));
// Root should be same because merkle_root sorts internally
let root2 = merkle_root(&shuffled);
prop_assert_eq!(root1, root2);
}
}
fn arb_receipt() -> impl Strategy<Value = Receipt<serde_json::Value>> {
(
arb_scroll(),
arb_receipt_type(),
any::<u64>(),
prop::collection::vec(any::<String>(), 0..5),
).prop_map(|(scroll, receipt_type, timestamp, tags)| {
Receipt {
header: ReceiptHeader {
receipt_type,
timestamp: DateTime::from_timestamp(timestamp as i64, 0).unwrap(),
root_hash: "blake3:placeholder".to_string(),
tags,
},
meta: ReceiptMeta {
scroll,
sequence: 0,
anchor_epoch: None,
proof_path: None,
},
body: serde_json::json!({"test": true}),
}
})
}
fn arb_scroll() -> impl Strategy<Value = Scroll> {
prop_oneof![
Just(Scroll::Drills),
Just(Scroll::Compliance),
Just(Scroll::Guardian),
Just(Scroll::Treasury),
Just(Scroll::Mesh),
Just(Scroll::OffSec),
Just(Scroll::Identity),
Just(Scroll::Observability),
Just(Scroll::Automation),
Just(Scroll::PsiField),
]
}
fn arb_receipt_type() -> impl Strategy<Value = String> {
prop_oneof![
Just("security_drill_run".to_string()),
Just("oracle_answer".to_string()),
Just("anchor_success".to_string()),
Just("treasury_credit".to_string()),
Just("mesh_node_join".to_string()),
]
}
```
### 3.2 Guardian Properties
```rust
// vaultmesh-guardian/tests/guardian_properties.rs
use proptest::prelude::*;
use vaultmesh_guardian::{ProofChain, AnchorCycle};
proptest! {
/// Anchor cycle produces valid proof for all included receipts
#[test]
fn anchor_cycle_valid_proofs(
receipts in prop::collection::vec(arb_receipt(), 1..50)
) {
let mut proofchain = ProofChain::new();
for receipt in &receipts {
proofchain.append(receipt)?;
}
let cycle = AnchorCycle::new(&proofchain);
let anchor_result = cycle.execute_mock()?;
// Every receipt should have a valid Merkle proof
for receipt in &receipts {
let proof = anchor_result.get_proof(&receipt.header.root_hash)?;
prop_assert!(proof.verify(&anchor_result.root_hash));
}
}
/// Anchor root changes when any receipt changes
#[test]
fn anchor_root_sensitive(
receipts in prop::collection::vec(arb_receipt(), 2..20),
index in any::<prop::sample::Index>()
) {
let mut proofchain1 = ProofChain::new();
let mut proofchain2 = ProofChain::new();
for receipt in &receipts {
proofchain1.append(receipt)?;
proofchain2.append(receipt)?;
}
let root1 = proofchain1.current_root();
// Modify one receipt in proofchain2
let idx = index.index(receipts.len());
let mut modified = receipts[idx].clone();
modified.body = serde_json::json!({"modified": true});
proofchain2.replace(idx, &modified)?;
let root2 = proofchain2.current_root();
prop_assert_ne!(root1, root2);
}
/// Sequential anchors form valid chain
#[test]
fn sequential_anchors_chain(
receipt_batches in prop::collection::vec(
prop::collection::vec(arb_receipt(), 1..20),
2..10
)
) {
let mut proofchain = ProofChain::new();
let mut previous_anchor: Option<AnchorResult> = None;
for batch in receipt_batches {
for receipt in batch {
proofchain.append(&receipt)?;
}
let cycle = AnchorCycle::new(&proofchain);
let anchor_result = cycle.execute_mock()?;
if let Some(prev) = &previous_anchor {
// Current anchor should reference previous
prop_assert_eq!(anchor_result.previous_root, Some(prev.root_hash.clone()));
}
previous_anchor = Some(anchor_result);
}
}
}
```
### 3.3 Treasury Properties
```rust
// vaultmesh-treasury/tests/treasury_properties.rs
use proptest::prelude::*;
use rust_decimal::Decimal;
use vaultmesh_treasury::{TreasuryEngine, Entry, EntryType, Settlement};
proptest! {
/// Sum of all entries is always zero (double-entry invariant)
#[test]
fn double_entry_balance(
entries in prop::collection::vec(arb_entry_pair(), 1..50)
) {
let mut engine = TreasuryEngine::new();
engine.create_account(test_account("account-a"))?;
engine.create_account(test_account("account-b"))?;
let mut total = Decimal::ZERO;
for (debit, credit) in entries {
engine.record_entry(debit.clone())?;
engine.record_entry(credit.clone())?;
total += credit.amount;
total -= debit.amount;
}
// Total should always be zero
prop_assert_eq!(total, Decimal::ZERO);
}
/// Settlement balances match pre/post snapshots
#[test]
fn settlement_balance_consistency(
settlement in arb_settlement()
) {
let mut engine = TreasuryEngine::new();
// Create accounts from settlement
for entry in &settlement.entries {
engine.create_account_if_not_exists(&entry.account)?;
}
// Fund accounts
for entry in &settlement.entries {
if entry.entry_type == EntryType::Debit {
engine.fund_account(&entry.account, entry.amount * 2)?;
}
}
// Snapshot before
let before = engine.snapshot_balances(&settlement.affected_accounts())?;
// Execute settlement
let result = engine.execute_settlement(settlement.clone())?;
// Snapshot after
let after = engine.snapshot_balances(&settlement.affected_accounts())?;
// Verify net flows match difference
for (account, net_flow) in &result.net_flow {
let expected_after = before.get(account).unwrap() + net_flow;
prop_assert_eq!(*after.get(account).unwrap(), expected_after);
}
}
}
fn arb_entry_pair() -> impl Strategy<Value = (Entry, Entry)> {
(1u64..1000000).prop_map(|cents| {
let amount = Decimal::new(cents as i64, 2);
let debit = Entry {
entry_id: format!("debit-{}", uuid::Uuid::new_v4()),
entry_type: EntryType::Debit,
account: "account-a".to_string(),
amount,
currency: Currency::EUR,
memo: "Test debit".to_string(),
timestamp: Utc::now(),
tags: vec![],
};
let credit = Entry {
entry_id: format!("credit-{}", uuid::Uuid::new_v4()),
entry_type: EntryType::Credit,
account: "account-b".to_string(),
amount,
currency: Currency::EUR,
memo: "Test credit".to_string(),
timestamp: Utc::now(),
tags: vec![],
};
(debit, credit)
})
}
```
---
## 4. Integration Tests
```rust
// tests/integration/full_cycle.rs
use testcontainers::{clients, images::postgres::Postgres, Container};
use vaultmesh_portal::Portal;
use vaultmesh_guardian::Guardian;
use vaultmesh_oracle::Oracle;
#[tokio::test]
async fn full_receipt_lifecycle() {
// Start containers
let docker = clients::Cli::default();
let postgres = docker.run(Postgres::default());
let db_url = format!(
"postgresql://postgres:postgres@localhost:{}/postgres",
postgres.get_host_port_ipv4(5432)
);
// Initialize services
let portal = Portal::new(&db_url).await?;
let guardian = Guardian::new(&db_url).await?;
// Create and emit receipt
let receipt = portal.emit_receipt(
Scroll::Drills,
"security_drill_run",
json!({
"drill_id": "test-drill-001",
"status": "completed"
}),
vec!["test".to_string()],
).await?;
// Verify receipt exists
let stored = portal.get_receipt(&receipt.header.root_hash).await?;
assert_eq!(stored.header.root_hash, receipt.header.root_hash);
// Trigger anchor
let anchor_result = guardian.anchor_now(None).await?;
assert!(anchor_result.success);
// Verify receipt has proof
let proof = guardian.get_proof(&receipt.header.root_hash).await?;
assert!(proof.is_some());
assert!(proof.unwrap().verify(&anchor_result.root_hash));
}
#[tokio::test]
async fn oracle_answer_receipted() {
let docker = clients::Cli::default();
let postgres = docker.run(Postgres::default());
let db_url = format!(
"postgresql://postgres:postgres@localhost:{}/postgres",
postgres.get_host_port_ipv4(5432)
);
let portal = Portal::new(&db_url).await?;
let oracle = Oracle::new(&db_url).await?;
// Load test corpus
oracle.load_corpus("tests/fixtures/corpus").await?;
// Ask question
let answer = oracle.answer(
"What are the requirements for technical documentation under Article 11?",
vec!["AI_Act".to_string()],
vec![],
).await?;
// Verify answer was receipted
let receipts = portal.query_receipts(
Some(Scroll::Compliance),
Some("oracle_answer".to_string()),
None,
None,
10,
).await?;
assert!(!receipts.is_empty());
assert_eq!(receipts[0].body["answer_hash"], answer.answer_hash);
}
```
---
## 5. Chaos Tests
```yaml
# chaos/anchor-failure.yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: anchor-network-partition
namespace: vaultmesh
spec:
action: partition
mode: all
selector:
namespaces:
- vaultmesh
labelSelectors:
app.kubernetes.io/name: guardian
direction: to
target:
selector:
namespaces:
- default
labelSelectors:
app: ethereum-node
mode: all
duration: "5m"
scheduler:
cron: "@every 6h"
---
apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
name: guardian-pod-kill
namespace: vaultmesh
spec:
action: pod-kill
mode: one
selector:
namespaces:
- vaultmesh
labelSelectors:
app.kubernetes.io/name: guardian
scheduler:
cron: "@every 4h"
```
```rust
// tests/chaos/anchor_resilience.rs
#[tokio::test]
#[ignore] // Run manually with chaos-mesh
async fn guardian_recovers_from_network_partition() {
let guardian = connect_to_guardian().await?;
let portal = connect_to_portal().await?;
// Generate receipts
for i in 0..100 {
portal.emit_receipt(
Scroll::Drills,
"test_receipt",
json!({"index": i}),
vec![],
).await?;
}
// Wait for chaos to potentially occur
tokio::time::sleep(Duration::from_secs(60)).await;
// Verify guardian state is consistent
let status = guardian.get_status().await?;
// Should either be anchoring or have recovered
assert!(
status.state == "idle" ||
status.state == "anchoring",
"Guardian in unexpected state: {}",
status.state
);
// If idle, verify all receipts are anchored
if status.state == "idle" {
let receipts = portal.query_receipts(None, None, None, None, 200).await?;
for receipt in receipts {
let proof = guardian.get_proof(&receipt.header.root_hash).await?;
assert!(proof.is_some(), "Receipt not anchored: {}", receipt.header.root_hash);
}
}
}
```
---
## 6. Test Fixtures
```rust
// tests/fixtures/mod.rs
use vaultmesh_core::*;
pub fn test_drill_receipt() -> Receipt<serde_json::Value> {
Receipt {
header: ReceiptHeader {
receipt_type: "security_drill_run".to_string(),
timestamp: Utc::now(),
root_hash: "blake3:placeholder".to_string(),
tags: vec!["test".to_string()],
},
meta: ReceiptMeta {
scroll: Scroll::Drills,
sequence: 1,
anchor_epoch: None,
proof_path: None,
},
body: json!({
"drill_id": "drill-test-001",
"prompt": "Test security scenario",
"status": "completed",
"stages_total": 3,
"stages_completed": 3
}),
}
}
pub fn test_oracle_receipt() -> Receipt<serde_json::Value> {
Receipt {
header: ReceiptHeader {
receipt_type: "oracle_answer".to_string(),
timestamp: Utc::now(),
root_hash: "blake3:placeholder".to_string(),
tags: vec!["test".to_string(), "compliance".to_string()],
},
meta: ReceiptMeta {
scroll: Scroll::Compliance,
sequence: 1,
anchor_epoch: None,
proof_path: None,
},
body: json!({
"question": "Test compliance question?",
"answer_hash": "blake3:test...",
"confidence": 0.95,
"frameworks": ["AI_Act"]
}),
}
}
pub fn test_corpus() -> Vec<CorpusDocument> {
vec![
CorpusDocument {
id: "doc-001".to_string(),
title: "AI Act Article 11 - Technical Documentation".to_string(),
content: "Providers shall draw up technical documentation...".to_string(),
framework: "AI_Act".to_string(),
section: "Article 11".to_string(),
},
// ... more test documents
]
}
```