use std::collections::HashSet;
use datasynth_graph::builders::hypergraph::{HypergraphBuilder, HypergraphConfig};
use datasynth_graph::exporters::hypergraph::{HypergraphExportConfig, HypergraphExporter};
use datasynth_graph::models::hypergraph::{
CrossLayerEdge, Hyperedge, HypergraphLayer, HypergraphMetadata, HypergraphNode,
};
use tempfile::tempdir;
#[test]
fn test_full_hypergraph_roundtrip() {
let config = HypergraphConfig {
max_nodes: 5000,
include_coso: true,
include_controls: false,
include_sox: false,
include_p2p: false,
include_o2c: false,
include_vendors: false,
include_customers: false,
include_employees: false,
..Default::default()
};
let mut builder = HypergraphBuilder::new(config);
builder.add_coso_framework();
let hg = builder.build();
let dir = tempdir().unwrap();
let exporter = HypergraphExporter::new(HypergraphExportConfig { pretty_print: true });
let metadata = exporter.export(&hg, dir.path()).unwrap();
for filename in &[
"nodes.jsonl",
"edges.jsonl",
"hyperedges.jsonl",
"metadata.json",
] {
assert!(
dir.path().join(filename).exists(),
"Missing output file: {}",
filename
);
}
let nodes_content = std::fs::read_to_string(dir.path().join("nodes.jsonl")).unwrap();
let nodes: Vec<HypergraphNode> = nodes_content
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect();
assert_eq!(nodes.len(), metadata.num_nodes);
assert_eq!(nodes.len(), 22);
let node_ids: HashSet<&str> = nodes.iter().map(|n| n.id.as_str()).collect();
assert_eq!(node_ids.len(), nodes.len(), "Node IDs must be unique");
for node in &nodes {
assert_eq!(node.layer, HypergraphLayer::GovernanceControls);
assert!(!node.id.is_empty());
assert!(!node.entity_type.is_empty());
assert!(!node.label.is_empty());
}
let edges_content = std::fs::read_to_string(dir.path().join("edges.jsonl")).unwrap();
let edges: Vec<CrossLayerEdge> = edges_content
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect();
assert_eq!(edges.len(), metadata.num_edges);
for edge in &edges {
assert!(
node_ids.contains(edge.source_id.as_str()),
"Edge source '{}' not found in nodes",
edge.source_id
);
assert!(
node_ids.contains(edge.target_id.as_str()),
"Edge target '{}' not found in nodes",
edge.target_id
);
}
let he_content = std::fs::read_to_string(dir.path().join("hyperedges.jsonl")).unwrap();
let hyperedges: Vec<Hyperedge> = he_content
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect();
assert_eq!(hyperedges.len(), metadata.num_hyperedges);
let meta_content = std::fs::read_to_string(dir.path().join("metadata.json")).unwrap();
let parsed_meta: HypergraphMetadata = serde_json::from_str(&meta_content).unwrap();
assert_eq!(parsed_meta.num_nodes, nodes.len());
assert_eq!(parsed_meta.num_edges, edges.len());
assert_eq!(parsed_meta.num_hyperedges, hyperedges.len());
assert_eq!(parsed_meta.source, "datasynth");
assert!(!parsed_meta.generated_at.is_empty());
assert_eq!(parsed_meta.files.len(), 4);
let l1_count = parsed_meta
.layer_node_counts
.get("Governance & Controls")
.copied()
.unwrap_or(0);
assert_eq!(l1_count, 22);
}
#[test]
fn test_budget_enforcement() {
let config = HypergraphConfig {
max_nodes: 100, include_coso: true,
include_controls: false,
include_sox: false,
include_p2p: false,
include_o2c: false,
include_vendors: false,
include_customers: false,
include_employees: false,
..Default::default()
};
let mut builder = HypergraphBuilder::new(config);
builder.add_coso_framework();
let hg = builder.build();
assert!(
hg.nodes.len() <= 100,
"Node count {} exceeds budget of 100",
hg.nodes.len()
);
assert_eq!(hg.budget_report.total_used, hg.nodes.len());
}
#[test]
fn test_metadata_consistency() {
let config = HypergraphConfig {
max_nodes: 1000,
include_coso: true,
include_controls: false,
include_sox: false,
include_p2p: false,
include_o2c: false,
include_vendors: false,
include_customers: false,
include_employees: false,
..Default::default()
};
let mut builder = HypergraphBuilder::new(config);
builder.add_coso_framework();
let hg = builder.build();
let sum: usize = hg.metadata.layer_node_counts.values().sum();
assert_eq!(sum, hg.metadata.num_nodes);
let type_sum: usize = hg.metadata.node_type_counts.values().sum();
assert_eq!(type_sum, hg.metadata.num_nodes);
}
#[test]
fn test_edge_node_referential_integrity() {
let config = HypergraphConfig {
max_nodes: 1000,
include_coso: true,
include_controls: false,
include_sox: false,
include_p2p: false,
include_o2c: false,
include_vendors: false,
include_customers: false,
include_employees: false,
..Default::default()
};
let mut builder = HypergraphBuilder::new(config);
builder.add_coso_framework();
let hg = builder.build();
let node_ids: HashSet<&str> = hg.nodes.iter().map(|n| n.id.as_str()).collect();
for edge in &hg.edges {
assert!(
node_ids.contains(edge.source_id.as_str()),
"Edge references non-existent source node: {}",
edge.source_id
);
assert!(
node_ids.contains(edge.target_id.as_str()),
"Edge references non-existent target node: {}",
edge.target_id
);
}
}
#[test]
fn test_empty_hypergraph() {
let config = HypergraphConfig {
max_nodes: 1000,
include_coso: false,
include_controls: false,
include_sox: false,
include_p2p: false,
include_o2c: false,
include_vendors: false,
include_customers: false,
include_employees: false,
..Default::default()
};
let builder = HypergraphBuilder::new(config);
let hg = builder.build();
assert_eq!(hg.nodes.len(), 0);
assert_eq!(hg.edges.len(), 0);
assert_eq!(hg.hyperedges.len(), 0);
assert_eq!(hg.metadata.num_nodes, 0);
let dir = tempdir().unwrap();
let exporter = HypergraphExporter::new(HypergraphExportConfig::default());
let metadata = exporter.export(&hg, dir.path()).unwrap();
assert_eq!(metadata.num_nodes, 0);
let nodes_content = std::fs::read_to_string(dir.path().join("nodes.jsonl")).unwrap();
assert!(nodes_content.is_empty());
}
#[test]
fn test_coso_edge_layers() {
let config = HypergraphConfig {
max_nodes: 1000,
include_coso: true,
include_controls: false,
include_sox: false,
include_p2p: false,
include_o2c: false,
include_vendors: false,
include_customers: false,
include_employees: false,
..Default::default()
};
let mut builder = HypergraphBuilder::new(config);
builder.add_coso_framework();
let hg = builder.build();
for edge in &hg.edges {
assert_eq!(
edge.source_layer,
HypergraphLayer::GovernanceControls,
"COSO edge source should be GovernanceControls"
);
assert_eq!(
edge.target_layer,
HypergraphLayer::GovernanceControls,
"COSO edge target should be GovernanceControls"
);
}
assert_eq!(hg.edges.len(), 17);
}