mirror of
https://github.com/logseq/logseq.git
synced 2026-05-22 03:34:07 +00:00
test(sync): cover stale fix/reject flows
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user