1use super::{Direction, Graph, GraphNode};
20use crate::{Arity, NodeType, graphs::node::InnovationId, node::Node};
21use radiate_core::{RdRand, Valid, random_provider};
22use radiate_utils::SortedBuffer;
23use std::{fmt::Debug, ops::Deref};
24
25const SOURCE_NODE_TYPES: &[NodeType] = &[NodeType::Input, NodeType::Vertex, NodeType::Edge];
26const TARGET_NODE_TYPES: &[NodeType] = &[NodeType::Output, NodeType::Vertex, NodeType::Edge];
27const MAX_REPAIR_ATTEMPTS: usize = 10;
28
29#[derive(Debug, Clone)]
34pub enum MutationStep {
35 AddNode(usize),
36 AddEdge(usize, usize),
37 RemoveEdge(usize, usize),
38 DirectionChange {
39 index: usize,
40 previous_direction: Direction,
41 },
42 InnovationChange {
43 node_idx: usize,
44 previous_innovation: Option<InnovationId>,
45 },
46}
47
48#[derive(Clone)]
54pub enum ReplayStep<T> {
55 AddNode(usize, Option<GraphNode<T>>),
56 AddEdge(usize, usize),
57 RemoveEdge(usize, usize),
58 DirectionChange(usize, Direction),
59 InnovationChange(usize, Option<InnovationId>),
60}
61
62pub enum TransactionResult<T> {
69 Valid(Vec<MutationStep>),
70 Invalid(Vec<MutationStep>, Vec<ReplayStep<T>>),
71}
72
73impl<T> TransactionResult<T> {
74 pub fn is_valid(&self) -> bool {
75 matches!(self, TransactionResult::Valid(_))
76 }
77
78 pub fn is_invalid(&self) -> bool {
79 matches!(self, TransactionResult::Invalid(_, _))
80 }
81
82 pub fn replay(&self, graph: &mut Graph<T>)
83 where
84 T: Clone,
85 {
86 if let TransactionResult::Invalid(_, replay_steps) = self {
87 let mut transaction = GraphTransaction::new(graph);
88 transaction.replay(replay_steps.clone());
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum InsertStep {
98 Detach(usize, usize),
99 Connect(usize, usize),
100 NewStructure(usize, usize, usize, NodeType),
101 Invalid,
102}
103
104pub struct GraphTransaction<'a, T> {
112 graph: &'a mut Graph<T>,
113 steps: Vec<MutationStep>,
114 effects: SortedBuffer<usize>,
115}
116
117impl<'a, T> GraphTransaction<'a, T> {
118 pub fn new(graph: &'a mut Graph<T>) -> Self {
119 GraphTransaction {
120 graph,
121 steps: Vec::with_capacity(5),
122 effects: SortedBuffer::new(),
123 }
124 }
125
126 pub fn commit(self) -> TransactionResult<T> {
132 self.commit_internal::<fn(&Graph<T>) -> bool>(None)
133 }
134
135 pub fn commit_with(self, validator: impl Fn(&Graph<T>) -> bool) -> TransactionResult<T> {
139 self.commit_internal(Some(validator))
140 }
141
142 pub fn try_commit(mut self) -> TransactionResult<T> {
147 let mut repaired = false;
148 let mut attempts = 0;
149
150 self.set_cycles();
151 while !repaired && attempts < MAX_REPAIR_ATTEMPTS {
152 repaired = self.repair_invalid_nodes();
153 if repaired {
154 self.set_cycles();
155 }
156
157 attempts += 1;
158 }
159
160 self.commit()
161 }
162
163 pub fn push(&mut self, node: impl Into<GraphNode<T>>) -> usize {
165 let index = self.graph.len();
166 self.steps.push(MutationStep::AddNode(index));
167 self.graph.push(node);
168 SortedBuffer::insert_sorted_unique(&mut self.effects, index);
169 index
170 }
171
172 pub fn attach(&mut self, from: usize, to: usize) {
174 self.steps.push(MutationStep::AddEdge(from, to));
175 self.graph.attach(from, to);
176 SortedBuffer::insert_sorted_unique(&mut self.effects, from);
177 SortedBuffer::insert_sorted_unique(&mut self.effects, to);
178 }
179
180 pub fn detach(&mut self, from: usize, to: usize) {
182 self.steps.push(MutationStep::RemoveEdge(from, to));
183 self.graph.detach(from, to);
184 SortedBuffer::insert_sorted_unique(&mut self.effects, from);
185 SortedBuffer::insert_sorted_unique(&mut self.effects, to);
186 }
187
188 pub fn change_direction(&mut self, index: usize, direction: Direction) {
192 if let Some(node) = self.graph.get_mut(index) {
193 if node.direction() == direction {
194 return;
195 }
196
197 self.steps.push(MutationStep::DirectionChange {
198 index,
199 previous_direction: node.direction(),
200 });
201 node.set_direction(direction);
202 }
203 }
204
205 pub fn rollback(self) -> Vec<ReplayStep<T>> {
210 let mut replay_steps = Vec::new();
211 for step in self.steps.into_iter().rev() {
212 match step {
213 MutationStep::AddNode(_) => {
214 let added_node = self.graph.pop();
215 replay_steps.push(ReplayStep::AddNode(self.graph.len(), added_node));
216 }
217 MutationStep::AddEdge(from, to) => {
218 self.graph.detach(from, to);
219 replay_steps.push(ReplayStep::AddEdge(from, to));
220 }
221 MutationStep::RemoveEdge(from, to) => {
222 self.graph.attach(from, to);
223 replay_steps.push(ReplayStep::RemoveEdge(from, to));
224 }
225 MutationStep::DirectionChange {
226 index,
227 previous_direction,
228 ..
229 } => {
230 if let Some(node) = self.graph.get_mut(index) {
231 let prev_dir = node.direction();
232 node.set_direction(previous_direction);
233 replay_steps.push(ReplayStep::DirectionChange(index, prev_dir));
234 }
235 }
236 MutationStep::InnovationChange {
237 node_idx,
238 previous_innovation,
239 } => {
240 if let Some(node) = self.graph.get_mut(node_idx) {
241 let current_innovation = node.innovation();
242 node.set_innovation(previous_innovation);
243 replay_steps
244 .push(ReplayStep::InnovationChange(node_idx, current_innovation));
245 }
246 }
247 }
248 }
249
250 replay_steps.reverse();
251 replay_steps
252 }
253
254 pub fn replay(&mut self, steps: Vec<ReplayStep<T>>) {
258 for step in steps {
259 match step {
260 ReplayStep::AddNode(_, node) => {
261 if let Some(node) = node {
262 self.push(node);
263 }
264 }
265 ReplayStep::AddEdge(from, to) => {
266 self.attach(from, to);
267 }
268 ReplayStep::RemoveEdge(from, to) => {
269 self.detach(from, to);
270 }
271 ReplayStep::DirectionChange(index, direction) => {
272 self.change_direction(index, direction);
273 }
274 ReplayStep::InnovationChange(node_idx, innovation) => {
275 self.set_innovation(node_idx, innovation);
276 }
277 }
278 }
279 }
280
281 pub fn set_cycles(&mut self) {
285 let effects = self.effects.clone();
286
287 for &idx in effects.iter() {
288 let node_cycles = self.graph.get_cycles(idx);
289
290 if node_cycles.is_empty() {
291 self.change_direction(idx, Direction::Forward);
292 } else {
293 for cycle_idx in node_cycles {
294 self.change_direction(cycle_idx, Direction::Backward);
295 }
296 }
297 }
298 }
299
300 pub fn set_innovation(&mut self, node_idx: usize, innovation: Option<InnovationId>) {
301 if let Some(node) = self.graph.get_mut(node_idx) {
302 let previous_innovation = node.innovation();
303 node.set_innovation(innovation);
304 self.steps.push(MutationStep::InnovationChange {
305 node_idx,
306 previous_innovation,
307 });
308 }
309 }
310
311 #[inline]
319 pub fn get_insertion_steps(
320 &self,
321 source_idx: usize,
322 target_idx: usize,
323 new_node_idx: usize,
324 rand: &mut RdRand,
325 ) -> Vec<InsertStep> {
326 let target_node = self.graph.get(target_idx).unwrap();
327 let source_node = self.graph.get(source_idx).unwrap();
328 let new_node = self.graph.get(new_node_idx).unwrap();
329
330 let mut steps = Vec::with_capacity(4);
331
332 let source_is_edge = source_node.node_type() == NodeType::Edge;
333 let target_is_edge = target_node.node_type() == NodeType::Edge;
334 let new_node_arity = new_node.arity();
335
336 if new_node_arity == Arity::Zero && !target_node.is_locked() {
337 steps.push(InsertStep::Connect(new_node_idx, target_idx));
338 return steps;
339 }
340
341 if source_is_edge {
342 let source_outgoing = *rand.choose(source_node.outgoing());
343
344 if source_outgoing == new_node_idx {
345 steps.push(InsertStep::Connect(source_idx, new_node_idx));
346 } else {
347 steps.push(InsertStep::Connect(source_idx, new_node_idx));
348 steps.push(InsertStep::Connect(new_node_idx, source_outgoing));
349 steps.push(InsertStep::Detach(source_idx, source_outgoing));
350 steps.push(InsertStep::NewStructure(
351 source_idx,
352 new_node_idx,
353 source_outgoing,
354 source_node.node_type(),
355 ));
356 }
357 } else if target_is_edge || target_node.is_locked() {
358 let target_incoming = *rand.choose(target_node.incoming());
359
360 if target_incoming == new_node_idx {
361 steps.push(InsertStep::Connect(target_incoming, new_node_idx));
362 } else {
363 steps.push(InsertStep::Connect(target_incoming, new_node_idx));
364 steps.push(InsertStep::Connect(new_node_idx, target_idx));
365 steps.push(InsertStep::Detach(target_incoming, target_idx));
366 steps.push(InsertStep::NewStructure(
367 target_incoming,
368 new_node_idx,
369 target_idx,
370 target_node.node_type(),
371 ));
372 }
373 } else {
374 steps.push(InsertStep::Connect(source_idx, new_node_idx));
375 steps.push(InsertStep::Connect(new_node_idx, target_idx));
376 steps.push(InsertStep::NewStructure(
377 source_idx,
378 new_node_idx,
379 target_idx,
380 new_node.node_type(),
381 ));
382 }
383
384 steps
385 }
386
387 #[inline]
396 pub fn random_source_node(&self, rand: &mut RdRand) -> Option<&GraphNode<T>> {
397 self.random_node_of_type(SOURCE_NODE_TYPES, rand)
398 }
399
400 #[inline]
403 pub fn random_target_node(&self, rand: &mut RdRand) -> Option<&GraphNode<T>> {
404 self.random_node_of_type(TARGET_NODE_TYPES, rand)
405 }
406
407 #[inline]
410 pub fn random_target_node_where<F>(&self, rand: &mut RdRand, filter: F) -> Option<&GraphNode<T>>
411 where
412 F: Fn(&GraphNode<T>) -> bool,
413 {
414 let candidates = self
415 .iter()
416 .filter(|node| TARGET_NODE_TYPES.contains(&node.node_type()) && filter(node))
417 .collect::<Vec<&GraphNode<T>>>();
418
419 if candidates.is_empty() {
420 return None;
421 }
422
423 Some(*rand.choose(&candidates))
424 }
425
426 #[inline]
429 fn random_source_node_where<F>(&self, rand: &mut RdRand, filter: F) -> Option<&GraphNode<T>>
430 where
431 F: Fn(&GraphNode<T>) -> bool,
432 {
433 let candidates = self
434 .iter()
435 .filter(|node| SOURCE_NODE_TYPES.contains(&node.node_type()) && filter(node))
436 .collect::<Vec<&GraphNode<T>>>();
437
438 if candidates.is_empty() {
439 return None;
440 }
441
442 Some(*rand.choose(&candidates))
443 }
444
445 fn repair_invalid_nodes(&mut self) -> bool {
446 if self.is_valid() {
447 return false;
448 }
449
450 let mut repaired = false;
451
452 let invalid_nodes = self
453 .iter()
454 .filter(|node| !node.is_valid())
455 .map(|n| n.index())
456 .collect::<Vec<usize>>();
457
458 for idx in invalid_nodes.iter() {
459 let arity = self.graph[*idx].arity();
460 match arity {
461 Arity::Zero if self.repair_zero_arity_node(*idx) => {
462 repaired = true;
463 }
464 Arity::Exact(_) if self.repair_exact_arity_node(*idx) => {
465 repaired = true;
466 }
467 _ => {}
468 }
469 }
470
471 repaired
472 }
473
474 fn repair_zero_arity_node(&mut self, node_idx: usize) -> bool {
475 let node = self.graph.get(node_idx).unwrap();
476 if node.arity() != Arity::Zero {
477 return false;
478 }
479
480 if node.outgoing().is_empty() {
481 let random_target = random_provider::with_rng(|rand| {
482 self.random_target_node_where(rand, |n| !n.is_locked() && n.index() != node_idx)
483 .map(|n| n.index())
484 });
485
486 if let Some(target) = random_target {
487 self.attach(node.index(), target);
488
489 if !self.graph[node_idx].outgoing().is_empty() {
490 return true;
491 }
492 }
493 }
494
495 false
496 }
497
498 fn repair_exact_arity_node(&mut self, node_idx: usize) -> bool {
499 let arity = self.graph[node_idx].arity();
500 if let Arity::Exact(n) = arity {
501 let current_incoming = self.graph[node_idx].incoming().len();
502 if current_incoming < n {
503 let needed = n - current_incoming;
504
505 let available_sources = random_provider::with_rng(|rand| {
506 (0..needed)
507 .filter_map(|_| {
508 self.random_source_node_where(rand, |n| {
509 !n.is_locked() && n.index() != node_idx
510 })
511 .map(|n| n.index())
512 })
513 .collect::<Vec<usize>>()
514 });
515
516 for src in available_sources {
517 self.attach(src, node_idx);
518 }
519
520 if self.graph[node_idx].incoming().len() == n {
521 return true;
522 }
523 } else if current_incoming > n {
524 let to_detach = current_incoming - n;
525
526 let valid_incoming = self.graph[node_idx]
527 .incoming()
528 .iter()
529 .cloned()
530 .filter(|incoming| self.graph[*incoming].outgoing().len() > 1)
531 .collect::<Vec<usize>>();
532
533 let rand_indices = random_provider::shuffled_indices(0..valid_incoming.len());
534 let rand_indices = &rand_indices[0..to_detach];
535
536 for &i in rand_indices.iter() {
537 let source_idx = valid_incoming[i];
538 self.detach(source_idx, node_idx);
539 }
540
541 if self.graph[node_idx].incoming().len() == n {
542 return true;
543 }
544 }
545 }
546
547 false
548 }
549
550 #[inline]
554 fn random_node_of_type(
555 &self,
556 node_types: &[NodeType],
557 rand: &mut RdRand,
558 ) -> Option<&GraphNode<T>> {
559 if node_types.is_empty() {
560 return None;
561 }
562
563 let gene_node_type = rand.choose(node_types);
564
565 let genes = match gene_node_type {
566 NodeType::Input => self
567 .iter()
568 .filter(|node| node.node_type() == NodeType::Input)
569 .collect::<Vec<&GraphNode<T>>>(),
570 NodeType::Output => self
571 .iter()
572 .filter(|node| node.node_type() == NodeType::Output)
573 .collect::<Vec<&GraphNode<T>>>(),
574 NodeType::Vertex => self
575 .iter()
576 .filter(|node| node.node_type() == NodeType::Vertex)
577 .collect::<Vec<&GraphNode<T>>>(),
578 NodeType::Edge => self
579 .iter()
580 .filter(|node| node.node_type() == NodeType::Edge)
581 .collect::<Vec<&GraphNode<T>>>(),
582 _ => vec![],
583 };
584
585 if genes.is_empty() {
586 return self.random_node_of_type(
587 node_types
588 .iter()
589 .filter(|nt| *nt != gene_node_type)
590 .cloned()
591 .collect::<Vec<NodeType>>()
592 .as_slice(),
593 rand,
594 );
595 }
596
597 Some(*rand.choose(&genes))
598 }
599
600 fn commit_internal<F: Fn(&Graph<T>) -> bool>(
601 mut self,
602 validator: Option<F>,
603 ) -> TransactionResult<T> {
604 self.set_cycles();
605 let result_steps = self.steps.iter().map(|step| (*step).clone()).collect();
606
607 if let Some(validator) = validator {
608 return if validator(self.graph) && self.is_valid() {
609 TransactionResult::Valid(result_steps)
610 } else {
611 let replay_steps = self.rollback();
612 TransactionResult::Invalid(result_steps, replay_steps)
613 };
614 }
615
616 if self.is_valid() {
617 TransactionResult::Valid(result_steps)
618 } else {
619 let replay_steps = self.rollback();
620 TransactionResult::Invalid(result_steps, replay_steps)
621 }
622 }
623}
624
625impl<T> Deref for GraphTransaction<'_, T> {
626 type Target = Graph<T>;
627
628 fn deref(&self) -> &Self::Target {
629 self.graph
630 }
631}
632
633#[cfg(test)]
634mod tests {
635 use super::{GraphTransaction, InsertStep, MutationStep, TransactionResult};
636 use crate::collections::graphs::{Direction, Graph, GraphNode, InnovationId};
637 use crate::{Arity, Node, NodeType};
638 use radiate_core::{Valid, random_provider};
639
640 fn assert_has_direction_change(steps: &[MutationStep], idxs: &[usize]) {
641 let mut seen = vec![];
642 for s in steps {
643 if let MutationStep::DirectionChange { index, .. } = s {
644 seen.push(*index);
645 }
646 }
647 for idx in idxs {
648 assert!(
649 seen.contains(idx),
650 "Expected DirectionChange for node {} not found in steps: {:?}",
651 idx,
652 steps
653 );
654 }
655 }
656
657 #[test]
658 fn commit_valid_add_and_attach() {
659 let mut g = Graph::<i32>::default();
660 let mut tx = GraphTransaction::new(&mut g);
661
662 let i = tx.push((0, NodeType::Input, 0));
663 let o = tx.push((1, NodeType::Output, 1));
664 tx.attach(i, o);
665
666 match tx.commit() {
667 TransactionResult::Valid(steps) => {
668 assert_eq!(steps.len(), 3);
669 assert!(matches!(steps[0], MutationStep::AddNode(0)));
670 assert!(matches!(steps[1], MutationStep::AddNode(1)));
671 assert!(matches!(steps[2], MutationStep::AddEdge(0, 1)));
672 assert!(g.is_valid());
673 assert_eq!(g[0].outgoing().len(), 1);
674 assert_eq!(g[1].incoming().len(), 1);
675 assert_eq!(g[0].direction(), Direction::Forward);
676 assert_eq!(g[1].direction(), Direction::Forward);
677 }
678 _ => panic!("expected Valid"),
679 }
680 }
681
682 #[test]
683 fn commit_invalid_rolls_back_and_replay_restores() {
684 let mut g = Graph::<i32>::default();
685
686 let mut tx = GraphTransaction::new(&mut g);
688 let input = tx.push((0, NodeType::Input, 0));
689 let vertex = tx.push((1, NodeType::Vertex, 1, Arity::Exact(2)));
690 let output = tx.push((2, NodeType::Output, 2));
691
692 tx.attach(input, vertex);
693 tx.attach(vertex, output);
694
695 let (steps, replay) = match tx.commit() {
696 TransactionResult::Invalid(steps, replay) => (steps, replay),
697 _ => panic!("expected Invalid"),
698 };
699
700 assert_eq!(g.len(), 0, "graph should be rolled back to empty");
702 assert!(g.is_valid());
703
704 let mut tx2 = GraphTransaction::new(&mut g);
706 tx2.replay(replay);
707
708 assert_eq!(g.len(), 3);
709 assert_eq!(g[0].node_type(), NodeType::Input);
710 assert_eq!(g[1].node_type(), NodeType::Vertex);
711 assert_eq!(g[2].node_type(), NodeType::Output);
712 assert!(g[0].outgoing().contains(&1));
713 assert!(g[1].incoming().contains(&0));
714 assert!(g[1].outgoing().contains(&2));
715 assert!(g[2].incoming().contains(&1));
716
717 assert!(steps.iter().any(|s| matches!(s, MutationStep::AddNode(0))));
719 assert!(steps.iter().any(|s| matches!(s, MutationStep::AddNode(1))));
720 assert!(steps.iter().any(|s| matches!(s, MutationStep::AddNode(2))));
721 assert!(
722 steps
723 .iter()
724 .any(|s| matches!(s, MutationStep::AddEdge(0, 1)))
725 );
726 assert!(
727 steps
728 .iter()
729 .any(|s| matches!(s, MutationStep::AddEdge(1, 2)))
730 );
731 }
732
733 #[test]
734 fn commit_sets_cycles_and_marks_backward() {
735 let mut g = Graph::<i32>::default();
736 let mut tx = GraphTransaction::new(&mut g);
737
738 let a = tx.push((0, NodeType::Vertex, 10));
739 let b = tx.push((1, NodeType::Vertex, 20));
740 tx.attach(a, b);
741 tx.attach(b, a); match tx.commit() {
744 TransactionResult::Valid(steps) => {
745 assert!(g.is_valid());
746 assert_eq!(g[0].direction(), Direction::Backward);
748 assert_eq!(g[1].direction(), Direction::Backward);
749 assert_has_direction_change(&steps, &[0, 1]);
751 }
752 _ => panic!("expected Valid"),
753 }
754 }
755
756 #[test]
757 fn insertion_steps_new_zero_arity_connects_to_target_when_unlocked() {
758 let mut g = Graph::<i32>::default();
759 let mut tx = GraphTransaction::new(&mut g);
760
761 let src = tx.push((0, NodeType::Input, 0));
762 let tgt = tx.push((1, NodeType::Vertex, 1)); let newn = tx.push((2, NodeType::Input, 2)); let steps = random_provider::with_rng(|r| tx.get_insertion_steps(src, tgt, newn, r));
766 assert_eq!(steps, vec![InsertStep::Connect(newn, tgt)]);
767 }
768
769 #[test]
770 fn insertion_steps_source_is_edge_with_single_outgoing_equal_new() {
771 let mut g = Graph::<i32>::default();
772 let mut tx = GraphTransaction::new(&mut g);
773
774 let source = tx
776 .push(GraphNode::with_arity(0, NodeType::Edge, 0, Arity::Exact(1)).with_outgoing([2]));
777 let target = tx.push((1, NodeType::Vertex, 1));
778 let newn = tx.push((2, NodeType::Vertex, 2));
779
780 let steps = random_provider::with_rng(|r| tx.get_insertion_steps(source, target, newn, r));
781 assert_eq!(steps, vec![InsertStep::Connect(source, newn)]);
782 }
783
784 #[test]
785 fn insertion_steps_source_is_edge_redirects_through_new() {
786 let mut g = Graph::<i32>::default();
787 let mut tx = GraphTransaction::new(&mut g);
788
789 let source = tx
791 .push(GraphNode::with_arity(0, NodeType::Edge, 0, Arity::Exact(1)).with_outgoing([1]));
792 let target = tx.push((1, NodeType::Vertex, 1));
793 let newn = tx.push((2, NodeType::Vertex, 2));
794
795 let steps = random_provider::with_rng(|r| tx.get_insertion_steps(source, target, newn, r));
796 assert_eq!(
797 steps[..3],
798 vec![
799 InsertStep::Connect(source, newn),
800 InsertStep::Connect(newn, target),
801 InsertStep::Detach(source, target),
802 ]
803 );
804 }
805
806 #[test]
807 fn insertion_steps_target_locked_prefers_detach_rewire() {
808 let mut g = Graph::<i32>::default();
809 let mut tx = GraphTransaction::new(&mut g);
810
811 let source = tx.push((0, NodeType::Vertex, 0));
814 let target = tx.push(
815 GraphNode::with_arity(1, NodeType::Vertex, 1, Arity::Exact(1)).with_incoming([0]),
816 );
817 let newn = tx.push((2, NodeType::Vertex, 2));
818
819 let steps = random_provider::with_rng(|r| tx.get_insertion_steps(source, target, newn, r));
820 assert_eq!(
821 steps[..3],
822 vec![
823 InsertStep::Connect(source, newn),
824 InsertStep::Connect(newn, target),
825 InsertStep::Detach(source, target),
826 ]
827 );
828 }
829
830 #[test]
831 fn random_node_helpers_can_return_edges_when_only_edges_exist() {
832 random_provider::seed(1337);
833 random_provider::with_rng(|rand| {
834 let mut g = Graph::<i32>::default();
835 let mut tx = GraphTransaction::new(&mut g);
836
837 tx.push((0, NodeType::Edge, 0, Arity::Exact(1)));
839 tx.push((1, NodeType::Edge, 1, Arity::Exact(1)));
840
841 let src = tx.random_source_node(rand).unwrap();
842 let tgt = tx.random_target_node(rand).unwrap();
843
844 assert_eq!(src.node_type(), NodeType::Edge);
845 assert_eq!(tgt.node_type(), NodeType::Edge);
846 });
847 }
848
849 #[test]
850 fn rollback_restores_previous_innovation_and_replay_reapplies() {
851 let mut g = Graph::<i32>::default();
852 let initial = InnovationId::new();
853 let updated = InnovationId::new();
854
855 {
856 let mut tx = GraphTransaction::new(&mut g);
857 let input = tx.push((0, NodeType::Input, 0));
858 let output = tx.push((1, NodeType::Output, 1));
859 tx.attach(input, output);
860 tx.set_innovation(input, Some(initial));
861 assert!(matches!(tx.commit(), TransactionResult::Valid(_)));
862 }
863 assert_eq!(g[0].innovation(), Some(initial));
864
865 let replay = {
866 let mut tx = GraphTransaction::new(&mut g);
867 tx.set_innovation(0, Some(updated));
868 match tx.commit_with(|_| false) {
869 TransactionResult::Invalid(_, replay) => replay,
870 _ => panic!("expected forced rejection"),
871 }
872 };
873
874 assert_eq!(
875 g[0].innovation(),
876 Some(initial),
877 "rollback should restore previous innovation, not clear it"
878 );
879
880 {
881 let mut tx = GraphTransaction::new(&mut g);
882 tx.replay(replay);
883 assert!(matches!(tx.commit(), TransactionResult::Valid(_)));
884 }
885 assert_eq!(
886 g[0].innovation(),
887 Some(updated),
888 "replay should reapply the innovation change"
889 );
890 }
891}