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

17 KiB

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:

// 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

// 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

// 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

// 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

// 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

# 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"
// 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

// 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
    ]
}