Establishes version control for the personal Logseq DB fork before starting the server-client architecture migration and domain features described in docs/agent-guide and the approved implementation plan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
462 lines
15 KiB
Clojure
462 lines
15 KiB
Clojure
(ns logseq.e2e.rtc-extra-part2-test
|
|
(:require [clojure.java.io :as io]
|
|
[clojure.string :as string]
|
|
[clojure.test :refer [deftest testing is use-fixtures]]
|
|
[jsonista.core :as json]
|
|
[logseq.e2e.block :as b]
|
|
[logseq.e2e.const :refer [*page1 *page2 *graph-name*]]
|
|
[logseq.e2e.custom-report :as custom-report]
|
|
[logseq.e2e.fixtures :as fixtures]
|
|
[logseq.e2e.graph :as graph]
|
|
[logseq.e2e.keyboard :as k]
|
|
[logseq.e2e.page :as page]
|
|
[logseq.e2e.rtc :as rtc]
|
|
[logseq.e2e.util :as util]
|
|
[wally.main :as w]))
|
|
|
|
(use-fixtures :once
|
|
fixtures/open-2-pages
|
|
(partial fixtures/prepare-rtc-graph-fixture "rtc-extra-part2-test-graph"))
|
|
|
|
(use-fixtures :each
|
|
fixtures/new-logseq-page-in-rtc)
|
|
|
|
(def ^:private stress-default-rounds 1)
|
|
(def ^:private stress-default-ops-per-client 50)
|
|
(def ^:private stress-default-seed-blocks 20)
|
|
(def ^:private stress-default-seed 20260330)
|
|
(def ^:private stress-max-seed-depth 4)
|
|
(def ^:private severe-sync-log-patterns
|
|
["db-sync/checksum-mismatch"
|
|
"db-sync/tx-rejected"
|
|
"db-sync/apply-remote-txs-failed"])
|
|
(def ^:private random-edit-actions
|
|
[:new :save :indent-outdent :delete-existing :undo :redo])
|
|
|
|
(defn- env-int
|
|
[k default]
|
|
(let [raw (System/getenv k)]
|
|
(if-not (string/blank? raw)
|
|
(try
|
|
(Integer/parseInt raw)
|
|
(catch Throwable _
|
|
default))
|
|
default)))
|
|
|
|
(defn- recent-console-logs
|
|
[]
|
|
(->> (some-> custom-report/*pw-page->console-logs* deref vals)
|
|
(mapcat identity)
|
|
vec))
|
|
|
|
(defn- assert-no-severe-sync-errors!
|
|
[]
|
|
(let [logs (recent-console-logs)
|
|
matched (->> logs
|
|
(filter (fn [line]
|
|
(some #(string/includes? line %) severe-sync-log-patterns)))
|
|
vec)]
|
|
(is (empty? matched)
|
|
(str "found severe sync errors in console logs: "
|
|
(pr-str (take-last 20 matched))))))
|
|
|
|
(defn- page-sync-state
|
|
[pw-page]
|
|
(w/with-page pw-page
|
|
(util/exit-edit)
|
|
{:rtc-tx (rtc/get-rtc-tx)
|
|
:blocks (util/get-page-blocks-contents)}))
|
|
|
|
(defn- assert-two-pages-synced!
|
|
[]
|
|
(let [s1 (page-sync-state @*page1)
|
|
s2 (page-sync-state @*page2)
|
|
tx1 (:rtc-tx s1)
|
|
tx2 (:rtc-tx s2)]
|
|
(is (= (:blocks s1) (:blocks s2))
|
|
(str "page blocks diverged: "
|
|
(pr-str {:page1-count (count (:blocks s1))
|
|
:page2-count (count (:blocks s2))
|
|
:page1-tail (take-last 8 (:blocks s1))
|
|
:page2-tail (take-last 8 (:blocks s2))})))
|
|
(is (= (:local-tx tx1) (:remote-tx tx1))
|
|
(str "page1 rtc-tx not converged: " (pr-str tx1)))
|
|
(is (= (:local-tx tx2) (:remote-tx tx2))
|
|
(str "page2 rtc-tx not converged: " (pr-str tx2)))))
|
|
|
|
(defn- try-indent!
|
|
[]
|
|
(if-let [editor (util/get-editor)]
|
|
(let [[x1 _] (util/bounding-xy editor)]
|
|
(k/tab)
|
|
(if-let [editor' (util/get-editor)]
|
|
(let [[x2 _] (util/bounding-xy editor')]
|
|
(> x2 x1))
|
|
false))
|
|
false))
|
|
|
|
(defn- try-outdent!
|
|
[]
|
|
(if-let [editor (util/get-editor)]
|
|
(let [[x1 _] (util/bounding-xy editor)]
|
|
(k/shift+tab)
|
|
(if-let [editor' (util/get-editor)]
|
|
(let [[x2 _] (util/bounding-xy editor')]
|
|
(> x1 x2))
|
|
false))
|
|
false))
|
|
|
|
(defn- align-depth!
|
|
[depth target]
|
|
(loop [d depth]
|
|
(cond
|
|
(< d target) (if (try-indent!)
|
|
(recur (inc d))
|
|
d)
|
|
(> d target) (if (try-outdent!)
|
|
(recur (dec d))
|
|
d)
|
|
:else d)))
|
|
|
|
(defn- new-block-safe!
|
|
[title]
|
|
(loop [attempt 4]
|
|
(let [created?
|
|
(try
|
|
(b/new-block "")
|
|
(b/save-block title)
|
|
true
|
|
(catch Throwable _
|
|
false))]
|
|
(if created?
|
|
true
|
|
(if (zero? attempt)
|
|
(throw (ex-info "new-block-safe failed" {:title title}))
|
|
(do
|
|
(util/exit-edit)
|
|
(util/wait-timeout 80)
|
|
(try
|
|
(b/open-last-block)
|
|
(catch Throwable _
|
|
nil))
|
|
(util/wait-timeout 80)
|
|
(recur (dec attempt))))))))
|
|
|
|
(defn- sync-by-trigger!
|
|
([tag]
|
|
(sync-by-trigger! tag nil))
|
|
([tag checkpoints]
|
|
(let [target-tx (some->> checkpoints
|
|
vals
|
|
(filter integer?)
|
|
seq
|
|
(apply max))]
|
|
;; Ensure both pages have observed all prior edit/undo-redo txs first.
|
|
(when target-tx
|
|
(w/with-page @*page1
|
|
(rtc/wait-tx-update-to target-tx))
|
|
(w/with-page @*page2
|
|
(rtc/wait-tx-update-to target-tx)))
|
|
(let [{:keys [remote-tx]}
|
|
(w/with-page @*page1
|
|
(rtc/with-wait-tx-updated
|
|
(new-block-safe! (str "sync-trigger-" tag))))]
|
|
(w/with-page @*page1
|
|
(rtc/wait-tx-update-to remote-tx))
|
|
(w/with-page @*page2
|
|
(rtc/wait-tx-update-to remote-tx))))))
|
|
|
|
(defn- seed-long-nested-page!
|
|
[seed]
|
|
(let [seed-blocks (max 20 (env-int "DB_SYNC_E2E_STRESS_SEED_BLOCKS" stress-default-seed-blocks))
|
|
rng (java.util.Random. (long (+ seed 97)))
|
|
titles (w/with-page @*page1
|
|
(util/exit-edit)
|
|
(loop [i 0
|
|
depth 0
|
|
titles #{}]
|
|
(if (< i seed-blocks)
|
|
(let [title (format "seed-r%s-%03d" seed i)
|
|
target-depth (.nextInt rng (inc stress-max-seed-depth))]
|
|
(new-block-safe! title)
|
|
(recur (inc i)
|
|
(align-depth! depth target-depth)
|
|
(conj titles title)))
|
|
(do
|
|
(util/exit-edit)
|
|
titles))))]
|
|
(sync-by-trigger! (str "seed-" seed))
|
|
titles))
|
|
|
|
(defn- next-action
|
|
[rng]
|
|
(nth random-edit-actions
|
|
(.nextInt rng (count random-edit-actions))))
|
|
|
|
(defn- delete-existing-random-block!
|
|
[rng known-titles]
|
|
(loop [attempt 8]
|
|
(if (zero? attempt)
|
|
0
|
|
(let [titles (vec @known-titles)]
|
|
(if (empty? titles)
|
|
0
|
|
(let [title (nth titles (.nextInt rng (count titles)))
|
|
deleted?
|
|
(try
|
|
(b/jump-to-block title)
|
|
(b/delete-blocks)
|
|
true
|
|
(catch Throwable _
|
|
false))]
|
|
(if deleted?
|
|
(do
|
|
(swap! known-titles disj title)
|
|
1)
|
|
(recur (dec attempt)))))))))
|
|
|
|
(defn- random-edit-op!
|
|
[rng known-titles client-prefix round op-idx]
|
|
(let [base (format "%s-r%s-op%s" client-prefix round op-idx)]
|
|
(case (next-action rng)
|
|
:new
|
|
(let [title (str base "-new")]
|
|
(new-block-safe! title)
|
|
(swap! known-titles conj title)
|
|
1)
|
|
|
|
:save
|
|
(let [save-title (str base "-save-updated")]
|
|
(new-block-safe! (str base "-save"))
|
|
(b/save-block save-title)
|
|
(swap! known-titles conj save-title)
|
|
2)
|
|
|
|
:indent-outdent
|
|
(let [title (str base "-nest")]
|
|
(new-block-safe! title)
|
|
(swap! known-titles conj title)
|
|
(+ 1
|
|
(if (try-indent!) 1 0)
|
|
(if (try-outdent!) 1 0)))
|
|
|
|
:delete-existing
|
|
(delete-existing-random-block! rng known-titles)
|
|
|
|
:undo
|
|
(do
|
|
(b/undo)
|
|
0)
|
|
|
|
:redo
|
|
(do
|
|
(b/redo)
|
|
0))))
|
|
|
|
(defn- local-random-edit-batch!
|
|
[rng known-titles client-prefix round]
|
|
(let [ops (max 1 (env-int "DB_SYNC_E2E_STRESS_OPS_PER_CLIENT" stress-default-ops-per-client))]
|
|
(loop [i 0
|
|
undo-steps 0]
|
|
(if (< i ops)
|
|
(recur (inc i)
|
|
(+ undo-steps
|
|
(random-edit-op! rng known-titles client-prefix round i)))
|
|
(do
|
|
(util/exit-edit)
|
|
undo-steps)))))
|
|
|
|
(defn- local-undo-redo-batch!
|
|
[undo-steps]
|
|
(let [steps (max 1 undo-steps)]
|
|
;; Undo and redo exactly what this client edited in the current round.
|
|
(b/open-last-block)
|
|
(dotimes [_ steps]
|
|
(b/undo))
|
|
(dotimes [_ steps]
|
|
(b/redo))
|
|
(util/exit-edit)))
|
|
|
|
(defn- run-two-clients-without-sync!
|
|
[p1-fn p2-fn]
|
|
[(p1-fn)
|
|
(p2-fn)])
|
|
|
|
(deftest online-two-clients-undo-redo-stress-test
|
|
(testing "two online RTC clients survive random edits + undo/redo loops on a long nested page"
|
|
(let [rounds (max 1 (env-int "DB_SYNC_E2E_STRESS_ROUNDS" stress-default-rounds))
|
|
seed (long (env-int "DB_SYNC_E2E_STRESS_SEED" stress-default-seed))
|
|
p1-rng (java.util.Random. (long (+ seed 101)))
|
|
p2-rng (java.util.Random. (long (+ seed 202)))
|
|
known-titles (atom (seed-long-nested-page! seed))]
|
|
(dotimes [round rounds]
|
|
(let [p1-undo-steps (atom 0)
|
|
p2-undo-steps (atom 0)
|
|
;; Phase 1: edit both online clients without forcing sync between them.
|
|
[_ _]
|
|
(run-two-clients-without-sync!
|
|
#(w/with-page @*page1
|
|
(reset! p1-undo-steps
|
|
(local-random-edit-batch! p1-rng known-titles "p1" round)))
|
|
#(w/with-page @*page2
|
|
(reset! p2-undo-steps
|
|
(local-random-edit-batch! p2-rng known-titles "p2" round))))
|
|
p1-edit-remote-tx (w/with-page @*page1
|
|
(-> (rtc/get-rtc-tx) :local-tx))
|
|
p2-edit-remote-tx (w/with-page @*page2
|
|
(-> (rtc/get-rtc-tx) :local-tx))
|
|
;; Phase 2: undo+redo both online clients without forcing sync between them.
|
|
[_ _]
|
|
(run-two-clients-without-sync!
|
|
#(w/with-page @*page1
|
|
(local-undo-redo-batch! @p1-undo-steps))
|
|
#(w/with-page @*page2
|
|
(local-undo-redo-batch! @p2-undo-steps)))
|
|
p1-undo-remote-tx (w/with-page @*page1
|
|
(-> (rtc/get-rtc-tx) :local-tx))
|
|
p2-undo-remote-tx (w/with-page @*page2
|
|
(-> (rtc/get-rtc-tx) :local-tx))]
|
|
|
|
(sync-by-trigger!
|
|
round
|
|
{:p1-edit p1-edit-remote-tx
|
|
:p2-edit p2-edit-remote-tx
|
|
:p1-undo p1-undo-remote-tx
|
|
:p2-undo p2-undo-remote-tx})
|
|
(assert-two-pages-synced!)
|
|
(assert-no-severe-sync-errors!))))))
|
|
|
|
;;; https://github.com/logseq/db-test/issues/651
|
|
(deftest issue-651-block-title-double-transit-encoded-test
|
|
(testing "
|
|
1. create pages named \"bbb\", \"aaa\", and turn these pages into tag
|
|
2. set \"bbb\" parent to \"aaa\"
|
|
3. create a new page \"ccc\", and create a simple query with filter tags = aaa/bbb
|
|
wait for 5-10 seconds, will found that \"aaa/bbb\" became \"aaa/<encrypted-string>\"
|
|
"
|
|
(w/with-page @*page1
|
|
(page/new-page "aaa")
|
|
(page/convert-to-tag "aaa")
|
|
(page/new-page "bbb")
|
|
(page/convert-to-tag "bbb" :extends ["aaa"])
|
|
(page/new-page "ccc")
|
|
(b/new-block "")
|
|
(util/input-command "query")
|
|
(w/click (util/-query-last "button:text('filter')"))
|
|
(util/input "tags")
|
|
(w/click "a.menu-link:has-text('tags')")
|
|
(w/click "a.menu-link:has-text('bbb')")
|
|
(util/wait-timeout 5000) ;as described in issue-url
|
|
)
|
|
(let [{:keys [remote-tx]}
|
|
(w/with-page @*page1
|
|
(rtc/with-wait-tx-updated
|
|
(b/new-block "done")))]
|
|
(w/with-page @*page2
|
|
(rtc/wait-tx-update-to remote-tx)))
|
|
|
|
;; check the selected tag still exists after the query waits and syncs
|
|
(w/with-page @*page1
|
|
(page/goto-page "bbb"))
|
|
(w/with-page @*page2
|
|
(page/goto-page "bbb"))
|
|
|
|
(rtc/validate-graphs-in-2-pw-pages)))
|
|
|
|
(deftest paste-multiple-blocks-test
|
|
(testing "
|
|
1. create 3 blocks
|
|
- block1
|
|
- block2
|
|
- block3
|
|
2. copy these 3 blocks
|
|
3. when cursor at block3, press <enter> to create a new block
|
|
4. paste them at current position 5 times
|
|
5. validate blocks are same on both clients"
|
|
(w/with-page @*page1
|
|
(b/new-blocks ["block1" "block2" "block3"])
|
|
(util/exit-edit)
|
|
(b/select-blocks 2)
|
|
(b/copy)
|
|
(b/jump-to-block "block3")
|
|
(util/repeat-keyboard 1 "Enter"))
|
|
|
|
(dotimes [_ 5]
|
|
(let [{:keys [remote-tx]}
|
|
(w/with-page @*page1
|
|
(rtc/with-wait-tx-updated
|
|
(b/paste)))]
|
|
(w/with-page @*page2
|
|
(rtc/wait-tx-update-to remote-tx))))
|
|
|
|
(let [{:keys [remote-tx]}
|
|
(w/with-page @*page1
|
|
(rtc/with-wait-tx-updated
|
|
(b/new-block "sync-trigger")))]
|
|
(w/with-page @*page2
|
|
(rtc/wait-tx-update-to remote-tx)))
|
|
|
|
(let [expected (vec (concat ["block1" "block2" "block3"]
|
|
(take (* 3 5) (cycle ["block1" "block2" "block3"]))
|
|
["sync-trigger"]))]
|
|
(w/with-page @*page1
|
|
(util/exit-edit)
|
|
(is (= expected
|
|
(util/get-page-blocks-contents))))
|
|
|
|
(w/with-page @*page2
|
|
(util/exit-edit)
|
|
(is (= expected
|
|
(util/get-page-blocks-contents)))))
|
|
|
|
(rtc/validate-graphs-in-2-pw-pages)))
|
|
|
|
(deftest asset-blocks-validate-after-init-downloaded-test
|
|
(testing "
|
|
- add some assets in client1
|
|
- remove local graph in client2
|
|
- re-download the remote graph in client2
|
|
- compare asset-blocks data in both clients"
|
|
(let [asset-file "../assets/icon.png"
|
|
page-title (w/with-page @*page1 (page/get-page-name))]
|
|
(w/with-page @*page1
|
|
(let [p (w/get-page)]
|
|
(.onFileChooser p (reify java.util.function.Consumer
|
|
(accept [_ fc]
|
|
(.setFiles fc (into-array java.nio.file.Path [(java.nio.file.Paths/get asset-file (into-array String []))])))))
|
|
(b/new-block "asset block")
|
|
(util/input-command "Upload an asset")
|
|
(w/wait-for ".ls-block img")))
|
|
|
|
(let [{:keys [remote-tx]}
|
|
(w/with-page @*page1
|
|
(rtc/with-wait-tx-updated
|
|
(b/new-block "sync done")))]
|
|
(w/with-page @*page2
|
|
(rtc/wait-tx-update-to remote-tx)))
|
|
|
|
(w/with-page @*page2
|
|
(graph/remove-local-graph *graph-name*)
|
|
(graph/wait-for-remote-graph *graph-name*)
|
|
(graph/switch-graph *graph-name* true false)
|
|
(page/goto-page page-title)
|
|
(w/wait-for ".ls-block img")
|
|
(is (some? (.getAttribute (w/-query ".ls-block img") "src"))))
|
|
|
|
(rtc/validate-graphs-in-2-pw-pages))))
|
|
|
|
(deftest issue-683-paste-large-block-test
|
|
(testing "Copying and pasting a large block of text into sync-ed graph causes sync to fail"
|
|
(let [large-text (slurp (io/resource "large_text.txt"))]
|
|
(w/with-page @*page1
|
|
(w/eval-js (str "navigator.clipboard.writeText(" (json/write-value-as-string large-text) ")")))
|
|
(let [{:keys [remote-tx]}
|
|
(w/with-page @*page1
|
|
(rtc/with-wait-tx-updated
|
|
(b/new-block "")
|
|
(b/paste)))]
|
|
(w/with-page @*page2
|
|
(rtc/wait-tx-update-to remote-tx)))
|
|
|
|
(rtc/validate-graphs-in-2-pw-pages))))
|