621 lines
17 KiB
Markdown
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
|
|
]
|
|
}
|
|
```
|