fix(tree): stop silent page loss on move-to-unloaded-parent + reconnect ghost roots (#159)
Two confirmed P1 data-loss findings in the sidebar tree sync. #1 — Move into an unloaded/collapsed parent silently dropped pages. When a moveTreeNode (or addTreeNode) broadcast targeted a parent whose children were NOT yet lazy-loaded, `insertByPosition` did `kids = parent.children ?? []` and inserted the moved node, MATERIALIZING a misleading partial child list (`[movedNode]`) out of an unloaded (`children === undefined`) parent. The lazy-load gate fetches only when children are absent/empty, so it then refused to fetch — leaving the parent showing ONLY the moved node and HIDING all its other real children (and, when the parent wasn't in the tree at all, the node was removed and never re-fetched). Fix: `insertByPosition` distinguishes `children === undefined` (not loaded) from `[]` (loaded-empty) and, for an unloaded parent, does NOT insert — it leaves children unloaded and just flags `hasChildren`, so expanding fetches the FULL set (including the moved/added node) via the existing lazy-load. #2 — After a socket reconnect, a deleted/moved-away root lingered as a 404 "ghost". `mergeRootTrees` was append-only: it kept every previously-loaded root and only added new ones, so a root removed during the missed-events gap was never dropped. It runs only once all root pages are fetched, so the incoming list is the authoritative complete root set — fix reconciles to it (drop roots absent from incoming) while PRESERVING each surviving root's lazy-loaded subtree and refreshing its own fields. Tests: insertByPosition unloaded-vs-loaded-empty parent; the move reducer keeps a collapsed destination lazy-loadable instead of partial; mergeRootTrees drops a ghost root, preserves a surviving subtree, adds new roots, refreshes fields. The existing "remove when parent not in tree" reducer test still holds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in: