Initialize repository snapshot
This commit is contained in:
620
docs/VAULTMESH-TESTING-FRAMEWORK.md
Normal file
620
docs/VAULTMESH-TESTING-FRAMEWORK.md
Normal file
@@ -0,0 +1,620 @@
|
||||
# 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
|
||||
]
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user