From d9c8532e71d83dd735cf3902903fb61f17a6cbec Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Fri, 31 Jul 2026 10:18:00 +0000 Subject: [PATCH] Test retain filter placement, source set divergence, and excluding keying --- node-graph/graph-craft/src/proto.rs | 113 ++++++++++++++++++ .../libraries/core-types/src/context.rs | 15 +++ 2 files changed, 128 insertions(+) diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index f7ae796008..0631fcdcc1 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -963,6 +963,81 @@ mod test { ); } + #[test] + fn retain_filter_placement_on_source_free_branch() { + let mut network = source_branch_network(vec![1], vec![]); + network.insert_context_nullification_nodes().expect("Error when calling 'insert_context_nullification_nodes'"); + + let filters = nullification_filters(&network); + assert_eq!(filters.len(), 1, "only the source-free branch gets a filter"); + let (filter_id, wrapped, retained) = &filters[0]; + assert_eq!(wrapped, "source_b"); + assert!(retained.is_empty(), "the source-free branch retains no sources"); + + let (source_a_id, _) = find_node(&network, "source_a"); + let (_, join) = find_node(&network, "join"); + let ConstructionArgs::Nodes(join_args) = &join.construction_args else { + panic!("join args must be nodes") + }; + assert_eq!(join_args, &vec![source_a_id, *filter_id], "the source branch stays direct, the filter replaces the source-free branch"); + } + + #[test] + fn diverging_source_sets_filter_each_branch() { + let mut network = source_branch_network(vec![1], vec![2]); + network.insert_context_nullification_nodes().expect("Error when calling 'insert_context_nullification_nodes'"); + + let mut filters = nullification_filters(&network); + filters.sort_by(|(_, a, _), (_, b, _)| a.cmp(b)); + let summary: Vec<_> = filters.iter().map(|(_, wrapped, retained)| (wrapped.as_str(), retained.as_slice())).collect(); + assert_eq!( + summary, + vec![("source_a", &[1u64][..]), ("source_b", &[2u64][..])], + "each diverging branch is filtered down to its own source set" + ); + } + + #[test] + fn matching_source_sets_insert_no_filter() { + let mut network = source_branch_network(vec![1], vec![1]); + network.insert_context_nullification_nodes().expect("Error when calling 'insert_context_nullification_nodes'"); + + assert!(nullification_filters(&network).is_empty(), "equal branch source sets need no filter"); + } + + fn find_node<'a>(network: &'a ProtoNetwork, name: &str) -> (NodeId, &'a ProtoNode) { + network + .nodes + .iter() + .find(|(_, node)| node.identifier.as_str() == name) + .map(|(id, node)| (*id, node)) + .unwrap_or_else(|| panic!("node {name} not found")) + } + + fn nullification_filters(network: &ProtoNetwork) -> Vec<(NodeId, String, Vec)> { + let node = |id: NodeId| &network.nodes[id.0 as usize].1; + network + .nodes + .iter() + .filter(|(_, candidate)| candidate.identifier.as_str() == graphene_core::context_modification::context_modification::IDENTIFIER.as_str()) + .map(|(id, candidate)| { + let ConstructionArgs::Nodes(args) = &candidate.construction_args else { + panic!("filter args must be nodes") + }; + let ConstructionArgs::Nodes(memoized) = &node(args[0]).construction_args else { + panic!("filter memoize args must be nodes") + }; + let ConstructionArgs::Value(value) = &node(args[1]).construction_args else { + panic!("filter payload must be a value") + }; + let value::TaggedValue::ContextModification(modification) = &**value else { + panic!("filter payload must be a context modification") + }; + (*id, node(memoized[0]).identifier.as_str().to_string(), modification.sources.clone()) + }) + .collect() + } + fn test_network() -> ProtoNetwork { ProtoNetwork { inputs: vec![NodeId(10)], @@ -1019,6 +1094,44 @@ mod test { } } + fn source_branch_network(branch_a_sources: Vec, branch_b_sources: Vec) -> ProtoNetwork { + let branch = |name: &str, sources: Vec| ProtoNode { + identifier: ProtoNodeIdentifier::with_owned_string(name.to_string()), + call_argument: concrete!(()), + construction_args: ConstructionArgs::Nodes(vec![NodeId(0)]), + context_features: ContextDependencies { sources, ..Default::default() }, + ..Default::default() + }; + ProtoNetwork { + inputs: vec![], + output: NodeId(3), + nodes: [ + ( + NodeId(0), + ProtoNode { + identifier: ProtoNodeIdentifier::new("value"), + call_argument: concrete!(()), + construction_args: ConstructionArgs::Value(value::TaggedValue::U32(2).into()), + ..Default::default() + }, + ), + (NodeId(1), branch("source_a", branch_a_sources)), + (NodeId(2), branch("source_b", branch_b_sources)), + ( + NodeId(3), + ProtoNode { + identifier: ProtoNodeIdentifier::new("join"), + call_argument: concrete!(()), + construction_args: ConstructionArgs::Nodes(vec![NodeId(1), NodeId(2)]), + ..Default::default() + }, + ), + ] + .into_iter() + .collect(), + } + } + fn test_network_with_cycles() -> ProtoNetwork { ProtoNetwork { inputs: vec![NodeId(1)], diff --git a/node-graph/libraries/core-types/src/context.rs b/node-graph/libraries/core-types/src/context.rs index 837b3e9e02..472d8717fe 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -1434,6 +1434,21 @@ mod context_impl_tests { assert_ne!(hash_with(&initial), hash_with(&bumped_retained), "retained source bumps must invalidate"); } + #[test] + fn excluding_keys_ignore_own_source_bumps() { + let arena = Arena::new(64); + let initial = [(7, 1), (9, 5)]; + let own_bumped = [(7, 2), (9, 5)]; + let other_bumped = [(7, 1), (9, 6)]; + + let hash_with = |generations: &[(SourceId, u64)]| { + let scope = scope_fixture(generations, &arena).excluding(7); + hash_of(&ContextImpl::root(&scope)) + }; + assert_eq!(hash_with(&initial), hash_with(&own_bumped), "a source's own generation bump must not change its slot key"); + assert_ne!(hash_with(&initial), hash_with(&other_bumped), "bumps of other sources must change the slot key"); + } + #[test] fn unretained_scope_sees_every_bump() { let arena = Arena::new(64);