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]
|
:<- [::scene] :<- [::context] :<- [::segments] :<- [::revealed]
|
||||||
(fn [[scene ctx segs revealed] _]
|
(fn [[scene ctx segs revealed] _]
|
||||||
(let [ann? (fn [gid] (= :annotation (:type (get-in scene [:groups gid]))))
|
(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
|
;; 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
|
;; 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))]
|
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)
|
;; 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))
|
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,
|
;; membership-reveal hierarchy: shows when ctx is a host it's filed under,
|
||||||
|
|
|
||||||
|
|
@ -190,10 +190,48 @@
|
||||||
(rf/dispatch [::ev/toggle-children :annA])
|
(rf/dispatch [::ev/toggle-children :annA])
|
||||||
(is (contains? (pane-ids) :annC) "revealed → annC shows as annA's child at root"))))
|
(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
|
;; 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
|
(deftest membership-survives-the-json-wire
|
||||||
(rf-test/run-test-sync
|
(rf-test/run-test-sync
|
||||||
(setup! (seed) [:root])
|
(setup! (seed) [:root])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue