From ef54f0d646a0827e8abf7287e6168ba07b20350c Mon Sep 17 00:00:00 2001 From: Cameron Cordes Date: Sat, 8 Aug 2026 10:58:13 -0400 Subject: [PATCH] test: add nested fork regression tests - branch_options_handles_nested_forks: verifies branch picker returns correct branches for both root fork and sub-fork - render_tree_path_nested_forks_only_emits_at_divergence: confirms fork_info doesn't propagate downstream (only at actual divergence point) --- src/ai/insight_chat.rs | 31 +++++++++++++++++++++++++++++ src/ai/llm_client.rs | 45 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/src/ai/insight_chat.rs b/src/ai/insight_chat.rs index 88badfd..80b342a 100644 --- a/src/ai/insight_chat.rs +++ b/src/ai/insight_chat.rs @@ -2894,6 +2894,37 @@ mod tests { assert_eq!(f.node_id, u, "divergence node is the forked user turn"); } + #[test] + fn render_tree_path_nested_forks_only_emits_at_divergence() { + // Regression: with .take() instead of .clone(), fork_info should only + // appear at the actual divergence point, not propagate downstream. + // Structure: + // u (user "q1") + // ├── a1 (assistant "answer 1") + // └── a2 (assistant "answer 2") + // ├── q2 (user "q2") + // └── r2 (assistant "reply 2") <- active leaf + // + // Expected fork_info: [None, Some, None, None] + // (only a2 has fork info since it's the divergence from a1) + let mut store = ChatHistoryStore::empty(); + let u = store.append_node(None, ChatMessage::user("q1")); + let _a1 = store.append_node(Some(u), assistant_text("answer 1")); + let a2 = store.append_node(Some(u), assistant_text("answer 2")); + let q2 = store.append_node(Some(a2), ChatMessage::user("q2")); + let r2 = store.append_node(Some(q2), assistant_text("reply 2")); + store.active_leaf_id = r2; + + let path = store.path_to_leaf(r2).expect("path to active leaf"); + let (rendered, _turns, _node_ids, fork_info) = render_tree_path(&store, &path); + + assert_eq!(rendered.len(), 4, "user + asst + user + asst"); + assert!(fork_info[0].is_none(), "initial user has no fork"); + assert!(fork_info[1].is_some(), "a2 is the fork point"); + assert!(fork_info[2].is_none(), "q2 should NOT carry fork forward"); + assert!(fork_info[3].is_none(), "r2 should NOT carry fork forward"); + } + #[test] fn rewind_strips_assistant_and_tool_scaffolding() { // Rendered: [user1, asst1, user2, asst2] → cut at rendered index 3 diff --git a/src/ai/llm_client.rs b/src/ai/llm_client.rs index 99f6b7b..2c1e9e9 100644 --- a/src/ai/llm_client.rs +++ b/src/ai/llm_client.rs @@ -743,4 +743,49 @@ mod tests { assert_eq!(store.nodes[1].parent_id, Some(parent)); assert_eq!(store.nodes[1].id, child); } + + #[test] + fn branch_options_handles_nested_forks() { + // Regression: verify sub-fork display when there are multiple forks. + // Structure: + // root (user "q1") + // ├── a1 (assistant "answer 1") + // └── a2 (assistant "answer 2") + // ├── q2 (user "q2") + // │ ├── r1 (assistant "reply 1") + // │ └── r2 (assistant "reply 2") <- active leaf + // + // branch_options_at(root) should return [a1, a2] (2 branches) + // branch_options_at(q2) should return [r1, r2] (2 sub-branches) + let mut store = ChatHistoryStore::empty(); + let root = store.append_node(None, mk_msg("user", "q1")); + let a1 = store.append_node(Some(root), mk_msg("assistant", "answer 1")); + let a2 = store.append_node(Some(root), mk_msg("assistant", "answer 2")); + let q2 = store.append_node(Some(a2), mk_msg("user", "q2")); + let r1 = store.append_node(Some(q2), mk_msg("assistant", "reply 1")); + let r2 = store.append_node(Some(q2), mk_msg("assistant", "reply 2")); + store.active_leaf_id = r2; + + // Root fork: should show [a1, a2's subtree] + let opts = store.branch_options_at(root, r2); + assert_eq!(opts.len(), 2); + assert_eq!(opts[0].id, a1); + assert_eq!(opts[0].position, Some(1)); + assert_eq!(opts[0].snippet, "answer 1"); + // a2's subtree includes r2, so the rep should be r2 (preferred leaf) + assert_eq!(opts[1].id, r2); + assert_eq!(opts[1].position, Some(2)); + assert_eq!(opts[1].snippet, "answer 2"); + + // Sub-fork at q2: should show [r1, r2] + let opts = store.branch_options_at(q2, r2); + assert_eq!(opts.len(), 2); + assert_eq!(opts[0].id, r1); + assert_eq!(opts[0].position, Some(1)); + assert_eq!(opts[0].snippet, "reply 1"); + assert_eq!(opts[1].id, r2); + assert_eq!(opts[1].position, Some(2)); + assert_eq!(opts[1].snippet, "reply 2"); + } } +