use std::borrow::Cow; use std::collections::HashMap; use core_types::context::ContextDependencies; use core_types::uuid::NodeId; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork}; use graph_craft::graphene_compiler::Compiler; use graph_craft::{ProtoNodeIdentifier, Type, concrete}; use crate::{NetworkId, NodeMetadataSource, PeerId, Position, Registry}; /// Helper function to verify a NodeNetwork can be compiled successfully. /// Note: This only works for complete networks with all inputs resolved. /// Test networks with Import inputs will fail compilation (which is expected). fn verify_network_compiles(network: &NodeNetwork) -> Result<(), String> { let compiler = Compiler {}; compiler.compile_single(network.clone()).map_err(|e| format!("Compilation failed: {:?}", e))?; Ok(()) } /// Convert a runtime network to a storage `Registry`, returning the declarations alongside it. /// Proto-node declaration content is no longer stored in the registry (it lives in a byte store); /// these tests have no byte store, so they keep the extracted bytes in hand and rebuild a /// `Declarations` map for the back-conversion. fn to_registry(network: &NodeNetwork) -> (Registry, crate::Declarations) { let conversion = Registry::convert_from_runtime(network, &crate::NoMetadata, &Default::default(), PeerId(0)).expect("Failed to convert NodeNetwork to Registry"); let declarations = conversion.declarations().expect("rebuild declarations"); (conversion.registry, declarations) } /// A one-node network whose single node references `id` via a `TaggedValue::Resource` input, so /// `convert_resources` (which only snapshots network-referenced resources) carries the resource. fn network_referencing_resource(id: graphene_resource::ResourceId) -> NodeNetwork { network_referencing_resources(&[id]) } /// A network with one node per resource, each referencing its resource via a `TaggedValue::Resource` /// input, so all listed resources are network-referenced and survive conversion. fn network_referencing_resources(ids: &[graphene_resource::ResourceId]) -> NodeNetwork { use graph_craft::document::value::TaggedValue; let nodes = ids .iter() .enumerate() .map(|(i, id)| { ( NodeId(i as u64), DocumentNode { inputs: vec![NodeInput::value(TaggedValue::Resource(*id), false)], implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::identity::IdentityNode")), ..Default::default() }, ) }) .collect(); NodeNetwork { nodes, ..Default::default() } } fn create_simple_network() -> NodeNetwork { NodeNetwork { exports: vec![NodeInput::node(NodeId(1), 0)], nodes: [ ( NodeId(0), DocumentNode { inputs: vec![NodeInput::import(concrete!(u32), 0), NodeInput::import(concrete!(u32), 1)], implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::structural::ConsNode")), ..Default::default() }, ), ( NodeId(1), DocumentNode { inputs: vec![NodeInput::node(NodeId(0), 0)], implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::AddPairNode")), ..Default::default() }, ), ] .into_iter() .collect(), ..Default::default() } } /// Creates a network with a nested sub-network fn create_nested_network() -> NodeNetwork { // Create a simple inner network let inner_network = NodeNetwork { exports: vec![NodeInput::node(NodeId(10), 0)], nodes: [( NodeId(10), DocumentNode { inputs: vec![NodeInput::import(concrete!(u32), 0)], implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::identity::IdentityNode")), ..Default::default() }, )] .into_iter() .collect(), ..Default::default() }; // Create outer network that uses the inner network NodeNetwork { exports: vec![NodeInput::node(NodeId(1), 0)], nodes: [ ( NodeId(0), DocumentNode { inputs: vec![NodeInput::import(concrete!(u32), 0)], implementation: DocumentNodeImplementation::Network(inner_network), ..Default::default() }, ), ( NodeId(1), DocumentNode { inputs: vec![NodeInput::node(NodeId(0), 0)], implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::identity::IdentityNode")), ..Default::default() }, ), ] .into_iter() .collect(), ..Default::default() } } #[test] fn test_simple_round_trip() { let original_network = create_simple_network(); // Convert to Registry let (registry, declarations) = to_registry(&original_network); // Convert back to NodeNetwork let (converted_network, _) = registry.to_runtime_with_metadata(&declarations).expect("Failed to convert Registry back to NodeNetwork"); // Verify structure is preserved assert_eq!(converted_network.nodes.len(), original_network.nodes.len(), "Node count should be preserved"); assert_eq!(converted_network.exports.len(), original_network.exports.len(), "Export count should be preserved"); // Verify exports reference the correct nodes match (&original_network.exports[0], &converted_network.exports[0]) { ( NodeInput::Node { node_id: orig_id, output_index: orig_idx, }, NodeInput::Node { node_id: conv_id, output_index: conv_idx, }, ) => { assert_eq!(orig_id, conv_id, "Export should reference the same node"); assert_eq!(orig_idx, conv_idx, "Export output index should match"); } _ => panic!("Exports should both be Node inputs"), } // Verify node implementations are preserved for (node_id, orig_node) in &original_network.nodes { let conv_node = converted_network.nodes.get(node_id).expect("Node should exist after round-trip"); match (&orig_node.implementation, &conv_node.implementation) { (DocumentNodeImplementation::ProtoNode(orig_ident), DocumentNodeImplementation::ProtoNode(conv_ident)) => { assert_eq!(orig_ident.as_str(), conv_ident.as_str(), "ProtoNode identifier should be preserved"); } _ => panic!("Implementation type should be preserved"), } // Verify input count is preserved assert_eq!(conv_node.inputs.len(), orig_node.inputs.len(), "Input count should be preserved"); } } #[test] fn test_nested_network_round_trip() { let original_network = create_nested_network(); // Convert to Registry let (registry, declarations) = to_registry(&original_network); // Convert back to NodeNetwork let (converted_network, _) = registry.to_runtime_with_metadata(&declarations).expect("Failed to convert Registry back to NodeNetwork"); // Verify structure is preserved assert_eq!(converted_network.nodes.len(), original_network.nodes.len(), "Node count should be preserved"); // Find the node with nested network let orig_nested_node = original_network.nodes.get(&NodeId(0)).expect("Node 0 should exist"); let conv_nested_node = converted_network.nodes.get(&NodeId(0)).expect("Node 0 should exist after round-trip"); // Verify nested network is preserved match (&orig_nested_node.implementation, &conv_nested_node.implementation) { (DocumentNodeImplementation::Network(orig_inner), DocumentNodeImplementation::Network(conv_inner)) => { assert_eq!(orig_inner.nodes.len(), conv_inner.nodes.len(), "Inner network node count should be preserved"); assert_eq!(orig_inner.exports.len(), conv_inner.exports.len(), "Inner network export count should be preserved"); } _ => panic!("Nested network should be preserved"), } } #[test] fn test_registry_structure() { let network = create_simple_network(); let (registry, _declarations) = to_registry(&network); assert!(registry.resources.len() >= 2, "Should have proto-node declaration resources"); assert!(!registry.networks.is_empty(), "Should have at least one network"); let root_network = registry.networks.get(&crate::ROOT_NETWORK).expect("Root network should exist"); assert_eq!(root_network.exports.len(), network.exports.len(), "Export count should match"); // Exports are first-class slots, no synthetic identity nodes in node_instances. for slot in &root_network.exports { assert!(slot.target.is_some(), "Round-tripped exports should have a target"); } } #[test] fn test_nested_network_flattening() { let network = create_nested_network(); let registry = Registry::try_from(&network).expect("Failed to convert to Registry"); // Outer network has 2 nodes, one of which contains a nested network with 1 node. // No more identity-node padding, so node_instances has exactly the real nodes. let expected_nodes = 3; assert_eq!( registry.node_instances.len(), expected_nodes, "Registry should have exactly {} nodes, found {}", expected_nodes, registry.node_instances.len() ); // Two networks: root (ROOT_NETWORK) and nested (1). assert!(registry.networks.len() >= 2, "Should have at least 2 networks (root + nested)"); } #[test] fn test_metadata_preservation() { // Create a network with nodes that have non-default metadata let context_features = ContextDependencies::new( core_types::context::ContextFeatures::FOOTPRINT | core_types::context::ContextFeatures::REAL_TIME, core_types::context::ContextFeatures::empty(), ); let network = NodeNetwork { exports: vec![NodeInput::node(NodeId(1), 0)], nodes: [ ( NodeId(0), DocumentNode { inputs: vec![NodeInput::import(concrete!(f64), 0), NodeInput::import(Type::Generic(Cow::Borrowed("T")), 1)], implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("test::NodeWithMetadata")), call_argument: concrete!(String), context_features, visible: false, // Non-default value skip_deduplication: true, // Non-default value ..Default::default() }, ), ( NodeId(1), DocumentNode { inputs: vec![NodeInput::node(NodeId(0), 0)], implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("test::OutputNode")), call_argument: concrete!((u32, u32)), ..Default::default() }, ), ] .into_iter() .collect(), ..Default::default() }; // Convert to Registry and back let (registry, declarations) = to_registry(&network); let (converted, _) = registry.to_runtime_with_metadata(&declarations).expect("Failed to convert back to NodeNetwork"); // Verify call_argument is preserved let orig_node_0 = network.nodes.get(&NodeId(0)).unwrap(); let conv_node_0 = converted.nodes.get(&NodeId(0)).unwrap(); assert_eq!(orig_node_0.call_argument, conv_node_0.call_argument, "call_argument for node 0 should be preserved"); let orig_node_1 = network.nodes.get(&NodeId(1)).unwrap(); let conv_node_1 = converted.nodes.get(&NodeId(1)).unwrap(); assert_eq!(orig_node_1.call_argument, conv_node_1.call_argument, "call_argument for node 1 should be preserved"); // Verify context_features is preserved assert_eq!(orig_node_0.context_features, conv_node_0.context_features, "context_features should be preserved"); // Verify visible is preserved assert_eq!(orig_node_0.visible, conv_node_0.visible, "visible should be preserved"); // Verify skip_deduplication is preserved assert_eq!(orig_node_0.skip_deduplication, conv_node_0.skip_deduplication, "skip_deduplication should be preserved"); // Verify import_type is preserved for Import inputs match (&orig_node_0.inputs[0], &conv_node_0.inputs[0]) { (NodeInput::Import { import_type: orig_type, .. }, NodeInput::Import { import_type: conv_type, .. }) => { assert_eq!(orig_type, conv_type, "import_type for first import should be preserved (f64)"); } _ => panic!("First input should be Import"), } match (&orig_node_0.inputs[1], &conv_node_0.inputs[1]) { (NodeInput::Import { import_type: orig_type, .. }, NodeInput::Import { import_type: conv_type, .. }) => { assert_eq!(orig_type, conv_type, "import_type for second import should be preserved (generic T)"); } _ => panic!("Second input should be Import"), } } #[test] fn test_demo_artwork_round_trip() { use graph_craft::util::{DEMO_ART, load_from_name}; // Test each demo artwork for artwork_name in DEMO_ART { println!("Testing artwork: {}", artwork_name); let original_network = load_from_name(artwork_name); // Convert to Registry let (registry, declarations) = to_registry(&original_network); // Convert back to NodeNetwork let (converted_network, _) = registry .to_runtime_with_metadata(&declarations) .unwrap_or_else(|e| panic!("Failed to convert {} back to NodeNetwork: {:?}", artwork_name, e)); // Basic structural checks assert_eq!(original_network.nodes.len(), converted_network.nodes.len(), "{}: Node count should be preserved", artwork_name); assert_eq!(original_network.exports.len(), converted_network.exports.len(), "{}: Export count should be preserved", artwork_name); // Verify each node's metadata is preserved for (node_id, orig_node) in &original_network.nodes { let conv_node = converted_network .nodes .get(node_id) .unwrap_or_else(|| panic!("{}: Node {:?} should exist after round-trip", artwork_name, node_id)); // Check metadata fields assert_eq!( orig_node.call_argument, conv_node.call_argument, "{}: call_argument should be preserved for node {:?}", artwork_name, node_id ); assert_eq!( orig_node.context_features, conv_node.context_features, "{}: context_features should be preserved for node {:?}", artwork_name, node_id ); assert_eq!(orig_node.visible, conv_node.visible, "{}: visible should be preserved for node {:?}", artwork_name, node_id); assert_eq!( orig_node.skip_deduplication, conv_node.skip_deduplication, "{}: skip_deduplication should be preserved for node {:?}", artwork_name, node_id ); // Check input count assert_eq!( orig_node.inputs.len(), conv_node.inputs.len(), "{}: Input count should be preserved for node {:?}", artwork_name, node_id ); } // Verify the converted demo artwork can be compiled (demo artworks are complete networks) verify_network_compiles(&converted_network).unwrap_or_else(|e| panic!("{}: Converted artwork should compile successfully: {}", artwork_name, e)); println!("✓ {} passed", artwork_name); } } /// Per-node UI state used by the in-test metadata source. Keyed by `(network_path, local_id)`. #[derive(Clone, Debug, Default, PartialEq)] struct UiState { position: Option, is_layer: bool, display_name: Option, locked: bool, pinned: bool, } /// In-test `NodeMetadataSource` backed by a `HashMap` keyed on the full `(network_path, local_id)` /// addressing the editor would use. struct TestMetadata { entries: HashMap<(Vec, NodeId), UiState>, } impl TestMetadata { fn new() -> Self { Self { entries: HashMap::new() } } fn insert(&mut self, network_path: &[NodeId], local_id: NodeId, state: UiState) { self.entries.insert((network_path.to_vec(), local_id), state); } fn get(&self, network_path: &[NodeId], local_id: NodeId) -> Option<&UiState> { self.entries.get(&(network_path.to_vec(), local_id)) } } impl NodeMetadataSource for TestMetadata { fn position(&self, network_path: &[NodeId], local_id: NodeId) -> Option { self.get(network_path, local_id).and_then(|s| s.position) } fn is_layer(&self, network_path: &[NodeId], local_id: NodeId) -> bool { self.get(network_path, local_id).is_some_and(|s| s.is_layer) } fn display_name(&self, network_path: &[NodeId], local_id: NodeId) -> Option<&str> { self.get(network_path, local_id).and_then(|s| s.display_name.as_deref()) } fn locked(&self, network_path: &[NodeId], local_id: NodeId) -> bool { self.get(network_path, local_id).is_some_and(|s| s.locked) } fn pinned(&self, network_path: &[NodeId], local_id: NodeId) -> bool { self.get(network_path, local_id).is_some_and(|s| s.pinned) } } /// Round-trips a nested network with editor metadata: layer + absolute position on one node, /// node-in-chain on another, layer-in-stack inside a nested network. Asserts every entry comes /// back unchanged and addressed by the correct `(network_path, local_id)`. #[test] fn test_ui_metadata_round_trip() { let network = create_nested_network(); let mut metadata = TestMetadata::new(); // Root-network node 0 (the one with a nested network): a layer at an absolute position with // a display name. Editor `network_path` for root-network nodes is empty. metadata.insert( &[], NodeId(0), UiState { position: Some(Position::Absolute([3, 5])), is_layer: true, display_name: Some("Outer layer".into()), locked: true, pinned: false, }, ); // Root-network node 1: a plain node in a chain. metadata.insert( &[], NodeId(1), UiState { position: Some(Position::Chain), ..Default::default() }, ); // Nested-network node 10 (lives under node 0): a layer in a stack. metadata.insert( &[NodeId(0)], NodeId(10), UiState { position: Some(Position::Stack(7)), is_layer: true, ..Default::default() }, ); let conversion = Registry::convert_from_runtime(&network, &metadata, &Default::default(), PeerId(0)).expect("Failed to convert to Registry with metadata"); let declarations = conversion.declarations().expect("rebuild declarations"); let registry = conversion.registry; let (converted, entries) = registry.to_runtime_with_metadata(&declarations).expect("Failed to convert Registry back with metadata"); // Graph structure still round-trips. assert_eq!(converted.nodes.len(), network.nodes.len()); // Three entries — one per node we attached metadata to. assert_eq!(entries.len(), 3, "expected 3 metadata entries, got {}: {entries:#?}", entries.len()); // Look entries back up by their address so we don't rely on emission order. let lookup: HashMap<(Vec, NodeId), &crate::NodeMetadataEntry> = entries.iter().map(|e| ((e.network_path.clone(), e.local_id), e)).collect(); let root_layer = lookup.get(&(vec![], NodeId(0))).expect("entry for root-network layer node missing"); assert_eq!(root_layer.position, Some(Position::Absolute([3, 5]))); assert!(root_layer.is_layer); assert_eq!(root_layer.display_name.as_deref(), Some("Outer layer")); assert!(root_layer.locked); assert!(!root_layer.pinned); let root_node = lookup.get(&(vec![], NodeId(1))).expect("entry for root-network chain node missing"); assert_eq!(root_node.position, Some(Position::Chain)); assert!(!root_node.is_layer); let nested_layer = lookup.get(&(vec![NodeId(0)], NodeId(10))).expect("entry for nested layer-in-stack missing"); assert_eq!(nested_layer.position, Some(Position::Stack(7))); assert!(nested_layer.is_layer); } /// A runtime `ResourceRegistry` (source chain + resolved hash) survives conversion into the storage /// `Registry`: source bodies are preserved in priority order and the hash carries through. #[test] fn resources_round_trip_through_from_runtime() { use graphene_resource::{DataSource, ResourceHash, ResourceId, ResourceRegistry}; let mut resources = ResourceRegistry::new(); let id = ResourceId::new(); // Two sources in chain order: an embedded fallback then a URL. resources.push_source_back(&id, DataSource::Embedded); resources.push_source_back(&id, DataSource::Url("https://example.com/img.png".parse().unwrap())); let hash = ResourceHash::from(&b"image bytes"[..]); resources.resolve(&id, hash); // The resource must be referenced by a node to be snapshotted: `convert_resources` only carries // resources the network uses (orphans in the runtime cache, e.g. retained across undo, are dropped). let network = network_referencing_resource(id); let registry = Registry::from_runtime_with_metadata(&network, &crate::NoMetadata, &resources, PeerId(7)).expect("from_runtime failed"); let entry = registry.resources.get(&id).expect("resource entry present in storage registry"); assert_eq!(entry.hash, Some(hash), "resolved hash carried through"); assert_eq!(entry.sources.len(), 2, "both sources carried through"); // The chain iterates in priority order; decode bodies back to DataSource to compare. let decoded: Vec = entry.sources.iter().map(|(_, v)| serde_json::from_value(v.source.clone()).expect("source body decodes")).collect(); assert_eq!(decoded, vec![DataSource::Embedded, DataSource::Url("https://example.com/img.png".parse().unwrap())]); // All source keys carry the document peer. assert!(entry.sources.iter().all(|(key, _)| key.peer == PeerId(7)), "source keys scoped to the document peer"); } /// Full resource round-trip: a runtime `ResourceRegistry` converted into storage and back is equal /// to the original (source chains in order, resolved hashes preserved). #[test] fn resource_registry_round_trips_runtime_to_storage_to_runtime() { use graphene_resource::{DataSource, ResourceHash, ResourceId, ResourceRegistry}; let mut original = ResourceRegistry::new(); // A resolved resource with a two-entry fallback chain. let image = ResourceId::new(); original.push_source_back(&image, DataSource::Embedded); original.push_source_back(&image, DataSource::Url("https://example.com/img.png".parse().unwrap())); original.resolve(&image, ResourceHash::from(&b"image bytes"[..])); // An unresolved resource (sources but no hash yet). let font = ResourceId::new(); original.push_source_back( &font, DataSource::Font { family: "Inter".into(), style: Some("Bold".into()), }, ); // Both resources must be referenced by a node to be snapshotted (see `convert_resources`). let network = network_referencing_resources(&[image, font]); let registry = Registry::from_runtime_with_metadata(&network, &crate::NoMetadata, &original, PeerId(3)).expect("from_runtime failed"); let restored = registry.to_resource_registry().expect("to_resource_registry failed"); // Compare the two document resources specifically; the referencing nodes' proto-node declarations // also become resources in the registry, so the restored set is a superset of `original`. for id in [image, font] { assert_eq!( restored.info(&id).map(|info| info.sources), original.info(&id).map(|info| info.sources), "sources for {id:?} did not survive the round-trip" ); assert_eq!( restored.info(&id).and_then(|info| info.hash.copied()), original.info(&id).and_then(|info| info.hash.copied()), "resolved hash for {id:?} did not survive the round-trip" ); } } /// A resource present in the runtime cache but not referenced by any node is *not* snapshotted into the /// storage registry. This is the orphan case: undoing an image paste removes the node but the runtime /// keeps the resource alive for redo, so a later diff must not see the orphan as a new `AddResource` /// (which would resurface the undone paste as a phantom interaction). Regression guard for that divergence. #[test] fn unreferenced_runtime_resource_is_not_snapshotted() { use graphene_resource::{DataSource, ResourceHash, ResourceId, ResourceRegistry}; let referenced = ResourceId::new(); let orphan = ResourceId::new(); let mut resources = ResourceRegistry::new(); for id in [referenced, orphan] { resources.push_source_back(&id, DataSource::Embedded); resources.resolve(&id, ResourceHash::from(&b"bytes"[..])); } // Only `referenced` is wired to a node; `orphan` lingers in the cache (as it would after an undo). let network = network_referencing_resource(referenced); let registry = Registry::from_runtime_with_metadata(&network, &crate::NoMetadata, &resources, PeerId(1)).expect("from_runtime failed"); assert!(registry.resources.contains_key(&referenced), "the network-referenced resource must be snapshotted"); assert!(!registry.resources.contains_key(&orphan), "the unreferenced (orphan) resource must not be snapshotted"); } /// A node-input `TaggedValue::F64` must survive the storage round-trip bit-exact. Inputs are stored as a /// self-describing `serde_json::Value` (encoded with the registry's MessagePack codec), so this guards /// against any precision loss in the f64 -> serde_json::Number -> f64 path for a value with a full /// 17-significant-digit mantissa. #[test] fn node_input_f64_round_trips_bit_exact() { use graph_craft::document::value::TaggedValue; // A value whose exact f64 bits matter: 1/3-ish with a non-terminating binary expansion. let precise = 107.33334350585939_f64; let network = NodeNetwork { nodes: [( NodeId(0), DocumentNode { inputs: vec![NodeInput::value(TaggedValue::F64(precise), false)], implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::identity::IdentityNode")), ..Default::default() }, )] .into_iter() .collect(), ..Default::default() }; let (registry, declarations) = to_registry(&network); let (converted, _) = registry.to_runtime_with_metadata(&declarations).expect("to_runtime"); let input = &converted.nodes.get(&NodeId(0)).expect("node 0").inputs[0]; let NodeInput::Value { tagged_value, .. } = input else { panic!("expected a value input, got {input:?}") }; let TaggedValue::F64(actual) = &**tagged_value else { panic!("expected F64, got {:?}", tagged_value) }; assert_eq!(actual.to_bits(), precise.to_bits(), "f64 node input drifted: {actual} != {precise}"); } /// Two storage nodes in one network carrying the same `ORIGINAL_NODE_ID` both map to one runtime ID. /// Conversion must reject this rather than silently collapse them and drop a node. #[test] fn duplicate_runtime_node_id_is_rejected() { use crate::AttributesWrite; use crate::TimeStamp; use crate::to_runtime::ConversionError; let (mut registry, declarations) = to_registry(&create_simple_network()); // Force both root-network nodes onto the same runtime ID. for node in registry.node_instances.values_mut() { node.attributes.set(crate::attr::node::ORIGINAL_NODE_ID, serde_json::json!(7), TimeStamp::ORIGIN); } let error = registry.to_runtime_with_metadata(&declarations).expect_err("duplicate runtime ID must error"); assert!( matches!(error, ConversionError::DuplicateRuntimeNodeId { runtime_id: 7, .. }), "expected DuplicateRuntimeNodeId, got {error:?}" ); } /// A node input referencing a node in a different network can't be remapped to a valid local runtime /// ID, so conversion must reject it rather than emit a dangling reference. #[test] fn cross_network_reference_is_rejected() { use crate::to_runtime::ConversionError; use crate::{Network, NodeInput}; let (mut registry, declarations) = to_registry(&create_simple_network()); // `create_simple_network` wires one node's input to another, both in the root network. Find the // referenced storage ID, then move that node into a fresh second network so the reference crosses // a network boundary. let referenced_storage_id = registry .node_instances .values() .flat_map(|node| node.inputs()) .find_map(|slot| match slot.input { NodeInput::Node { id: node_id, .. } => Some(node_id), _ => None, }) .expect("simple network has a node-to-node reference"); let other_network = NetworkId(999); registry.networks.insert(other_network, Network::default()); registry.node_instances.get_mut(&referenced_storage_id).expect("referenced node exists").network = other_network; let error = registry.to_runtime_with_metadata(&declarations).expect_err("cross-network reference must error"); assert!(matches!(error, ConversionError::CrossNetworkReference { .. }), "expected CrossNetworkReference, got {error:?}"); } /// A network's `scope_injections` (key -> (NodeId, Type)) must survive a storage round trip, with the /// node reference resolved back to the same runtime-local ID it pointed at originally. #[test] fn scope_injections_round_trip() { let mut network = create_simple_network(); network.scope_injections.insert("editor-api".to_string(), (NodeId(0), concrete!(u32))); let (registry, declarations) = to_registry(&network); let (converted, _) = registry.to_runtime_with_metadata(&declarations).expect("to_runtime"); let (node_id, ty) = converted.scope_injections.get("editor-api").expect("scope injection must survive the round trip"); assert_eq!(*node_id, NodeId(0), "the injection's node reference must resolve back to its original runtime ID"); assert_eq!(*ty, concrete!(u32), "the injection's type must be preserved"); } /// A stored scope injection whose node reference no longer resolves (node removed, or moved to another /// network) must error rather than emit an injection pointing at a nonexistent runtime node. #[test] fn dangling_scope_injection_is_rejected() { use crate::AttributesWrite; use crate::TimeStamp; use crate::to_runtime::ConversionError; let (mut registry, declarations) = to_registry(&create_simple_network()); // Store an injection pointing at a storage ID that no node carries, leaving the reference dangling // while the rest of the graph stays valid. The root network is whichever one holds the nodes. let root_network_id = registry.node_instances.values().next().expect("simple network has nodes").network(); let injections: HashMap = [("editor-api".to_string(), (crate::NodeId(u64::MAX), concrete!(u32)))].into_iter().collect(); registry .networks .get_mut(&root_network_id) .expect("root network exists") .attributes .set_serialized(crate::attr::network::SCOPE_INJECTIONS, &injections, TimeStamp::ORIGIN) .expect("serialize injections"); let error = registry.to_runtime_with_metadata(&declarations).expect_err("dangling scope injection must error"); assert!(matches!(error, ConversionError::DanglingScopeInjection { .. }), "expected DanglingScopeInjection, got {error:?}"); } #[test] fn cyclic_network_reference_is_rejected() { use crate::to_runtime::ConversionError; use crate::{Implementation, Network, Node}; // A runtime `NodeNetwork` embeds children by value and so can't be cyclic; the cycle only exists // in the storage form, where networks reference each other by `NetworkId`. Build it directly: // the root network holds a node whose implementation is the child network, whose own node points // back at the root, closing the loop. let child_network_id = NetworkId(1); let mut registry = Registry::default(); registry.networks.insert(crate::ROOT_NETWORK, Network::default()); registry.networks.insert(child_network_id, Network::default()); registry.node_instances.insert( crate::NodeId(0), Node { implementation: Implementation::Network(child_network_id), inputs: Vec::new(), attributes: crate::Attributes::default(), network: crate::ROOT_NETWORK, }, ); registry.node_instances.insert( crate::NodeId(1), Node { implementation: Implementation::Network(crate::ROOT_NETWORK), inputs: Vec::new(), attributes: crate::Attributes::default(), network: child_network_id, }, ); let error = registry.to_runtime_with_metadata(&crate::Declarations::new()).expect_err("cyclic network reference must error"); assert!(matches!(error, ConversionError::CyclicNetwork(_)), "expected CyclicNetwork, got {error:?}"); }