fix: rescue orphaned annotations to root in the pane
Found by loading the real Challengers project through re-frame-test: 3 of 15 stored annotations were filed under a since-deleted parent. Under the :in model their sole host was a ghost, so ::all-annotations never surfaced them — invisible and unrefileable. ::all-annotations now drops :in hosts that no longer exist and rescues an annotation left with none to root (full-scene context, so peer-delta safe). Tests (re-frame-test, high-level events + pane sub): - orphaned-annotation-is-rescued-to-root - legacy-parent-data-migrates-through-peer-delta (the real DB shape) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
65a80857be
commit
d54a6ae8f8
2 changed files with 45 additions and 2 deletions
|
|
@ -135,11 +135,16 @@
|
|||
:<- [::scene] :<- [::context] :<- [::segments] :<- [::revealed]
|
||||
(fn [[scene ctx segs revealed] _]
|
||||
(let [ann? (fn [gid] (= :annotation (:type (get-in scene [:groups gid]))))
|
||||
exists? (fn [x] (contains? (:groups scene) x))
|
||||
;; each annotation → the mark-groups it's FILED UNDER (:in membership). A
|
||||
;; normal annotation has one; a linked one has several, so it lists under
|
||||
;; each. Asserted, not derived from where marks resolve.
|
||||
;; each. Asserted, not derived from where marks resolve. Any :in host that
|
||||
;; no longer exists (its group was deleted) is dropped; an annotation left
|
||||
;; with NO surviving host is rescued to root so it stays visible/refileable
|
||||
;; instead of vanishing into a ghost parent.
|
||||
parents (into {} (for [[gid g] (:groups scene) :when (= :annotation (:type g))]
|
||||
[gid (scene/membership scene gid)]))
|
||||
(let [ms (filter exists? (scene/membership scene gid))]
|
||||
[gid (if (seq ms) (set ms) #{:root})])))
|
||||
;; child count per timeline (drives the "Show N" nested badge)
|
||||
nested (reduce (fn [acc ps] (reduce #(update %1 %2 (fnil inc 0)) acc ps)) {} (vals parents))
|
||||
;; membership-reveal hierarchy: shows when ctx is a host it's filed under,
|
||||
|
|
|
|||
|
|
@ -190,10 +190,48 @@
|
|||
(rf/dispatch [::ev/toggle-children :annA])
|
||||
(is (contains? (pane-ids) :annC) "revealed → annC shows as annA's child at root"))))
|
||||
|
||||
;; =========================================================================
|
||||
;; real-data hazard: an annotation whose only host was deleted (an orphan)
|
||||
;; must NOT vanish — the pane rescues it to root so it stays refileable.
|
||||
;; (found by loading the actual Challengers project: 3/15 annotations were
|
||||
;; filed under a since-deleted parent.)
|
||||
;; =========================================================================
|
||||
|
||||
(deftest orphaned-annotation-is-rescued-to-root
|
||||
(rf-test/run-test-sync
|
||||
;; annC's sole host (annA) is deleted out from under it, as if by restore of
|
||||
;; stale data or a peer deleting the parent
|
||||
(setup! (update (seed) :groups dissoc :annA) [:root])
|
||||
(testing "annC's :in still points at the now-missing annA"
|
||||
(is (= [:annA] (:in (get-in (scene*) [:groups :annC]))))
|
||||
(is (nil? (get-in (scene*) [:groups :annA]))))
|
||||
(testing "but the pane rescues it to root rather than dropping it"
|
||||
(is (contains? (pane-ids) :annC) "orphan is visible at root, not lost")
|
||||
(is (= #{:root} (:parents (card :annC))) "listed under root until re-filed"))
|
||||
(testing "and it can be re-filed normally from there"
|
||||
(rf/dispatch [::ev/file-into :annC :annB])
|
||||
(is (contains? (s/membership (scene*) :annC) :annB)))))
|
||||
|
||||
;; =========================================================================
|
||||
;; JSON wire round-trip of :in (vector, keyworded) via restore-annotations
|
||||
;; =========================================================================
|
||||
|
||||
(deftest legacy-parent-data-migrates-through-peer-delta
|
||||
(rf-test/run-test-sync
|
||||
(setup! clips-scene [:root])
|
||||
;; a reload/peer delivers a legacy annotation — string :parent, no :in, exactly
|
||||
;; the shape stored in the real DB before the membership migration
|
||||
(rf/dispatch [::ev/peer-delta
|
||||
{:changed {:ann-legacy {:type "annotation" :parent "root" :name "L"
|
||||
:marks [{:id "lm" :start {:ref "clip-a" :at 0}
|
||||
:end {:ref "clip-a" :at 50}}]}}
|
||||
:deleted []}])
|
||||
(testing "it migrates :parent -> :in [:root] on ingest and drops the old field"
|
||||
(is (= [:root] (:in (get-in (scene*) [:groups :ann-legacy]))))
|
||||
(is (nil? (:parent (get-in (scene*) [:groups :ann-legacy])))))
|
||||
(testing "and shows up in the root pane like any other"
|
||||
(is (contains? (pane-ids) :ann-legacy)))))
|
||||
|
||||
(deftest membership-survives-the-json-wire
|
||||
(rf-test/run-test-sync
|
||||
(setup! (seed) [:root])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue