Initial import of LogDarioSeq baseline
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>
This commit is contained in:
461
clj-e2e/test/logseq/e2e/rtc_extra_part2_test.clj
Normal file
461
clj-e2e/test/logseq/e2e/rtc_extra_part2_test.clj
Normal file
@@ -0,0 +1,461 @@
|
||||
(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))))
|
||||
Reference in New Issue
Block a user