Initialize repository snapshot

This commit is contained in:
Vault Sovereign
2025-12-27 00:10:32 +00:00
commit 110d644e10
281 changed files with 40331 additions and 0 deletions

21
vaultmesh-mesh/Cargo.toml Normal file
View File

@@ -0,0 +1,21 @@
[package]
name = "vaultmesh-mesh"
version = "0.1.0"
edition = "2021"
[dependencies]
vaultmesh-core = { path = "../vaultmesh-core" }
vaultmesh-observability = { path = "../vaultmesh-observability", optional = true }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[features]
default = []
metrics = ["vaultmesh-observability"]
[dev-dependencies]
tempfile = "3.8"
tokio = { version = "1.28", features = ["rt-multi-thread", "macros", "time"] }
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
vaultmesh-observability = { path = "../vaultmesh-observability" }

824
vaultmesh-mesh/src/lib.rs Normal file
View File

@@ -0,0 +1,824 @@
//! VaultMesh Mesh Engine - Federation topology management
//!
//! The Mesh engine tracks nodes, routes, and capabilities in the federation
//! network, emitting receipts for all topology changes.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
#[cfg(feature = "metrics")]
use std::sync::Arc;
#[cfg(feature = "metrics")]
use std::time::Instant;
use vaultmesh_core::{Scroll, VmHash};
#[cfg(feature = "metrics")]
use vaultmesh_observability::ObservabilityEngine;
/// Schema version for mesh receipts
pub const SCHEMA_VERSION: &str = "2.0.0";
/// Node type in the mesh
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NodeType {
Infrastructure,
Edge,
Oracle,
Guardian,
External,
}
/// Node status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NodeStatus {
Active,
Inactive,
Degraded,
}
/// A node in the mesh
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Node {
pub node_id: String,
pub display_name: String,
pub node_type: NodeType,
pub endpoints: HashMap<String, String>,
pub public_key: Option<String>,
pub capabilities: Vec<String>,
pub status: NodeStatus,
pub joined_at: DateTime<Utc>,
pub tags: Vec<String>,
}
/// Route status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RouteStatus {
Active,
Degraded,
Failed,
}
/// A route between nodes
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Route {
pub route_id: String,
pub source: String,
pub destination: String,
pub transport: String,
pub priority: u32,
pub status: RouteStatus,
pub latency_ms: Option<u32>,
pub established_at: DateTime<Utc>,
}
/// Capability scope
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CapabilityScope {
Global,
Local,
Limited,
}
/// A capability granted to a node
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Capability {
pub capability_id: String,
pub node_id: String,
pub capability: String,
pub scope: CapabilityScope,
pub granted_by: String,
pub granted_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
}
/// Topology snapshot
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TopologySnapshot {
pub snapshot_id: String,
pub timestamp: DateTime<Utc>,
pub node_count: usize,
pub route_count: usize,
pub capability_count: usize,
pub nodes: Vec<String>,
pub topology_hash: String,
}
/// Generic mesh receipt wrapper
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MeshReceipt {
pub schema_version: String,
#[serde(rename = "type")]
pub receipt_type: String,
pub timestamp: DateTime<Utc>,
pub scroll: String,
pub tags: Vec<String>,
pub root_hash: String,
pub body: serde_json::Value,
}
/// Node join receipt body
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeJoinReceiptBody {
pub node_id: String,
pub display_name: String,
pub node_type: NodeType,
pub joined_by: String,
}
/// Node leave receipt body
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeLeaveReceiptBody {
pub node_id: String,
pub reason: String,
pub left_by: String,
}
/// Route change receipt body
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouteChangeReceiptBody {
pub route_id: String,
pub operation: String,
pub source: String,
pub destination: String,
pub transport: String,
}
/// Capability grant receipt body
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityGrantReceiptBody {
pub capability_id: String,
pub node_id: String,
pub capability: String,
pub scope: CapabilityScope,
pub granted_by: String,
pub expires_at: Option<DateTime<Utc>>,
}
/// Capability revoke receipt body
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityRevokeReceiptBody {
pub capability_id: String,
pub node_id: String,
pub capability: String,
pub revoked_by: String,
pub reason: String,
}
/// Topology snapshot receipt body
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TopologySnapshotReceiptBody {
pub snapshot_id: String,
pub node_count: usize,
pub route_count: usize,
pub capability_count: usize,
pub topology_hash: String,
}
/// Mesh engine errors
#[derive(Debug)]
pub enum MeshError {
NodeExists(String),
NodeNotFound(String),
RouteExists(String),
RouteNotFound(String),
CapabilityExists(String),
CapabilityNotFound(String),
IoError(std::io::Error),
SerializationError(serde_json::Error),
}
impl std::fmt::Display for MeshError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MeshError::NodeExists(id) => write!(f, "Node already exists: {}", id),
MeshError::NodeNotFound(id) => write!(f, "Node not found: {}", id),
MeshError::RouteExists(id) => write!(f, "Route already exists: {}", id),
MeshError::RouteNotFound(id) => write!(f, "Route not found: {}", id),
MeshError::CapabilityExists(id) => write!(f, "Capability already exists: {}", id),
MeshError::CapabilityNotFound(id) => write!(f, "Capability not found: {}", id),
MeshError::IoError(e) => write!(f, "IO error: {}", e),
MeshError::SerializationError(e) => write!(f, "Serialization error: {}", e),
}
}
}
impl std::error::Error for MeshError {}
impl From<std::io::Error> for MeshError {
fn from(e: std::io::Error) -> Self {
MeshError::IoError(e)
}
}
impl From<serde_json::Error> for MeshError {
fn from(e: serde_json::Error) -> Self {
MeshError::SerializationError(e)
}
}
/// Mesh engine for federation topology management
pub struct MeshEngine {
/// Path to VaultMesh root
pub vaultmesh_root: PathBuf,
/// In-memory node registry
nodes: HashMap<String, Node>,
/// In-memory route registry
routes: HashMap<String, Route>,
/// In-memory capability registry
capabilities: HashMap<String, Capability>,
/// Optional observability engine for metrics
#[cfg(feature = "metrics")]
pub observability: Option<Arc<ObservabilityEngine>>,
}
impl MeshEngine {
/// Create a new Mesh engine
pub fn new(vaultmesh_root: impl AsRef<Path>) -> Self {
Self {
vaultmesh_root: vaultmesh_root.as_ref().to_path_buf(),
nodes: HashMap::new(),
routes: HashMap::new(),
capabilities: HashMap::new(),
#[cfg(feature = "metrics")]
observability: None,
}
}
/// Set the observability engine for metrics
#[cfg(feature = "metrics")]
pub fn with_observability(mut self, obs: Arc<ObservabilityEngine>) -> Self {
self.observability = Some(obs);
self
}
/// Get path to mesh events JSONL
fn events_path(&self) -> PathBuf {
self.vaultmesh_root.join(Scroll::Mesh.jsonl_path())
}
/// Register a new node in the mesh
pub fn node_join(
&mut self,
node_id: &str,
display_name: &str,
node_type: NodeType,
endpoints: HashMap<String, String>,
joined_by: &str,
) -> Result<Node, MeshError> {
if self.nodes.contains_key(node_id) {
return Err(MeshError::NodeExists(node_id.to_string()));
}
let node = Node {
node_id: node_id.to_string(),
display_name: display_name.to_string(),
node_type: node_type.clone(),
endpoints,
public_key: None,
capabilities: Vec::new(),
status: NodeStatus::Active,
joined_at: Utc::now(),
tags: vec!["mesh".to_string(), "node".to_string()],
};
self.nodes.insert(node_id.to_string(), node.clone());
// Emit receipt
let receipt_body = NodeJoinReceiptBody {
node_id: node_id.to_string(),
display_name: display_name.to_string(),
node_type,
joined_by: joined_by.to_string(),
};
self.emit_receipt("mesh_node_join", &receipt_body, vec![
"mesh".to_string(),
"node".to_string(),
"join".to_string(),
node_id.to_string(),
])?;
Ok(node)
}
/// Remove a node from the mesh
pub fn node_leave(
&mut self,
node_id: &str,
reason: &str,
left_by: &str,
) -> Result<(), MeshError> {
if !self.nodes.contains_key(node_id) {
return Err(MeshError::NodeNotFound(node_id.to_string()));
}
self.nodes.remove(node_id);
// Remove associated routes
self.routes.retain(|_, r| r.source != node_id && r.destination != node_id);
// Remove associated capabilities
self.capabilities.retain(|_, c| c.node_id != node_id);
// Emit receipt
let receipt_body = NodeLeaveReceiptBody {
node_id: node_id.to_string(),
reason: reason.to_string(),
left_by: left_by.to_string(),
};
self.emit_receipt("mesh_node_leave", &receipt_body, vec![
"mesh".to_string(),
"node".to_string(),
"leave".to_string(),
node_id.to_string(),
])?;
Ok(())
}
/// Add a route between nodes
pub fn route_add(
&mut self,
source: &str,
destination: &str,
transport: &str,
) -> Result<Route, MeshError> {
let route_id = format!("route-{}-to-{}", source.split(':').last().unwrap_or(source),
destination.split(':').last().unwrap_or(destination));
if self.routes.contains_key(&route_id) {
return Err(MeshError::RouteExists(route_id));
}
let route = Route {
route_id: route_id.clone(),
source: source.to_string(),
destination: destination.to_string(),
transport: transport.to_string(),
priority: 1,
status: RouteStatus::Active,
latency_ms: None,
established_at: Utc::now(),
};
self.routes.insert(route_id.clone(), route.clone());
// Emit receipt
let receipt_body = RouteChangeReceiptBody {
route_id: route_id.clone(),
operation: "add".to_string(),
source: source.to_string(),
destination: destination.to_string(),
transport: transport.to_string(),
};
self.emit_receipt("mesh_route_change", &receipt_body, vec![
"mesh".to_string(),
"route".to_string(),
"add".to_string(),
])?;
Ok(route)
}
/// Remove a route
pub fn route_remove(&mut self, route_id: &str) -> Result<(), MeshError> {
let route = self.routes.remove(route_id)
.ok_or_else(|| MeshError::RouteNotFound(route_id.to_string()))?;
// Emit receipt
let receipt_body = RouteChangeReceiptBody {
route_id: route_id.to_string(),
operation: "remove".to_string(),
source: route.source,
destination: route.destination,
transport: route.transport,
};
self.emit_receipt("mesh_route_change", &receipt_body, vec![
"mesh".to_string(),
"route".to_string(),
"remove".to_string(),
])?;
Ok(())
}
/// Grant a capability to a node
pub fn capability_grant(
&mut self,
node_id: &str,
capability: &str,
scope: CapabilityScope,
granted_by: &str,
expires_at: Option<DateTime<Utc>>,
) -> Result<Capability, MeshError> {
let capability_id = format!("cap:{}:{}:{}",
node_id.split(':').last().unwrap_or(node_id),
capability,
Utc::now().format("%Y"));
if self.capabilities.contains_key(&capability_id) {
return Err(MeshError::CapabilityExists(capability_id));
}
let cap = Capability {
capability_id: capability_id.clone(),
node_id: node_id.to_string(),
capability: capability.to_string(),
scope: scope.clone(),
granted_by: granted_by.to_string(),
granted_at: Utc::now(),
expires_at,
};
self.capabilities.insert(capability_id.clone(), cap.clone());
// Update node capabilities list
if let Some(node) = self.nodes.get_mut(node_id) {
if !node.capabilities.contains(&capability.to_string()) {
node.capabilities.push(capability.to_string());
}
}
// Emit receipt
let receipt_body = CapabilityGrantReceiptBody {
capability_id: capability_id.clone(),
node_id: node_id.to_string(),
capability: capability.to_string(),
scope,
granted_by: granted_by.to_string(),
expires_at,
};
self.emit_receipt("mesh_capability_grant", &receipt_body, vec![
"mesh".to_string(),
"capability".to_string(),
"grant".to_string(),
capability.to_string(),
])?;
Ok(cap)
}
/// Revoke a capability from a node
pub fn capability_revoke(
&mut self,
node_id: &str,
capability: &str,
revoked_by: &str,
reason: &str,
) -> Result<(), MeshError> {
// Find and remove the capability
let cap_to_remove: Option<String> = self.capabilities.iter()
.find(|(_, c)| c.node_id == node_id && c.capability == capability)
.map(|(id, _)| id.clone());
let capability_id = cap_to_remove
.ok_or_else(|| MeshError::CapabilityNotFound(
format!("{}:{}", node_id, capability)
))?;
self.capabilities.remove(&capability_id);
// Update node capabilities list
if let Some(node) = self.nodes.get_mut(node_id) {
node.capabilities.retain(|c| c != capability);
}
// Emit receipt
let receipt_body = CapabilityRevokeReceiptBody {
capability_id,
node_id: node_id.to_string(),
capability: capability.to_string(),
revoked_by: revoked_by.to_string(),
reason: reason.to_string(),
};
self.emit_receipt("mesh_capability_revoke", &receipt_body, vec![
"mesh".to_string(),
"capability".to_string(),
"revoke".to_string(),
capability.to_string(),
])?;
Ok(())
}
/// Create a topology snapshot
pub fn topology_snapshot(&self) -> Result<TopologySnapshot, MeshError> {
let now = Utc::now();
let snapshot_id = format!("snapshot-{}", now.format("%Y%m%d%H%M%S"));
// Compute topology hash
let topology_data = serde_json::json!({
"nodes": self.nodes.keys().collect::<Vec<_>>(),
"routes": self.routes.keys().collect::<Vec<_>>(),
"capabilities": self.capabilities.keys().collect::<Vec<_>>(),
});
let topology_hash = VmHash::from_json(&topology_data)?;
let snapshot = TopologySnapshot {
snapshot_id: snapshot_id.clone(),
timestamp: now,
node_count: self.nodes.len(),
route_count: self.routes.len(),
capability_count: self.capabilities.len(),
nodes: self.nodes.keys().cloned().collect(),
topology_hash: topology_hash.as_str().to_string(),
};
// Emit receipt (use a separate method to avoid borrow issues)
let receipt_body = TopologySnapshotReceiptBody {
snapshot_id: snapshot.snapshot_id.clone(),
node_count: snapshot.node_count,
route_count: snapshot.route_count,
capability_count: snapshot.capability_count,
topology_hash: snapshot.topology_hash.clone(),
};
self.emit_snapshot_receipt(&receipt_body)?;
Ok(snapshot)
}
/// Get a node by ID
pub fn get_node(&self, node_id: &str) -> Option<&Node> {
self.nodes.get(node_id)
}
/// List all nodes
pub fn list_nodes(&self) -> Vec<&Node> {
self.nodes.values().collect()
}
/// Get a route by ID
pub fn get_route(&self, route_id: &str) -> Option<&Route> {
self.routes.get(route_id)
}
/// List all routes
pub fn list_routes(&self) -> Vec<&Route> {
self.routes.values().collect()
}
/// Check if node has capability
pub fn has_capability(&self, node_id: &str, capability: &str) -> bool {
self.capabilities.values()
.any(|c| c.node_id == node_id && c.capability == capability)
}
/// Emit a mesh receipt
fn emit_receipt<T: Serialize>(
&self,
receipt_type: &str,
body: &T,
tags: Vec<String>,
) -> Result<(), MeshError> {
#[cfg(feature = "metrics")]
let start = Instant::now();
let body_json = serde_json::to_value(body)?;
let root_hash = VmHash::from_json(&body_json)?;
let receipt = MeshReceipt {
schema_version: SCHEMA_VERSION.to_string(),
receipt_type: receipt_type.to_string(),
timestamp: Utc::now(),
scroll: "mesh".to_string(),
tags,
root_hash: root_hash.as_str().to_string(),
body: body_json,
};
// Ensure directory exists
let path = self.events_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
// Append to JSONL
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)?;
let json = serde_json::to_string(&receipt)?;
writeln!(file, "{}", json)?;
// Update ROOT file
self.update_root_file()?;
// Record metrics if observability is enabled
#[cfg(feature = "metrics")]
if let Some(ref obs) = self.observability {
let elapsed = start.elapsed().as_secs_f64();
obs.observe_emitted("mesh", elapsed);
}
Ok(())
}
/// Emit snapshot receipt (separate method to work with &self)
fn emit_snapshot_receipt(&self, body: &TopologySnapshotReceiptBody) -> Result<(), MeshError> {
#[cfg(feature = "metrics")]
let start = Instant::now();
let body_json = serde_json::to_value(body)?;
let root_hash = VmHash::from_json(&body_json)?;
let receipt = MeshReceipt {
schema_version: SCHEMA_VERSION.to_string(),
receipt_type: "mesh_topology_snapshot".to_string(),
timestamp: Utc::now(),
scroll: "mesh".to_string(),
tags: vec!["mesh".to_string(), "snapshot".to_string(), "topology".to_string()],
root_hash: root_hash.as_str().to_string(),
body: body_json,
};
let path = self.events_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)?;
let json = serde_json::to_string(&receipt)?;
writeln!(file, "{}", json)?;
self.update_root_file()?;
#[cfg(feature = "metrics")]
if let Some(ref obs) = self.observability {
let elapsed = start.elapsed().as_secs_f64();
obs.observe_emitted("mesh", elapsed);
}
Ok(())
}
/// Update ROOT.mesh.txt with current Merkle root
fn update_root_file(&self) -> Result<(), MeshError> {
let events_path = self.events_path();
if !events_path.exists() {
return Ok(());
}
let content = fs::read_to_string(&events_path)?;
let hashes: Vec<VmHash> = content
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| VmHash::blake3(l.as_bytes()))
.collect();
let root = vaultmesh_core::merkle_root(&hashes);
let root_path = self.vaultmesh_root.join(Scroll::Mesh.root_file());
fs::write(&root_path, root.as_str())?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn setup_test_env() -> (TempDir, MeshEngine) {
let tmp = TempDir::new().unwrap();
let engine = MeshEngine::new(tmp.path());
(tmp, engine)
}
#[test]
fn test_node_join_creates_receipt() {
let (tmp, mut engine) = setup_test_env();
let mut endpoints = HashMap::new();
endpoints.insert("portal".to_string(), "https://brick-01.local:8443".to_string());
let node = engine.node_join(
"did:vm:node:brick-01",
"BRICK-01 (Dublin)",
NodeType::Infrastructure,
endpoints,
"did:vm:human:admin",
).unwrap();
assert_eq!(node.node_id, "did:vm:node:brick-01");
assert_eq!(node.status, NodeStatus::Active);
// Verify receipt was written
let events_path = tmp.path().join("receipts/mesh/mesh_events.jsonl");
assert!(events_path.exists());
let content = fs::read_to_string(&events_path).unwrap();
assert!(content.contains("mesh_node_join"));
}
#[test]
fn test_node_leave_removes_node() {
let (_tmp, mut engine) = setup_test_env();
engine.node_join(
"did:vm:node:brick-01",
"BRICK-01",
NodeType::Infrastructure,
HashMap::new(),
"did:vm:human:admin",
).unwrap();
assert!(engine.get_node("did:vm:node:brick-01").is_some());
engine.node_leave(
"did:vm:node:brick-01",
"decommissioned",
"did:vm:human:admin",
).unwrap();
assert!(engine.get_node("did:vm:node:brick-01").is_none());
}
#[test]
fn test_route_add_connects_nodes() {
let (_tmp, mut engine) = setup_test_env();
let route = engine.route_add(
"did:vm:node:brick-01",
"did:vm:node:brick-02",
"wireguard",
).unwrap();
assert_eq!(route.source, "did:vm:node:brick-01");
assert_eq!(route.destination, "did:vm:node:brick-02");
assert_eq!(route.transport, "wireguard");
assert_eq!(route.status, RouteStatus::Active);
// Verify route is retrievable
assert!(engine.get_route(&route.route_id).is_some());
}
#[test]
fn test_capability_grant_and_check() {
let (_tmp, mut engine) = setup_test_env();
engine.node_join(
"did:vm:node:brick-01",
"BRICK-01",
NodeType::Infrastructure,
HashMap::new(),
"did:vm:human:admin",
).unwrap();
let cap = engine.capability_grant(
"did:vm:node:brick-01",
"anchor",
CapabilityScope::Global,
"did:vm:human:admin",
None,
).unwrap();
assert_eq!(cap.capability, "anchor");
assert!(engine.has_capability("did:vm:node:brick-01", "anchor"));
// Revoke and verify
engine.capability_revoke(
"did:vm:node:brick-01",
"anchor",
"did:vm:human:admin",
"security audit",
).unwrap();
assert!(!engine.has_capability("did:vm:node:brick-01", "anchor"));
}
#[test]
fn test_topology_snapshot() {
let (_tmp, mut engine) = setup_test_env();
// Add some nodes and routes
engine.node_join("did:vm:node:brick-01", "BRICK-01", NodeType::Infrastructure, HashMap::new(), "admin").unwrap();
engine.node_join("did:vm:node:brick-02", "BRICK-02", NodeType::Infrastructure, HashMap::new(), "admin").unwrap();
engine.route_add("did:vm:node:brick-01", "did:vm:node:brick-02", "wireguard").unwrap();
let snapshot = engine.topology_snapshot().unwrap();
assert_eq!(snapshot.node_count, 2);
assert_eq!(snapshot.route_count, 1);
assert!(snapshot.snapshot_id.starts_with("snapshot-"));
assert!(!snapshot.topology_hash.is_empty());
}
}

