1#![allow(
33 clippy::must_use_candidate,
34 clippy::module_name_repetitions,
35 clippy::doc_markdown
36)]
37
38use anyhow::Context as AnyhowContext;
39use rusqlite::Connection;
40use std::collections::{HashSet, VecDeque};
41use std::fmt;
42
43use crate::db::query::{self, QueryItem};
44
45#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct GoalProgress {
59 pub done: u32,
61 pub in_progress: u32,
63 pub total: u32,
65}
66
67impl GoalProgress {
68 pub const fn zero() -> Self {
70 Self {
71 done: 0,
72 in_progress: 0,
73 total: 0,
74 }
75 }
76
77 pub fn percent_complete(&self) -> f32 {
81 if self.total == 0 {
82 return 100.0;
83 }
84 #[allow(clippy::cast_precision_loss)]
85 {
86 (self.done as f32 / self.total as f32) * 100.0
87 }
88 }
89
90 pub const fn is_complete(&self) -> bool {
92 self.total == 0 || self.done == self.total
93 }
94
95 pub const fn remaining(&self) -> u32 {
97 self.total.saturating_sub(self.done)
98 }
99}
100
101impl fmt::Display for GoalProgress {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 write!(
104 f,
105 "{}/{} ({:.0}%)",
106 self.done,
107 self.total,
108 self.percent_complete()
109 )
110 }
111}
112
113#[derive(Debug)]
115pub enum HierarchyError {
116 NotAGoal {
119 item_id: String,
120 actual_kind: String,
121 },
122 ItemNotFound(String),
124 CycleDetected {
127 item_id: String,
128 proposed_parent: String,
129 },
130 Db(anyhow::Error),
132}
133
134impl fmt::Display for HierarchyError {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 match self {
137 Self::NotAGoal {
138 item_id,
139 actual_kind,
140 } => write!(
141 f,
142 "item '{item_id}' is not a goal (kind={actual_kind}): only goals may be parents"
143 ),
144 Self::ItemNotFound(id) => write!(f, "item not found: '{id}'"),
145 Self::CycleDetected {
146 item_id,
147 proposed_parent,
148 } => write!(
149 f,
150 "reparenting '{item_id}' under '{proposed_parent}' would create a cycle"
151 ),
152 Self::Db(e) => write!(f, "database error: {e}"),
153 }
154 }
155}
156
157impl std::error::Error for HierarchyError {
158 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
159 if let Self::Db(e) = self {
160 Some(e.as_ref())
161 } else {
162 None
163 }
164 }
165}
166
167impl From<anyhow::Error> for HierarchyError {
168 fn from(e: anyhow::Error) -> Self {
169 Self::Db(e)
170 }
171}
172
173pub fn compute_direct_progress(
189 conn: &Connection,
190 goal_id: &str,
191) -> Result<GoalProgress, HierarchyError> {
192 let goal = require_goal(conn, goal_id)?;
194 let _ = goal; let children = query::get_children(conn, goal_id)
197 .with_context(|| format!("get_children for '{goal_id}'"))?;
198
199 Ok(tally_progress(&children))
200}
201
202pub fn compute_nested_progress(
228 conn: &Connection,
229 goal_id: &str,
230) -> Result<GoalProgress, HierarchyError> {
231 require_goal(conn, goal_id)?;
233
234 let mut total_progress = GoalProgress::zero();
235 accumulate_progress(conn, goal_id, &mut total_progress, &mut HashSet::new())?;
236
237 Ok(total_progress)
238}
239
240pub fn get_subtree_ids(conn: &Connection, root_id: &str) -> Result<Vec<String>, HierarchyError> {
252 let mut visited: HashSet<String> = HashSet::new();
253 let mut queue: VecDeque<String> = VecDeque::new();
254 let mut result: Vec<String> = Vec::new();
255
256 queue.push_back(root_id.to_string());
257
258 while let Some(current_id) = queue.pop_front() {
259 if !visited.insert(current_id.clone()) {
260 continue; }
262 result.push(current_id.clone());
263
264 let children = query::get_children(conn, ¤t_id)
265 .with_context(|| format!("get_children for '{current_id}'"))?;
266
267 for child in children {
268 if !visited.contains(&child.item_id) {
269 queue.push_back(child.item_id);
270 }
271 }
272 }
273
274 Ok(result)
275}
276
277pub fn get_ancestors(conn: &Connection, item_id: &str) -> Result<Vec<QueryItem>, HierarchyError> {
290 let start = query::get_item(conn, item_id, false)
292 .with_context(|| format!("get_item '{item_id}'"))?
293 .ok_or_else(|| HierarchyError::ItemNotFound(item_id.to_string()))?;
294
295 let mut ancestors: Vec<QueryItem> = Vec::new();
296 let mut visited: HashSet<String> = HashSet::new();
297 visited.insert(start.item_id.clone());
298
299 let mut current_parent_id = start.parent_id;
300
301 while let Some(ref parent_id) = current_parent_id {
302 if parent_id.is_empty() {
303 break;
304 }
305 if !visited.insert(parent_id.clone()) {
306 break; }
308 let parent = query::get_item(conn, parent_id, false)
309 .with_context(|| format!("get_item '{parent_id}'"))?
310 .ok_or_else(|| HierarchyError::ItemNotFound(parent_id.clone()))?;
311
312 current_parent_id.clone_from(&parent.parent_id);
313 ancestors.push(parent);
314 }
315
316 Ok(ancestors)
317}
318
319pub fn validate_reparent(
336 conn: &Connection,
337 item_id: &str,
338 new_parent_id: &str,
339) -> Result<(), HierarchyError> {
340 if query::get_item(conn, item_id, false)
342 .with_context(|| format!("get_item '{item_id}'"))?
343 .is_none()
344 {
345 return Err(HierarchyError::ItemNotFound(item_id.to_string()));
346 }
347
348 require_goal(conn, new_parent_id)?;
350
351 let subtree = get_subtree_ids(conn, item_id)?;
353 if subtree.contains(&new_parent_id.to_string()) {
354 return Err(HierarchyError::CycleDetected {
355 item_id: item_id.to_string(),
356 proposed_parent: new_parent_id.to_string(),
357 });
358 }
359
360 Ok(())
361}
362
363fn require_goal(conn: &Connection, item_id: &str) -> Result<QueryItem, HierarchyError> {
372 let item = query::get_item(conn, item_id, false)
373 .with_context(|| format!("get_item '{item_id}'"))?
374 .ok_or_else(|| HierarchyError::ItemNotFound(item_id.to_string()))?;
375
376 if item.kind != "goal" {
377 return Err(HierarchyError::NotAGoal {
378 item_id: item_id.to_string(),
379 actual_kind: item.kind,
380 });
381 }
382
383 Ok(item)
384}
385
386fn tally_progress(items: &[QueryItem]) -> GoalProgress {
388 let mut progress = GoalProgress::zero();
389 for item in items {
390 progress.total += 1;
391 match item.state.as_str() {
392 "done" | "archived" => progress.done += 1,
393 "doing" => progress.in_progress += 1,
394 _ => {} }
396 }
397 progress
398}
399
400fn accumulate_progress(
407 conn: &Connection,
408 current_id: &str,
409 accumulator: &mut GoalProgress,
410 visited: &mut HashSet<String>,
411) -> Result<(), HierarchyError> {
412 if !visited.insert(current_id.to_string()) {
413 return Ok(()); }
415
416 let children = query::get_children(conn, current_id)
417 .with_context(|| format!("get_children for '{current_id}'"))?;
418
419 if children.is_empty() {
420 return Ok(());
422 }
423
424 for child in &children {
425 if child.kind == "goal" {
426 accumulate_progress(conn, &child.item_id, accumulator, visited)?;
428 } else {
429 accumulator.total += 1;
431 match child.state.as_str() {
432 "done" | "archived" => accumulator.done += 1,
433 "doing" => accumulator.in_progress += 1,
434 _ => {}
435 }
436 }
437 }
438
439 Ok(())
440}
441
442#[cfg(test)]
447mod tests {
448 use super::*;
449 use crate::db::migrations;
450 use rusqlite::{Connection, params};
451
452 fn test_db() -> Connection {
457 let mut conn = Connection::open_in_memory().expect("open in-memory db");
458 migrations::migrate(&mut conn).expect("migrate");
459 conn
460 }
461
462 fn insert_item(conn: &Connection, id: &str, kind: &str, state: &str, parent_id: Option<&str>) {
464 conn.execute(
465 "INSERT INTO items \
466 (item_id, title, kind, state, urgency, is_deleted, search_labels, \
467 parent_id, created_at_us, updated_at_us) \
468 VALUES (?1, ?2, ?3, ?4, 'default', 0, '', ?5, 1000, 2000)",
469 params![id, format!("Title for {id}"), kind, state, parent_id],
470 )
471 .expect("insert item");
472 }
473
474 #[test]
479 fn goal_progress_zero() {
480 let p = GoalProgress::zero();
481 assert_eq!(p.done, 0);
482 assert_eq!(p.in_progress, 0);
483 assert_eq!(p.total, 0);
484 }
485
486 #[test]
487 fn goal_progress_percent_zero_total_is_100() {
488 let p = GoalProgress::zero();
489 assert_eq!(p.percent_complete(), 100.0);
490 assert!(p.is_complete());
491 }
492
493 #[test]
494 fn goal_progress_percent_half() {
495 let p = GoalProgress {
496 done: 1,
497 in_progress: 0,
498 total: 2,
499 };
500 assert!((p.percent_complete() - 50.0).abs() < f32::EPSILON);
501 assert!(!p.is_complete());
502 }
503
504 #[test]
505 fn goal_progress_all_done() {
506 let p = GoalProgress {
507 done: 5,
508 in_progress: 0,
509 total: 5,
510 };
511 assert_eq!(p.percent_complete(), 100.0);
512 assert!(p.is_complete());
513 }
514
515 #[test]
516 fn goal_progress_remaining() {
517 let p = GoalProgress {
518 done: 3,
519 in_progress: 1,
520 total: 5,
521 };
522 assert_eq!(p.remaining(), 2);
523 }
524
525 #[test]
526 fn goal_progress_display() {
527 let p = GoalProgress {
528 done: 2,
529 in_progress: 1,
530 total: 4,
531 };
532 let s = p.to_string();
533 assert!(s.contains("2/4"), "display: {s}");
534 assert!(s.contains("50%"), "display: {s}");
535 }
536
537 #[test]
542 fn hierarchy_error_display_not_a_goal() {
543 let e = HierarchyError::NotAGoal {
544 item_id: "bn-001".to_string(),
545 actual_kind: "task".to_string(),
546 };
547 assert!(e.to_string().contains("bn-001"));
548 assert!(e.to_string().contains("task"));
549 }
550
551 #[test]
552 fn hierarchy_error_display_not_found() {
553 let e = HierarchyError::ItemNotFound("bn-missing".to_string());
554 assert!(e.to_string().contains("bn-missing"));
555 }
556
557 #[test]
558 fn hierarchy_error_display_cycle() {
559 let e = HierarchyError::CycleDetected {
560 item_id: "bn-001".to_string(),
561 proposed_parent: "bn-002".to_string(),
562 };
563 let s = e.to_string();
564 assert!(s.contains("bn-001"));
565 assert!(s.contains("bn-002"));
566 assert!(s.contains("cycle"));
567 }
568
569 #[test]
574 fn direct_progress_empty_goal() {
575 let conn = test_db();
576 insert_item(&conn, "bn-goal", "goal", "open", None);
577
578 let p = compute_direct_progress(&conn, "bn-goal").unwrap();
579 assert_eq!(p.total, 0);
580 assert_eq!(p.done, 0);
581 assert!(p.is_complete()); }
583
584 #[test]
585 fn direct_progress_all_open() {
586 let conn = test_db();
587 insert_item(&conn, "bn-goal", "goal", "open", None);
588 insert_item(&conn, "bn-c1", "task", "open", Some("bn-goal"));
589 insert_item(&conn, "bn-c2", "task", "open", Some("bn-goal"));
590
591 let p = compute_direct_progress(&conn, "bn-goal").unwrap();
592 assert_eq!(p.total, 2);
593 assert_eq!(p.done, 0);
594 assert_eq!(p.in_progress, 0);
595 assert!(!p.is_complete());
596 }
597
598 #[test]
599 fn direct_progress_mixed_states() {
600 let conn = test_db();
601 insert_item(&conn, "bn-goal", "goal", "open", None);
602 insert_item(&conn, "bn-c1", "task", "done", Some("bn-goal"));
603 insert_item(&conn, "bn-c2", "task", "doing", Some("bn-goal"));
604 insert_item(&conn, "bn-c3", "task", "open", Some("bn-goal"));
605 insert_item(&conn, "bn-c4", "task", "archived", Some("bn-goal"));
606
607 let p = compute_direct_progress(&conn, "bn-goal").unwrap();
608 assert_eq!(p.total, 4);
609 assert_eq!(p.done, 2); assert_eq!(p.in_progress, 1);
611 assert!(!p.is_complete());
612 }
613
614 #[test]
615 fn direct_progress_all_done() {
616 let conn = test_db();
617 insert_item(&conn, "bn-goal", "goal", "open", None);
618 insert_item(&conn, "bn-c1", "task", "done", Some("bn-goal"));
619 insert_item(&conn, "bn-c2", "task", "done", Some("bn-goal"));
620
621 let p = compute_direct_progress(&conn, "bn-goal").unwrap();
622 assert_eq!(p.total, 2);
623 assert_eq!(p.done, 2);
624 assert!(p.is_complete());
625 }
626
627 #[test]
628 fn direct_progress_not_found_returns_error() {
629 let conn = test_db();
630 let err = compute_direct_progress(&conn, "bn-missing").unwrap_err();
631 assert!(matches!(err, HierarchyError::ItemNotFound(_)));
632 }
633
634 #[test]
635 fn direct_progress_not_a_goal_returns_error() {
636 let conn = test_db();
637 insert_item(&conn, "bn-task", "task", "open", None);
638
639 let err = compute_direct_progress(&conn, "bn-task").unwrap_err();
640 assert!(matches!(
641 err,
642 HierarchyError::NotAGoal { actual_kind, .. } if actual_kind == "task"
643 ));
644 }
645
646 #[test]
647 fn direct_progress_excludes_deleted_children() {
648 let conn = test_db();
649 insert_item(&conn, "bn-goal", "goal", "open", None);
650 insert_item(&conn, "bn-c1", "task", "done", Some("bn-goal"));
651 conn.execute(
653 "INSERT INTO items \
654 (item_id, title, kind, state, urgency, is_deleted, search_labels, \
655 parent_id, created_at_us, updated_at_us) \
656 VALUES ('bn-del', 'Deleted', 'task', 'open', 'default', 1, '', 'bn-goal', 1000, 2000)",
657 [],
658 )
659 .unwrap();
660
661 let p = compute_direct_progress(&conn, "bn-goal").unwrap();
662 assert_eq!(p.total, 1); assert_eq!(p.done, 1);
664 }
665
666 #[test]
671 fn nested_progress_flat_goal() {
672 let conn = test_db();
673 insert_item(&conn, "bn-goal", "goal", "open", None);
674 insert_item(&conn, "bn-c1", "task", "done", Some("bn-goal"));
675 insert_item(&conn, "bn-c2", "task", "open", Some("bn-goal"));
676
677 let p = compute_nested_progress(&conn, "bn-goal").unwrap();
678 assert_eq!(p.total, 2);
679 assert_eq!(p.done, 1);
680 }
681
682 #[test]
683 fn nested_progress_rolls_up_through_subgoals() {
684 let conn = test_db();
691 insert_item(&conn, "bn-ga", "goal", "open", None);
692 insert_item(&conn, "bn-tx", "task", "done", Some("bn-ga"));
693 insert_item(&conn, "bn-gb", "goal", "open", Some("bn-ga"));
694 insert_item(&conn, "bn-ty", "task", "done", Some("bn-gb"));
695 insert_item(&conn, "bn-tz", "task", "open", Some("bn-gb"));
696 insert_item(&conn, "bn-tw", "task", "doing", Some("bn-ga"));
697
698 let p = compute_nested_progress(&conn, "bn-ga").unwrap();
699 assert_eq!(p.total, 4, "leaf items: tx, ty, tz, tw");
700 assert_eq!(p.done, 2, "tx and ty are done");
701 assert_eq!(p.in_progress, 1, "tw is doing");
702 }
703
704 #[test]
705 fn nested_progress_deeply_nested() {
706 let conn = test_db();
708 insert_item(&conn, "bn-g1", "goal", "open", None);
709 insert_item(&conn, "bn-g2", "goal", "open", Some("bn-g1"));
710 insert_item(&conn, "bn-g3", "goal", "open", Some("bn-g2"));
711 insert_item(&conn, "bn-t1", "task", "done", Some("bn-g3"));
712
713 let p = compute_nested_progress(&conn, "bn-g1").unwrap();
714 assert_eq!(p.total, 1);
715 assert_eq!(p.done, 1);
716 assert!(p.is_complete());
717 }
718
719 #[test]
720 fn nested_progress_empty_subgoal_contributes_nothing() {
721 let conn = test_db();
723 insert_item(&conn, "bn-ga", "goal", "open", None);
724 insert_item(&conn, "bn-t1", "task", "done", Some("bn-ga"));
725 insert_item(&conn, "bn-gb", "goal", "open", Some("bn-ga")); let p = compute_nested_progress(&conn, "bn-ga").unwrap();
728 assert_eq!(p.total, 1, "only the task counts");
729 assert_eq!(p.done, 1);
730 }
731
732 #[test]
733 fn nested_progress_not_found() {
734 let conn = test_db();
735 let err = compute_nested_progress(&conn, "bn-missing").unwrap_err();
736 assert!(matches!(err, HierarchyError::ItemNotFound(_)));
737 }
738
739 #[test]
740 fn nested_progress_not_a_goal() {
741 let conn = test_db();
742 insert_item(&conn, "bn-task", "task", "open", None);
743 let err = compute_nested_progress(&conn, "bn-task").unwrap_err();
744 assert!(matches!(err, HierarchyError::NotAGoal { .. }));
745 }
746
747 #[test]
752 fn subtree_single_node() {
753 let conn = test_db();
754 insert_item(&conn, "bn-root", "goal", "open", None);
755
756 let ids = get_subtree_ids(&conn, "bn-root").unwrap();
757 assert_eq!(ids, vec!["bn-root"]);
758 }
759
760 #[test]
761 fn subtree_with_children() {
762 let conn = test_db();
763 insert_item(&conn, "bn-root", "goal", "open", None);
764 insert_item(&conn, "bn-c1", "task", "open", Some("bn-root"));
765 insert_item(&conn, "bn-c2", "task", "done", Some("bn-root"));
766
767 let ids = get_subtree_ids(&conn, "bn-root").unwrap();
768 assert_eq!(ids.len(), 3);
769 assert!(ids.contains(&"bn-root".to_string()));
770 assert!(ids.contains(&"bn-c1".to_string()));
771 assert!(ids.contains(&"bn-c2".to_string()));
772 }
773
774 #[test]
775 fn subtree_bfs_order_root_first() {
776 let conn = test_db();
778 insert_item(&conn, "bn-root", "goal", "open", None);
779 insert_item(&conn, "bn-c1", "goal", "open", Some("bn-root"));
780 insert_item(&conn, "bn-c2", "task", "open", Some("bn-root"));
781 insert_item(&conn, "bn-gc1", "task", "done", Some("bn-c1"));
782
783 let ids = get_subtree_ids(&conn, "bn-root").unwrap();
784 assert_eq!(ids[0], "bn-root"); assert_eq!(ids.len(), 4);
786 }
787
788 #[test]
793 fn ancestors_no_parent() {
794 let conn = test_db();
795 insert_item(&conn, "bn-root", "goal", "open", None);
796
797 let ancestors = get_ancestors(&conn, "bn-root").unwrap();
798 assert!(ancestors.is_empty());
799 }
800
801 #[test]
802 fn ancestors_one_level() {
803 let conn = test_db();
804 insert_item(&conn, "bn-parent", "goal", "open", None);
805 insert_item(&conn, "bn-child", "task", "open", Some("bn-parent"));
806
807 let ancestors = get_ancestors(&conn, "bn-child").unwrap();
808 assert_eq!(ancestors.len(), 1);
809 assert_eq!(ancestors[0].item_id, "bn-parent");
810 }
811
812 #[test]
813 fn ancestors_three_levels() {
814 let conn = test_db();
815 insert_item(&conn, "bn-g1", "goal", "open", None);
816 insert_item(&conn, "bn-g2", "goal", "open", Some("bn-g1"));
817 insert_item(&conn, "bn-t1", "task", "open", Some("bn-g2"));
818
819 let ancestors = get_ancestors(&conn, "bn-t1").unwrap();
820 assert_eq!(ancestors.len(), 2);
821 assert_eq!(ancestors[0].item_id, "bn-g2");
822 assert_eq!(ancestors[1].item_id, "bn-g1");
823 }
824
825 #[test]
826 fn ancestors_not_found() {
827 let conn = test_db();
828 let err = get_ancestors(&conn, "bn-missing").unwrap_err();
829 assert!(matches!(err, HierarchyError::ItemNotFound(_)));
830 }
831
832 #[test]
837 fn validate_reparent_ok() {
838 let conn = test_db();
839 insert_item(&conn, "bn-goal", "goal", "open", None);
840 insert_item(&conn, "bn-task", "task", "open", None);
841
842 assert!(validate_reparent(&conn, "bn-task", "bn-goal").is_ok());
843 }
844
845 #[test]
846 fn validate_reparent_to_non_goal_rejected() {
847 let conn = test_db();
848 insert_item(&conn, "bn-task1", "task", "open", None);
849 insert_item(&conn, "bn-task2", "task", "open", None);
850
851 let err = validate_reparent(&conn, "bn-task1", "bn-task2").unwrap_err();
852 assert!(matches!(err, HierarchyError::NotAGoal { .. }));
853 }
854
855 #[test]
856 fn validate_reparent_item_not_found() {
857 let conn = test_db();
858 insert_item(&conn, "bn-goal", "goal", "open", None);
859
860 let err = validate_reparent(&conn, "bn-missing", "bn-goal").unwrap_err();
861 assert!(matches!(err, HierarchyError::ItemNotFound(_)));
862 }
863
864 #[test]
865 fn validate_reparent_parent_not_found() {
866 let conn = test_db();
867 insert_item(&conn, "bn-task", "task", "open", None);
868
869 let err = validate_reparent(&conn, "bn-task", "bn-missing").unwrap_err();
870 assert!(matches!(err, HierarchyError::ItemNotFound(_)));
871 }
872
873 #[test]
874 fn validate_reparent_detects_cycle_direct() {
875 let conn = test_db();
877 insert_item(&conn, "bn-parent", "goal", "open", None);
878 insert_item(&conn, "bn-child", "goal", "open", Some("bn-parent"));
879
880 let err = validate_reparent(&conn, "bn-parent", "bn-child").unwrap_err();
882 assert!(matches!(err, HierarchyError::CycleDetected { .. }));
883 }
884
885 #[test]
886 fn validate_reparent_detects_cycle_indirect() {
887 let conn = test_db();
889 insert_item(&conn, "bn-g1", "goal", "open", None);
890 insert_item(&conn, "bn-g2", "goal", "open", Some("bn-g1"));
891 insert_item(&conn, "bn-g3", "goal", "open", Some("bn-g2"));
892
893 let err = validate_reparent(&conn, "bn-g1", "bn-g3").unwrap_err();
894 assert!(matches!(err, HierarchyError::CycleDetected { .. }));
895 }
896
897 #[test]
898 fn validate_reparent_self_cycle() {
899 let conn = test_db();
901 insert_item(&conn, "bn-goal", "goal", "open", None);
902
903 let err = validate_reparent(&conn, "bn-goal", "bn-goal").unwrap_err();
904 assert!(matches!(err, HierarchyError::CycleDetected { .. }));
905 }
906
907 #[test]
908 fn validate_reparent_move_to_different_goal() {
909 let conn = test_db();
911 insert_item(&conn, "bn-ga", "goal", "open", None);
912 insert_item(&conn, "bn-gb", "goal", "open", None);
913 insert_item(&conn, "bn-task", "task", "open", Some("bn-ga"));
914
915 assert!(validate_reparent(&conn, "bn-task", "bn-gb").is_ok());
916 }
917
918 #[test]
919 fn validate_reparent_bug_under_goal_ok() {
920 let conn = test_db();
922 insert_item(&conn, "bn-goal", "goal", "open", None);
923 insert_item(&conn, "bn-bug", "bug", "open", None);
924
925 assert!(validate_reparent(&conn, "bn-bug", "bn-goal").is_ok());
926 }
927}