Просмотр исходного кода

test(sync): cover stale fix/reject flows

Tienson Qin 3 дней назад
Родитель
Сommit
8fcd8fb518

+ 4 - 4
deps/db-sync/src/logseq/db_sync/worker/handler/sync.cljs

@@ -311,10 +311,10 @@
                                       outliner-op (assoc :outliner-op outliner-op)))
         true
         (catch :default e
-          ;; Rebase txs are inferred from local history and can become stale when
-          ;; concurrent remote edits remove referenced entities before upload.
-          ;; Treat stale :entity-id/missing rebases as no-op so sync can continue.
-          (if (and (= outliner-op :rebase)
+          ;; Rebase/fix txs are inferred from local history and can become stale
+          ;; when concurrent remote edits remove referenced entities before upload.
+          ;; Treat stale :entity-id/missing rebases/fixes as no-op so sync can continue.
+          (if (and (contains? #{:rebase :fix} outliner-op)
                    (= :entity-id/missing (:error (ex-data e))))
             (do
               (log/warn :db-sync/drop-stale-rebase-tx

+ 48 - 0
deps/db-sync/test/logseq/db_sync/worker_handler_sync_test.cljs

@@ -760,6 +760,54 @@
       (is (empty? (storage/fetch-tx-since sql t-before)))
       (is (empty? @changed-messages)))))
 
+(deftest tx-batch-ignores-stale-fix-with-missing-lookup-entity-test
+  (testing "stale fix lookup refs to missing entities are treated as no-op"
+    (let [sql (test-sql/make-sql)
+          conn (storage/open-conn sql)
+          self #js {:sql sql
+                    :conn conn
+                    :schema-ready true}
+          page-uuid (random-uuid)
+          sibling-uuid (random-uuid)
+          missing-block-uuid (random-uuid)
+          _ (d/transact! conn [{:block/uuid page-uuid
+                                :block/name "fix-stale-page"
+                                :block/title "fix-stale-page"}
+                               {:block/uuid sibling-uuid
+                                :block/title "existing-sibling"
+                                :block/order "a5Uzl"
+                                :block/parent [:block/uuid page-uuid]
+                                :block/page [:block/uuid page-uuid]}])
+          t-before (storage/get-t sql)
+          checksum-before (storage/get-checksum sql)
+          tx-entry {:tx (protocol/tx->transit
+                         [[:db/retract [:block/uuid missing-block-uuid]
+                           :block/order
+                           "a5Uzl"
+                           536871101]
+                          [:db/add [:block/uuid missing-block-uuid]
+                           :block/order
+                           "a5c"
+                           536871101]
+                          [:db/retract [:block/uuid sibling-uuid]
+                           :block/order
+                           "a5Uzl"
+                           536871101]
+                          [:db/add [:block/uuid sibling-uuid]
+                           :block/order
+                           "a5k"
+                           536871101]])
+                    :outliner-op :fix}
+          changed-messages (atom [])
+          response (with-redefs [ws/broadcast! (fn [_self _sender payload]
+                                                 (swap! changed-messages conj payload))]
+                     (sync-handler/handle-tx-batch! self nil [tx-entry] t-before))]
+      (is (= "tx/batch/ok" (:type response)))
+      (is (= t-before (:t response)))
+      (is (= checksum-before (storage/get-checksum sql)))
+      (is (empty? (storage/fetch-tx-since sql t-before)))
+      (is (empty? @changed-messages)))))
+
 (deftest server-incremental-checksum-matches-full-recompute-fuzz-test
   (testing "server stored checksum stays equal to full recompute across randomized tx/rebase/no-op sequences"
     (doseq [seed (range 1 11)]

+ 226 - 0
src/test/frontend/worker/db_sync_sim_test.cljs

@@ -1102,6 +1102,73 @@
                (:block/uuid (d/entity db id))))
        set))
 
+(defn- block-tree-preorder
+  [root]
+  (letfn [(walk [node]
+            (cons node
+                  (mapcat walk (ldb/sort-by-order (:block/_parent node)))))]
+    (walk root)))
+
+(defn- random-copied-block-tree
+  [rng source]
+  (let [nodes (vec (block-tree-preorder source))
+        max-size (max 1 (min 12 (count nodes)))
+        size (+ 1 (rand-int! rng max-size))
+        nodes' (subvec nodes 0 size)
+        uuid-map (into {}
+                       (map (fn [node]
+                              [(:block/uuid node) (rng-uuid rng)])
+                            nodes'))
+        copied (mapv (fn [node]
+                       (let [old-uuid (:block/uuid node)
+                             new-uuid (get uuid-map old-uuid)
+                             parent-uuid (some-> node :block/parent :block/uuid)]
+                         (cond-> {:block/uuid new-uuid
+                                  :block/title (or (:block/title node) "")}
+                           (contains? uuid-map parent-uuid)
+                           (assoc :block/parent [:block/uuid (get uuid-map parent-uuid)]))))
+                     nodes')]
+    copied))
+
+(defn- op-copy-paste-block-tree-into-empty-target!
+  [rng conn state _base-uuid]
+  (let [db @conn
+        sources (->> (existing-blocks db (:blocks @state))
+                     (filter (fn [block]
+                               (seq (ldb/sort-by-order (:block/_parent block))))))
+        source (rand-nth! rng (vec sources))]
+    (when source
+      (let [source-uuid (:block/uuid source)
+            source-page-uuid (:block/uuid (:block/page source))
+            source-descendants (block-and-descendant-uuids db source)
+            targets (->> (existing-blocks db (:blocks @state))
+                         (remove (fn [target]
+                                   (contains? source-descendants (:block/uuid target))))
+                         (filter (fn [target]
+                                   (and (= source-page-uuid
+                                           (:block/uuid (:block/page target)))
+                                        (string/blank? (or (:block/title target) ""))
+                                        (empty? (:block/_parent target))))))
+            target (rand-nth! rng (vec targets))]
+        (when target
+          (let [target-uuid (:block/uuid target)
+                copied-tree (random-copied-block-tree rng source)]
+            (when (seq copied-tree)
+              ;; Simulate "copy + paste tree into empty target block" using
+              ;; replace-empty-target paste semantics.
+              (outliner-op/apply-ops!
+               conn
+               [[:insert-blocks [copied-tree
+                                 (:db/id target)
+                                 {:sibling? true
+                                  :outliner-op :paste
+                                  :replace-empty-target? true}]]]
+               {})
+              {:op :copy-paste-block-tree-into-empty-target
+               :uuid source-uuid
+               :target target-uuid
+               :copied-size (count copied-tree)})))))))
+
 (defn- op-cut-paste-block-with-child! [rng conn state _base-uuid]
   (let [db @conn
         sources (->> (existing-blocks db (:blocks @state))
@@ -1180,10 +1247,16 @@
    {:name :redo :weight 10 :f op-redo!}
    {:name :create-block :weight 10 :f op-create-block!}
    {:name :move-block :weight 6 :f op-move-block!}
+   {:name :copy-paste-block-tree-into-empty-target :weight 4 :f op-copy-paste-block-tree-into-empty-target!}
    {:name :cut-paste-block-with-child :weight 4 :f op-cut-paste-block-with-child!}
    {:name :delete-block :weight 4 :f op-delete-block!}
    {:name :update-title :weight 8 :f op-update-title!}])
 
+(deftest copy-paste-tree-op-registered-in-sim-op-table-test
+  (testing "sim op-table includes copy-paste tree op for random sync stress"
+    (is (contains? (set (map :name op-table))
+                   :copy-paste-block-tree-into-empty-target))))
+
 (deftest cut-paste-op-registered-in-sim-op-table-test
   (testing "sim op-table includes cut-paste op for random sync stress"
     (is (contains? (set (map :name op-table))
@@ -1450,6 +1523,7 @@
                    :create-block (f rng conn state base-uuid {:gen-uuid gen-uuid})
                    :update-title (f rng conn state base-uuid)
                    :move-block (f rng conn state base-uuid)
+                   :copy-paste-block-tree-into-empty-target (f rng conn state base-uuid)
                    :cut-paste-block-with-child (f rng conn state base-uuid)
                    :delete-block (f rng conn state)
                    (f rng conn))]
@@ -2674,6 +2748,158 @@
               (finally
                 (restore)))))))))
 
+(deftest ^:long ^:large-vars/cleanup-todo two-clients-a-wins-b-overlap-rebase-3-tries-test
+  (testing "three deterministic tries: B rebases pending deletes while applying overlapping remote slices"
+    (doseq [seed [301 302 303]]
+      (let [rng (make-rng seed)
+            gen-uuid #(rng-uuid rng)
+            scenario-runs 90
+            base-uuid (gen-uuid)
+            conn-a (db-test/create-conn)
+            conn-b (db-test/create-conn)
+            ops-a (d/create-conn client-op/schema-in-db)
+            ops-b (d/create-conn client-op/schema-in-db)
+            client-a (make-client repo-a)
+            client-b (make-client repo-b)
+            server (make-server)
+            history (atom [])
+            state-a (atom {:pages #{base-uuid} :blocks #{}})
+            state-b (atom {:pages #{base-uuid} :blocks #{}})
+            a-ops #{:create-block
+                    :delete-block
+                    :move-block
+                    :indent-outdent-blocks
+                    :copy-paste-block-tree-into-empty-target
+                    :undo
+                    :redo}
+            a-op-weights {:create-block 18
+                          :delete-block 10
+                          :move-block 10
+                          :indent-outdent-blocks 10
+                          :copy-paste-block-tree-into-empty-target 8
+                          :undo 8
+                          :redo 8}
+            b-ops #{:delete-block :delete-blocks}
+            b-op-weights {:delete-block 14
+                          :delete-blocks 14}
+            a-op-table (build-weighted-op-table a-ops a-op-weights :a-overlap-rebase)
+            b-op-table (build-weighted-op-table b-ops b-op-weights :b-overlap-rebase)
+            overlap-apply-count (atom 0)
+            b-rebase-with-pending (atom 0)]
+        (with-test-repos {repo-a {:conn conn-a :ops-conn ops-a}
+                          repo-b {:conn conn-b :ops-conn ops-b}}
+          (fn []
+            (let [{:keys [repro restore]} (install-invalid-tx-repro! seed history)
+                  refresh-state! (fn [state conn]
+                                   (let [db @conn
+                                         block-uuids (->> (active-block-uuids db)
+                                                          (remove (fn [uuid]
+                                                                    (some-> (d/entity db [:block/uuid uuid])
+                                                                            ldb/page?)))
+                                                          set)]
+                                     (swap! state assoc :pages #{base-uuid}
+                                            :blocks block-uuids)))
+                  sync-a-then-overlap-b!
+                  (fn [iter-idx]
+                    (sync-client! server {:repo repo-a
+                                          :conn conn-a
+                                          :client client-a
+                                          :online? true
+                                          :gen-uuid gen-uuid})
+                    (let [local-tx-b (or (client-op/get-local-tx repo-b) 0)
+                          server-t (:t @server)]
+                      (when (< local-tx-b server-t)
+                        (let [pending-before (boolean (seq (#'sync-apply/pending-txs repo-b)))
+                              remote-txs (mapv (fn [tx-data] {:tx-data tx-data})
+                                               (server-pull server local-tx-b))
+                              slices (if (<= (count remote-txs) 1)
+                                       [remote-txs]
+                                       (let [split (+ 1 (rand-int! rng (dec (count remote-txs))))
+                                             left (subvec remote-txs 0 split)
+                                             ;; Intentional overlap to mimic duplicated/out-of-order pulls.
+                                             right (subvec remote-txs (max 0 (dec split)))]
+                                         [left right]))]
+                          (when pending-before
+                            (swap! b-rebase-with-pending inc))
+                          (doseq [slice slices]
+                            (when (seq slice)
+                              (try
+                                (#'sync-apply/apply-remote-txs! repo-b client-b slice)
+                                (catch :default e
+                                  (report-history! seed history
+                                                   {:type :b-overlap-apply-remote-failed
+                                                    :iter iter-idx
+                                                    :slice-size (count slice)
+                                                    :local-tx local-tx-b
+                                                    :server-t server-t
+                                                    :pending-before pending-before
+                                                    :error (ex-data e)})
+                                  (throw e)))))
+                          (when (> (count slices) 1)
+                            (swap! overlap-apply-count inc))
+                          (client-op/update-local-tx repo-b server-t))))
+                    (refresh-state! state-a conn-a)
+                    (refresh-state! state-b conn-b))]
+              (try
+                (reset! db-sync/*repo->latest-remote-tx {})
+                (record-meta! history {:seed seed
+                                       :base-uuid base-uuid
+                                       :phase :a-wins-b-overlap-rebase
+                                       :scenario-runs scenario-runs})
+                (doseq [conn [conn-a conn-b]]
+                  (ensure-base-page! conn base-uuid))
+                (doseq [repo [repo-a repo-b]]
+                  (client-op/update-local-tx repo 0))
+
+                (let [base-a (d/entity @conn-a [:block/uuid base-uuid])]
+                  (dotimes [i 10]
+                    (let [seed-block-uuid (gen-uuid)]
+                      (create-block! conn-a base-a (str "seed-overlap-" i) seed-block-uuid)
+                      (swap! state-a update :blocks conj seed-block-uuid))))
+
+                (sync-a-then-overlap-b! -1)
+
+                (dotimes [i scenario-runs]
+                  (run-ops! rng {:repo repo-a
+                                 :conn conn-a
+                                 :base-uuid base-uuid
+                                 :state state-a
+                                 :gen-uuid gen-uuid}
+                            1
+                            history
+                            {:op-table-override a-op-table
+                             :context {:phase :a-overlap-op :iter i}})
+                  (run-ops! rng {:repo repo-b
+                                 :conn conn-b
+                                 :base-uuid base-uuid
+                                 :state state-b
+                                 :gen-uuid gen-uuid}
+                            1
+                            history
+                            {:op-table-override b-op-table
+                             :context {:phase :b-delete-op :iter i}})
+                  (sync-a-then-overlap-b! i))
+
+                (sync-a-then-overlap-b! scenario-runs)
+
+                (let [issues-a (db-issues @conn-a)
+                      issues-b (db-issues @conn-b)
+                      checksum-a (sync-checksum/recompute-checksum @conn-a)
+                      checksum-server (sync-checksum/recompute-checksum @(get @server :conn))]
+                  (is (empty? issues-a) (str "db A issues seed=" seed " " (pr-str issues-a)))
+                  (is (empty? issues-b) (str "db B issues seed=" seed " " (pr-str issues-b)))
+                  (is (= checksum-a checksum-server)
+                      (str "winner/server checksum mismatch seed=" seed
+                           " a=" checksum-a
+                           " server=" checksum-server))
+                  (is (pos? @b-rebase-with-pending)
+                      (str "expected rebases with pending deletes seed=" seed))
+                  (is (pos? @overlap-apply-count)
+                      (str "expected overlapping apply-remote slices seed=" seed))
+                  (assert-no-invalid-tx! seed history repro))
+                (finally
+                  (restore))))))))))
+
 (deftest ^:long ^:large-vars/cleanup-todo three-clients-single-repo-sim-test
   (testing "db-sync convergence with three clients sharing one repo"
     (let [seed (or (env-seed) default-seed)

+ 288 - 24
src/test/frontend/worker/db_sync_test.cljs

@@ -347,6 +347,64 @@
       (is (= tx-id payload-tx-id))
       (is (string? payload-tx-id)))))
 
+(deftest coerce-ws-server-message-accepts-legacy-tx-reject-shape-test
+  (testing "legacy tx/reject with error-detail and UUID-object ids should coerce"
+    (let [failed-tx-id (random-uuid)
+          success-tx-id (random-uuid)
+          coerced (sync-transport/coerce-ws-server-message
+                   {:type "tx/reject"
+                    :reason "db transact failed"
+                    :t 1392
+                    :error-detail "legacy server detail"
+                    :failed-tx-id {:uuid (str failed-tx-id)}
+                    :success-tx-ids [{:uuid (str success-tx-id)}]})]
+      (is (= "tx/reject" (:type coerced)))
+      (is (= "legacy server detail" (:error-detail coerced)))
+      (is (= failed-tx-id (:failed-tx-id coerced)))
+      (is (= [success-tx-id] (:success-tx-ids coerced))))))
+
+(deftest flush-pending-honors-stop-upload-debug-flag-test
+  (testing "when stop-upload debug flag is enabled, flush-pending should skip preparing/sending tx batches"
+    (let [{:keys [conn client-ops-conn child1]} (setup-parent-child)
+          tx-id (random-uuid)
+          prepare-calls (atom 0)
+          send-calls (atom 0)
+          client {:repo test-repo
+                  :graph-id "graph-1"
+                  :inflight (atom [])
+                  :ws (doto (js-obj)
+                        (aset "readyState" 1)
+                        (aset "send" (fn [_raw] (swap! send-calls inc))))}]
+      (with-datascript-conns conn client-ops-conn
+        (fn []
+          (reset! sync-apply/*repo->latest-remote-tx {test-repo 0})
+          (client-op/update-local-tx test-repo 0)
+          (ldb/transact! client-ops-conn
+                         [{:db-sync/tx-id tx-id
+                           :db-sync/pending? true
+                           :db-sync/created-at 1
+                           :db-sync/outliner-op :save-block
+                           :db-sync/normalized-tx-data
+                           [[:db/add [:block/uuid (:block/uuid child1)]
+                             :block/title
+                             "pending upload debug gate test"]]}])
+          (with-redefs [worker-state/online? (constantly true)
+                        sync-apply/prepare-upload-tx-entries
+                        (fn [_conn _pending]
+                          (swap! prepare-calls inc)
+                          {:tx-entries []
+                           :drop-tx-ids []})]
+            (#'sync-apply/set-upload-stopped! test-repo true)
+            (#'sync-apply/flush-pending! test-repo client)
+            (is (= 0 @prepare-calls))
+            (is (= 0 @send-calls))
+
+            (#'sync-apply/set-upload-stopped! test-repo false)
+            (#'sync-apply/flush-pending! test-repo client)
+            (is (= 1 @prepare-calls))
+            (is (= 0 @send-calls)))
+          (#'sync-apply/set-upload-stopped! test-repo false))))))
+
 (deftest sync-counts-counts-only-true-pending-local-ops-test
   (testing "pending-local should count only rows with :db-sync/pending? true"
     (let [{:keys [conn client-ops-conn]} (setup-parent-child)]
@@ -571,35 +629,117 @@
 
 (deftest tx-reject-stale-keeps-inflight-op-pending-test
   (testing "stale tx/reject should keep inflight ops pending for retry"
-    (let [{:keys [conn client-ops-conn]} (setup-parent-child)
-          tx-id (random-uuid)
-          *sent (atom [])
-          raw-message (js/JSON.stringify
-                       (clj->js {:type "tx/reject"
-                                 :reason "stale"
-                                 :t 3}))
+    (async done
+           (let [{:keys [conn client-ops-conn]} (setup-parent-child)
+                 tx-id (random-uuid)
+                 *sent (atom [])
+                 ws (doto (js-obj)
+                      (aset "readyState" 1)
+                      (aset "send" (fn [raw]
+                                     (swap! *sent conj (js->clj (js/JSON.parse raw) :keywordize-keys true)))))
+                 raw-message (js/JSON.stringify
+                              (clj->js {:type "tx/reject"
+                                        :reason "stale"
+                                        :t 3}))
+                 client {:repo test-repo
+                         :graph-id "graph-1"
+                         :ws ws
+                         :send-queue (atom (p/resolved nil))
+                         :inflight (atom [tx-id])
+                         :online-users (atom [])
+                         :ws-state (atom :open)}]
+             (with-datascript-conns conn client-ops-conn
+               (fn []
+                 (ldb/transact! client-ops-conn
+                                [{:db-sync/tx-id tx-id
+                                  :db-sync/created-at 1
+                                  :db-sync/pending? true}])
+                 (with-redefs [client-op/get-local-tx (constantly 0)]
+                   (sync-handle-message/handle-message! test-repo client raw-message)
+                   (-> @(:send-queue client)
+                       (p/then (fn [_]
+                                 (let [ent (d/entity @client-ops-conn [:db-sync/tx-id tx-id])]
+                                   (is (= [{:type "pull" :since 0}] @*sent))
+                                   (is (= [tx-id] @(:inflight client)))
+                                   (is (= true (:db-sync/pending? ent)))
+                                   (is (not= true (:db-sync/failed? ent))))))
+                       (p/finally (fn [] (done)))))))))))
+
+(deftest tx-reject-stale-dedupes-pull-request-test
+  (testing "repeated stale tx/reject should not send duplicated pull requests"
+    (async done
+           (let [*sent (atom [])
+                 ws (doto (js-obj)
+                      (aset "readyState" 1)
+                      (aset "send" (fn [raw]
+                                     (swap! *sent conj (js->clj (js/JSON.parse raw) :keywordize-keys true)))))
+                 raw-message (js/JSON.stringify
+                              (clj->js {:type "tx/reject"
+                                        :reason "stale"
+                                        :t 3}))
+                 client {:repo test-repo
+                         :graph-id "graph-1"
+                         :ws ws
+                         :send-queue (atom (p/resolved nil))
+                         :pending-pull-since (atom nil)
+                         :inflight (atom [])
+                         :online-users (atom [])
+                         :ws-state (atom :open)}]
+             (with-redefs [client-op/get-local-tx (constantly 0)]
+               (sync-handle-message/handle-message! test-repo client raw-message)
+               (sync-handle-message/handle-message! test-repo client raw-message)
+               (-> @(:send-queue client)
+                   (p/then (fn [_]
+                             (is (= [{:type "pull" :since 0}] @*sent))
+                             (is (= 0 @(:pending-pull-since client)))))
+                   (p/finally (fn [] (done)))))))))
+
+(deftest changed-message-dedupes-pull-request-test
+  (testing "repeated changed should not send duplicated pull requests"
+    (async done
+           (let [*sent (atom [])
+                 ws (doto (js-obj)
+                      (aset "readyState" 1)
+                      (aset "send" (fn [raw]
+                                     (swap! *sent conj (js->clj (js/JSON.parse raw) :keywordize-keys true)))))
+                 raw-message (js/JSON.stringify
+                              (clj->js {:type "changed"
+                                        :t 10}))
+                 client {:repo test-repo
+                         :graph-id "graph-1"
+                         :ws ws
+                         :send-queue (atom (p/resolved nil))
+                         :pending-pull-since (atom nil)
+                         :inflight (atom [])
+                         :online-users (atom [])
+                         :ws-state (atom :open)}]
+             (with-redefs [client-op/get-local-tx (constantly 3)]
+               (sync-handle-message/handle-message! test-repo client raw-message)
+               (sync-handle-message/handle-message! test-repo client raw-message)
+               (-> @(:send-queue client)
+                   (p/then (fn [_]
+                             (is (= [{:type "pull" :since 3}] @*sent))
+                             (is (= 3 @(:pending-pull-since client)))))
+                   (p/finally (fn [] (done)))))))))
+
+(deftest pull-ok-clears-pending-pull-request-marker-test
+  (testing "pull/ok clears pending pull marker so future changed can request next pull"
+    (let [raw-message (js/JSON.stringify
+                       (clj->js {:type "pull/ok"
+                                 :t 4
+                                 :txs []}))
           client {:repo test-repo
                   :graph-id "graph-1"
                   :ws #js {}
-                  :inflight (atom [tx-id])
+                  :pending-pull-since (atom 3)
+                  :inflight (atom [])
                   :online-users (atom [])
                   :ws-state (atom :open)}]
-      (with-datascript-conns conn client-ops-conn
-        (fn []
-          (ldb/transact! client-ops-conn
-                         [{:db-sync/tx-id tx-id
-                           :db-sync/created-at 1
-                           :db-sync/pending? true}])
-          (with-redefs [client-op/get-local-tx (constantly 0)
-                        sync-transport/ws-open? (constantly true)
-                        sync-transport/send! (fn [_coerce-f _ws message]
-                                               (swap! *sent conj message))]
-            (sync-handle-message/handle-message! test-repo client raw-message)
-            (let [ent (d/entity @client-ops-conn [:db-sync/tx-id tx-id])]
-              (is (= [{:type "pull" :since 0}] @*sent))
-              (is (= [tx-id] @(:inflight client)))
-              (is (= true (:db-sync/pending? ent)))
-              (is (not= true (:db-sync/failed? ent))))))))))
+      (with-redefs [client-op/get-local-tx (constantly 3)
+                    client-op/update-local-tx (fn [_repo _t] nil)
+                    sync-apply/flush-pending! (fn [& _] nil)]
+        (sync-handle-message/handle-message! test-repo client raw-message)
+        (is (nil? @(:pending-pull-since client)))))))
 
 (deftest hello-checksum-mismatch-logs-warning-test
   (testing "hello with matching t but mismatched checksum logs warning without throwing"
@@ -3767,6 +3907,130 @@
             (when target'
               (is (= "remote-restored" (:block/title target'))))))))))
 
+(deftest apply-remote-txs-local-delete-parent-remote-move-then-delete-parent-repro-test
+  (testing "reproduces transact-remote failure when remote moves blocks under a locally deleted parent and then retracts that parent"
+    (let [conn (db-test/create-conn-with-blocks
+                {:pages-and-blocks
+                 [{:page {:block/title "page 1"}
+                   :blocks [{:block/title "parent"}
+                            {:block/title "mover-1"}
+                            {:block/title "mover-2"}]}]})
+          client-ops-conn (d/create-conn client-op/schema-in-db)
+          parent (db-test/find-block-by-content @conn "parent")
+          mover-1 (db-test/find-block-by-content @conn "mover-1")
+          mover-2 (db-test/find-block-by-content @conn "mover-2")
+          parent-uuid (:block/uuid parent)
+          page-uuid (:block/uuid (:block/page parent))
+          mover-1-uuid (:block/uuid mover-1)
+          mover-2-uuid (:block/uuid mover-2)
+          mover-1-order (:block/order mover-1)
+          mover-2-order (:block/order mover-2)
+          remote-txs [{:tx-data [[:db/retract [:block/uuid mover-1-uuid] :block/parent [:block/uuid page-uuid]]
+                                 [:db/add [:block/uuid mover-1-uuid] :block/parent [:block/uuid parent-uuid]]
+                                 [:db/retract [:block/uuid mover-1-uuid] :block/order mover-1-order]
+                                 [:db/add [:block/uuid mover-1-uuid] :block/order "ZxV"]]}
+                     {:tx-data [[:db/retract [:block/uuid mover-2-uuid] :block/parent [:block/uuid page-uuid]]
+                                 [:db/add [:block/uuid mover-2-uuid] :block/parent [:block/uuid parent-uuid]]
+                                 [:db/retract [:block/uuid mover-2-uuid] :block/order mover-2-order]
+                                 [:db/add [:block/uuid mover-2-uuid] :block/order "ZxG"]]}
+                     {:tx-data [[:db/retractEntity [:block/uuid parent-uuid]]]}]
+          client {:repo test-repo
+                  :graph-id "graph-1"
+                  :inflight (atom [])
+                  :online-users (atom [])
+                  :ws-state (atom :open)}]
+      (with-datascript-conns conn client-ops-conn
+        (fn []
+          ;; Local delete creates pending tx requiring reverse before remote apply.
+          (outliner-core/delete-blocks! conn [parent] {})
+          (is (seq (#'sync-apply/pending-txs test-repo)))
+          (let [result (try
+                         (#'sync-apply/apply-remote-txs! test-repo client remote-txs)
+                         nil
+                         (catch :default e
+                           e))]
+            (is (instance? js/Error result))
+            (is (string/includes? (or (ex-message result) "")
+                                  "DB write failed with invalid data")
+                (str "unexpected error: " (ex-message result)))))))))
+
+(deftest apply-remote-txs-overlap-out-of-order-parent-delete-then-move-repro-test
+  (testing "reproduces missing-parent transact-remote failure when overlapping remote slices arrive out of order"
+    (let [conn (db-test/create-conn-with-blocks
+                {:pages-and-blocks
+                 [{:page {:block/title "page 1"}
+                   :blocks [{:block/title "parent"}
+                            {:block/title "mover"}
+                            {:block/title "local-pending-delete"}]}]})
+          client-ops-conn (d/create-conn client-op/schema-in-db)
+          parent (db-test/find-block-by-content @conn "parent")
+          mover (db-test/find-block-by-content @conn "mover")
+          local-delete (db-test/find-block-by-content @conn "local-pending-delete")
+          page-uuid (:block/uuid (:block/page parent))
+          parent-uuid (:block/uuid parent)
+          mover-uuid (:block/uuid mover)
+          mover-order (:block/order mover)
+          tx-delete-parent {:tx-data [[:db/retractEntity [:block/uuid parent-uuid]]]}
+          tx-move-under-parent
+          {:tx-data [[:db/retract [:block/uuid mover-uuid] :block/parent [:block/uuid page-uuid]]
+                     [:db/add [:block/uuid mover-uuid] :block/parent [:block/uuid parent-uuid]]
+                     [:db/retract [:block/uuid mover-uuid] :block/order mover-order]
+                     [:db/add [:block/uuid mover-uuid] :block/order "ZxV"]]}
+          client {:repo test-repo
+                  :graph-id "graph-1"
+                  :inflight (atom [])
+                  :online-users (atom [])
+                  :ws-state (atom :open)}]
+      (with-datascript-conns conn client-ops-conn
+        (fn []
+          ;; Keep one unrelated local pending tx so apply-remote uses reverse+rebase path.
+          (outliner-core/delete-blocks! conn [local-delete] {})
+          (is (= 1 (count (#'sync-apply/pending-txs test-repo))))
+
+          ;; Simulate overlapped/out-of-order pull slices:
+          ;; 1) later tx deletes parent
+          ;; 2) earlier tx moves a block under that parent
+          (#'sync-apply/apply-remote-txs! test-repo client [tx-delete-parent])
+          (let [result (try
+                         (#'sync-apply/apply-remote-txs! test-repo client [tx-move-under-parent])
+                         nil
+                         (catch :default e
+                           e))]
+            (is (instance? js/Error result))
+            (is (string/includes? (or (ex-message result) "")
+                                  "Nothing found for entity id")
+                (str "unexpected error: " (ex-message result)))))))))
+
+(deftest insert-blocks-reproduces-fractional-index-order-boundary-error-test
+  (testing "insert-blocks can reproduce fractional-index boundary crash when start-order >= end-order"
+    (let [conn (db-test/create-conn-with-blocks
+                {:pages-and-blocks
+                 [{:page {:block/title "page 1"}
+                   :blocks [{:block/title "target"}
+                            {:block/title "right-sibling"}]}]})
+          target (db-test/find-block-by-content @conn "target")
+          right-sibling (db-test/find-block-by-content @conn "right-sibling")]
+      ;; Force a malformed sibling order interval that matches production symptom:
+      ;; start "a4wv" and end "a4wt" (start >= end).
+      (d/transact! conn
+                   [[:db/add (:db/id target) :block/order "a4wv"]
+                    [:db/add (:db/id right-sibling) :block/order "a4wt"]])
+      (let [result (try
+                     (outliner-op/apply-ops!
+                      conn
+                      [[:insert-blocks [[{:block/title "insert crash repro"}]
+                                        (:db/id target)
+                                        {:sibling? true
+                                         :right-sibling-id (:db/id right-sibling)}]]]
+                      {})
+                     nil
+                     (catch :default e
+                       e))]
+        (is (instance? js/Error result))
+        (is (string/includes? (or (ex-message result) "")
+                              "a4wv >= a4wt")
+            (str "unexpected error: " (ex-message result)))))))
+
 (deftest rebase-persisted-row-contains-forward-and-inverse-outliner-ops-test
   (testing "rebased pending tx should always persist both forward and inverse outliner ops"
     (let [{:keys [conn client-ops-conn parent child1]} (setup-parent-child)