View File

@@ -0,0 +1,76 @@
//! Integration test: Mesh operations update observability metrics
//!
//! Run with:
//! cargo test -p vaultmesh-mesh --features metrics --test metrics_integration
//!
#![cfg(feature = "metrics")]
use std::collections::HashMap;
use std::net::TcpListener;
use std::sync::Arc;
use std::time::Duration;
use tempfile::TempDir;
use tokio::time::sleep;
use vaultmesh_mesh::{MeshEngine, NodeType};
use vaultmesh_observability::ObservabilityEngine;
#[tokio::test]
async fn test_mesh_operations_update_observability_metrics() {
// Temporary dir for mesh persistence
let tmp = TempDir::new().expect("tempdir");
// Dynamic port allocation - bind to port 0 to get an available port
let listener = TcpListener::bind("127.0.0.1:0").expect("bind to port 0");
let addr = listener.local_addr().expect("get local addr");
drop(listener); // Release so ObservabilityEngine can bind
// Start observability engine on the dynamically assigned port
let obs = Arc::new(ObservabilityEngine::new());
obs.clone()
.serve(&addr)
.await
.expect("observability serve failed");
// Small delay to allow server to bind and be ready
sleep(Duration::from_millis(100)).await;
// Create MeshEngine and attach observability
let mut mesh = MeshEngine::new(tmp.path())
.with_observability(obs.clone());
// Perform a basic operation that emits a receipt
mesh.node_join(
"did:vm:node:test",
"Test Node",
NodeType::Infrastructure,
HashMap::new(),
"admin",
)
.expect("node_join");
// Wait a little to allow the receipt emission and metric recording
sleep(Duration::from_millis(100)).await;
// Fetch metrics from observability server using dynamic address
let metrics_url = format!("http://{}/metrics", addr);
let resp = reqwest::get(&metrics_url)
.await
.expect("request failed");
assert!(resp.status().is_success(), "metrics endpoint must return 200");
let body = resp.text().await.expect("read body");
// Basic asserts: receipts counter exists and mesh module is present
assert!(
body.contains("vaultmesh_receipts_total"),
"metrics should expose receipts counter"
);
// Assert mesh module appears in metrics
assert!(
body.contains("mesh"),
"mesh module should appear in metrics"
);
}