(ns logseq.cli.e2e.main-test (:require [babashka.cli :as cli] [clojure.string :as string] [clojure.test :refer [deftest is]] [logseq.cli.e2e.cleanup :as cleanup] [logseq.cli.e2e.main :as main] [logseq.cli.e2e.manifests :as manifests] [logseq.cli.e2e.sync-fixture :as sync-fixture])) (def sample-cases [{:id "global-help" :cmds ["node static/logseq-cli.js --help"] :covers {:options {:global ["--help"]}} :tags [:global :smoke]} {:id "graph-create" :cmds ["node static/logseq-cli.js graph create --graph demo"] :covers {:commands ["graph create"] :options {:graph ["--type"]}} :tags [:graph]} {:id "graph-list" :cmds ["node static/logseq-cli.js graph list"] :covers {:commands ["graph list"] :options {:graph ["--file"]}} :tags [:graph :smoke]}]) (def complete-inventory {:excluded-command-prefixes ["sync" "login" "logout"] :scopes {:global {:options ["--help"]} :graph {:commands ["graph create" "graph list"] :options ["--type" "--file"]}}}) (def cli-parse-config {:alias {:i :include :h :help} :spec {:jobs {:default 4}} :coerce {:include [] :help :boolean :dry-run :boolean :skip-build :boolean :verbose :boolean :timings :boolean :jobs :long}}) (deftest cli-opts-parses-jobs-as-integer (is (= 3 (:jobs (cli/parse-opts ["--jobs" "3"] cli-parse-config))))) (deftest cli-opts-defaults-jobs-to-four (is (= 4 (:jobs (cli/parse-opts [] cli-parse-config))))) (deftest cli-opts-rejects-non-integer-jobs (is (thrown-with-msg? clojure.lang.ExceptionInfo #"Coerce failure" (cli/parse-opts ["--jobs" "nope"] cli-parse-config)))) (deftest run-rejects-jobs-less-than-one (is (thrown-with-msg? clojure.lang.ExceptionInfo #"--jobs must be a positive integer" (main/run! {:inventory complete-inventory :cases sample-cases :skip-build true :jobs 0 :run-command (fn [_] {:exit 0 :out "" :err ""}) :run-case (fn [case _opts] {:id (:id case) :status :ok})})))) (deftest run-non-sync-uses-parallel-runner-when-jobs-greater-than-one (let [parallel-call (atom nil) serial-called? (atom false)] (with-redefs [main/run-selected-cases-in-parallel! (fn [selected-cases run-case run-command opts] (reset! parallel-call {:case-ids (mapv :id selected-cases) :run-case run-case :run-command run-command :jobs (:jobs opts)}) [{:id "global-help" :status :ok} {:id "graph-list" :status :ok}]) main/run-selected-cases! (fn [& _] (reset! serial-called? true) (throw (ex-info "serial runner should not be used" {})))] (let [result (main/run! {:inventory complete-inventory :cases sample-cases :include ["smoke"] :skip-build true :jobs 2 :run-command (fn [_] {:exit 0 :out "" :err ""}) :run-case (fn [case _opts] {:id (:id case) :status :ok})})] (is (= :ok (:status result))) (is (= ["global-help" "graph-list"] (:case-ids @parallel-call))) (is (= 2 (:jobs @parallel-call))) (is (false? @serial-called?)))))) (deftest run-sync-suite-uses-parallel-runner-when-jobs-greater-than-one (let [parallel-call (atom nil) serial-called? (atom false) sync-inventory {:excluded-command-prefixes ["login" "logout"] :scopes {:sync {:commands ["sync upload" "sync status"] :options []}}} sync-cases [{:id "sync-upload" :cmds ["node static/logseq-cli.js sync upload"] :covers {:commands ["sync upload"]}} {:id "sync-status" :cmds ["node static/logseq-cli.js sync status"] :covers {:commands ["sync status"]}}]] (with-redefs [main/run-selected-cases-in-parallel! (fn [selected-cases run-case run-command opts] (reset! parallel-call {:case-ids (mapv :id selected-cases) :run-case run-case :run-command run-command :jobs (:jobs opts)}) (mapv (fn [case] {:id (:id case) :status :ok}) selected-cases)) main/run-selected-cases! (fn [& _] (reset! serial-called? true) (throw (ex-info "serial runner should not be used" {})))] (let [result (main/run! {:suite :sync :inventory sync-inventory :cases sync-cases :skip-build true :jobs 4 :run-command (fn [_] {:exit 0 :out "" :err ""}) :run-case (fn [case _opts] {:id (:id case) :status :ok})})] (is (= :ok (:status result))) (is (= ["sync-upload" "sync-status"] (:case-ids @parallel-call))) (is (= 4 (:jobs @parallel-call))) (is (false? @serial-called?)))))) (deftest run-jobs-one-keeps-serial-runner (let [serial-call (atom nil) parallel-called? (atom false)] (with-redefs [main/run-selected-cases! (fn [selected-cases run-case run-command opts] (reset! serial-call {:case-ids (mapv :id selected-cases) :run-case run-case :run-command run-command :jobs (:jobs opts)}) (mapv (fn [case] {:id (:id case) :status :ok}) selected-cases)) main/run-selected-cases-in-parallel! (fn [& _] (reset! parallel-called? true) (throw (ex-info "parallel runner should not be used" {})))] (let [result (main/run! {:inventory complete-inventory :cases sample-cases :include ["smoke"] :skip-build true :jobs 1 :run-command (fn [_] {:exit 0 :out "" :err ""}) :run-case (fn [case _opts] {:id (:id case) :status :ok})})] (is (= :ok (:status result))) (is (= ["global-help" "graph-list"] (:case-ids @serial-call))) (is (= 1 (:jobs @serial-call))) (is (false? @parallel-called?)))))) (deftest parallel-runner-collects-completions-before-rethrowing-failure (let [started (atom []) finished (atom []) started-latch (java.util.concurrent.CountDownLatch. 2) release-success (promise) cases [{:id "global-help"} {:id "graph-list"}] error (try (main/run-selected-cases-in-parallel! cases (fn [case _opts] (swap! started conj (:id case)) (.countDown started-latch) (.await started-latch) (if (= "graph-list" (:id case)) (do (swap! finished conj [:failed (:id case)]) (deliver release-success true) (throw (ex-info "boom" {:id (:id case)}))) (do @release-success (swap! finished conj [:ok (:id case)]) {:id (:id case) :status :ok}))) (fn [_] {:exit 0 :out "" :err ""}) {:jobs 2}) nil (catch Exception ex ex))] (is (instance? Exception error)) (is (= #{"global-help" "graph-list"} (set @started))) (is (= #{[:ok "global-help"] [:failed "graph-list"]} (set @finished))))) (deftest parallel-runner-elapsed-ms-starts-when-case-begins-running (let [events (atom [])] (main/run-selected-cases-in-parallel! [{:id "slow-1"} {:id "slow-2"} {:id "fast-after-queue"}] (fn [test-case _opts] (clojure.core/case (:id test-case) "slow-1" (Thread/sleep 180) "slow-2" (Thread/sleep 180) "fast-after-queue" (Thread/sleep 10)) {:id (:id test-case) :status :ok}) (fn [_] {:exit 0 :out "" :err ""}) {:jobs 2 :on-case-success (fn [payload] (swap! events conj [(:id (:result payload)) (:elapsed-ms payload)]))}) (let [elapsed-map (into {} @events)] (is (< (get elapsed-map "fast-after-queue" 1000) 120) "queued case should measure only its own execution time, not time spent waiting in the pool") (is (>= (get elapsed-map "slow-1" 0) 150)) (is (>= (get elapsed-map "slow-2" 0) 150))))) (deftest select-cases-supports-case-id (is (= ["graph-create"] (mapv :id (main/select-cases sample-cases {:case "graph-create"}))))) (deftest select-cases-supports-include-tags (is (= ["global-help" "graph-list"] (mapv :id (main/select-cases sample-cases {:include ["smoke"]}))))) (deftest select-cases-omits-skipped-cases (let [cases (conj sample-cases {:id "stale-fallback" :cmds ["node static/logseq-cli.js debug pull --id 1"] :skip "Requires a removed current graph fallback" :covers {:commands ["debug pull"]} :tags [:debug :smoke]})] (is (= ["global-help" "graph-create" "graph-list"] (mapv :id (main/select-cases cases {})))) (is (= ["global-help" "graph-list"] (mapv :id (main/select-cases cases {:include ["smoke"]})))) (is (= [] (mapv :id (main/select-cases cases {:case "stale-fallback"})))))) (deftest run-fails-on-missing-coverage-before-case-execution (let [ran? (atom false)] (is (thrown-with-msg? clojure.lang.ExceptionInfo #"Missing coverage" (main/run! {:inventory complete-inventory :cases [{:id "global-help" :cmds ["node static/logseq-cli.js --help"] :covers {:options {:global ["--help"]}}}] :skip-build true :run-command (fn [_] (reset! ran? true) {:exit 0 :out "" :err ""})}))) (is (false? @ran?)))) (deftest run-succeeds-when-coverage-is-complete (let [result (main/run! {:inventory complete-inventory :cases [{:id "global-help" :cmds ["node static/logseq-cli.js --help"] :covers {:options {:global ["--help"]}}} {:id "graph-create" :cmds ["node static/logseq-cli.js graph create --type markdown"] :covers {:commands ["graph create"] :options {:graph ["--type"]}}} {:id "graph-list" :cmds ["node static/logseq-cli.js graph list --file demo.edn"] :covers {:commands ["graph list"] :options {:graph ["--file"]}}}] :skip-build true :run-command (fn [_] {:exit 0 :out "" :err ""})})] (is (= :ok (:status result))) (is (= 3 (count (:cases result)))))) (deftest targeted-run-skips-full-coverage-check (let [executed (atom [])] (let [result (main/run! {:inventory complete-inventory :cases [{:id "global-help" :cmds ["node static/logseq-cli.js --help"] :covers {:options {:global ["--help"]}}} {:id "graph-create" :cmds ["node static/logseq-cli.js graph create --graph demo"] :covers {:commands ["graph create"] :options {:graph ["--type"]}}}] :case "global-help" :skip-build true :run-command (fn [_] {:exit 0 :out "" :err ""}) :run-case (fn [case _opts] (swap! executed conj (:id case)) {:id (:id case) :status :ok})})] (is (= :ok (:status result))) (is (= ["global-help"] @executed))))) (deftest run-invokes-case-progress-hooks (let [events (atom [])] (main/run! {:inventory complete-inventory :cases sample-cases :include ["smoke"] :skip-build true :run-command (fn [_] {:exit 0 :out "" :err ""}) :run-case (fn [case _opts] {:id (:id case) :status :ok}) :on-case-start (fn [{:keys [index total case]}] (swap! events conj [:start index total (:id case)])) :on-case-success (fn [{:keys [index total result]}] (swap! events conj [:ok index total (:id result)]))}) (is (= [[:start 1 2 "global-help"] [:ok 1 2 "global-help"] [:start 2 2 "graph-list"] [:ok 2 2 "graph-list"]] @events)))) (deftest run-loads-non-sync-suite-by-default (let [suite-calls (atom [])] (with-redefs [manifests/load-inventory (fn [suite] (swap! suite-calls conj [:inventory suite]) complete-inventory) manifests/load-cases (fn [suite] (swap! suite-calls conj [:cases suite]) sample-cases)] (let [result (main/run! {:skip-build true :run-command (fn [_] {:exit 0 :out "" :err ""}) :run-case (fn [case _opts] {:id (:id case) :status :ok})})] (is (= :ok (:status result)))) (is (= [[:inventory :non-sync] [:cases :non-sync]] @suite-calls))))) (deftest run-loads-sync-suite-when-explicit (let [suite-calls (atom []) sync-inventory {:excluded-command-prefixes ["login" "logout"] :scopes {:sync {:commands ["sync upload"] :options []}}} sync-cases [{:id "sync-upload" :cmds ["node static/logseq-cli.js sync upload"] :covers {:commands ["sync upload"]}}]] (with-redefs [manifests/load-inventory (fn [suite] (swap! suite-calls conj [:inventory suite]) sync-inventory) manifests/load-cases (fn [suite] (swap! suite-calls conj [:cases suite]) sync-cases)] (let [result (main/run! {:suite :sync :skip-build true :run-command (fn [_] {:exit 0 :out "" :err ""}) :run-case (fn [case _opts] {:id (:id case) :status :ok})})] (is (= :ok (:status result)))) (is (= [[:inventory :sync] [:cases :sync]] @suite-calls))))) (deftest run-sync-suite-uses-suite-fixture-once-with-parallel-runner (let [before-called (atom 0) after-called (atom 0) prepared-case-ids (atom []) run-case-seen (atom []) parallel-call (atom nil) sync-inventory {:excluded-command-prefixes ["login" "logout"] :scopes {:sync {:commands ["sync upload" "sync status"] :options []}}} sync-cases [{:id "sync-upload" :cmds ["node static/logseq-cli.js sync upload"] :covers {:commands ["sync upload"]}} {:id "sync-status" :cmds ["node static/logseq-cli.js sync status"] :covers {:commands ["sync status"]}}]] (with-redefs [sync-fixture/before-suite! (fn [_] (swap! before-called inc) {:suite :sync}) sync-fixture/prepare-case (fn [case _suite-context] (swap! prepared-case-ids conj (:id case)) (assoc case :prepared? true)) sync-fixture/after-suite! (fn [_ _] (swap! after-called inc)) main/run-selected-cases-in-parallel! (fn [selected-cases run-case run-command opts] (reset! parallel-call {:case-ids (mapv :id selected-cases) :jobs (:jobs opts)}) (mapv (fn [case] (run-case case {:run-command run-command})) selected-cases))] (let [result (main/run! {:suite :sync :inventory sync-inventory :cases sync-cases :skip-build true :jobs 2 :run-command (fn [_] {:exit 0 :out "" :err ""}) :run-case (fn [case _opts] (swap! run-case-seen conj [(:id case) (:prepared? case)]) {:id (:id case) :status :ok})})] (is (= :ok (:status result))))) (is (= 1 @before-called)) (is (= 1 @after-called)) (is (= ["sync-upload" "sync-status"] (:case-ids @parallel-call))) (is (= 2 (:jobs @parallel-call))) (is (= ["sync-upload" "sync-status"] @prepared-case-ids)) (is (= [["sync-upload" true] ["sync-status" true]] @run-case-seen)))) (deftest list-cases-defaults-to-non-sync (let [selected-suite (atom nil) output (with-out-str (with-redefs [manifests/load-cases (fn [suite] (reset! selected-suite suite) [{:id "non-sync-case"}])] (main/list-cases! {})))] (is (= :non-sync @selected-suite)) (is (string/includes? output "non-sync-case")))) (deftest list-sync-cases-uses-sync-suite (let [selected-suite (atom nil) output (with-out-str (with-redefs [manifests/load-cases (fn [suite] (reset! selected-suite suite) [{:id "sync-case"}])] (main/list-sync-cases! {})))] (is (= :sync @selected-suite)) (is (string/includes? output "sync-case")))) (deftest test-prints-progress-and-summary (let [output (with-out-str (main/test! {:inventory complete-inventory :cases sample-cases :include ["smoke"] :skip-build true :run-command (fn [_] {:exit 0 :out "" :err ""}) :run-case (fn [case _opts] {:id (:id case) :status :ok :cmd (last (:cmds case))})}))] (is (string/includes? output "==> Running cli-e2e cases")) (is (string/includes? output "==> Build preflight: running...")) (is (string/includes? output "==> Build preflight: skipped (--skip-build)")) (is (string/includes? output "==> Prepared 2 case(s), starting execution")) (is (string/includes? output "[1/2] ▶ global-help")) (is (string/includes? output "[1/2] ✓ global-help")) (is (string/includes? output "[2/2] ▶ graph-list")) (is (string/includes? output "[2/2] ✓ graph-list")) (is (string/includes? output "Summary: 2 passed, 0 failed")))) (deftest test-parallel-output-omits-meaningless-index-prefixes (let [output (with-out-str (main/test! {:inventory complete-inventory :cases sample-cases :include ["smoke"] :skip-build true :jobs 2 :run-command (fn [_] {:exit 0 :out "" :err ""}) :run-case (fn [case _opts] (Thread/sleep (if (= "global-help" (:id case)) 25 5)) {:id (:id case) :status :ok})}))] (is (string/includes? output "▶ global-help")) (is (string/includes? output "✓ global-help")) (is (string/includes? output "✓ graph-list")) (is (not (string/includes? output "[1/2]"))) (is (not (string/includes? output "[2/2]"))))) (deftest test-timings-prints-step-details-and-slow-summary (let [output (with-out-str (main/test! {:inventory complete-inventory :cases sample-cases :include ["smoke"] :skip-build true :timings true :run-command (fn [_] {:exit 0 :out "" :err ""}) :run-case (fn [case _opts] (if (= "global-help" (:id case)) {:id "global-help" :status :ok :timings [{:phase :setup :step-index 1 :step-total 1 :elapsed-ms 12 :status :ok :cmd "setup-global"} {:phase :main :step-index 1 :step-total 1 :elapsed-ms 55 :status :ok :cmd "main-global"}]} {:id "graph-list" :status :ok :timings [{:phase :main :step-index 1 :step-total 1 :elapsed-ms 210 :status :ok :cmd "main-graph-list"}]}))}))] (is (string/includes? output "==> Step timing enabled (--timings)")) (is (string/includes? output "step timings:")) (is (string/includes? output "Slow steps (top 10):")) (is (string/includes? output "main-graph-list")))) (deftest test-without-timings-keeps-output-concise (let [output (with-out-str (main/test! {:inventory complete-inventory :cases sample-cases :include ["smoke"] :skip-build true :run-command (fn [_] {:exit 0 :out "" :err ""}) :run-case (fn [case _opts] {:id (:id case) :status :ok :timings [{:phase :main :step-index 1 :step-total 1 :elapsed-ms 88 :status :ok :cmd "hidden-step"}]})}))] (is (not (string/includes? output "==> Step timing enabled (--timings)"))) (is (not (string/includes? output "step timings:"))) (is (not (string/includes? output "Slow steps (top 10):"))))) (deftest test-sync-timings-prints-step-details-and-slow-summary (let [sync-inventory {:excluded-command-prefixes ["login" "logout"] :scopes {:sync {:commands ["sync status"] :options []}}} sync-cases [{:id "sync-status-case" :cmds ["node static/logseq-cli.js sync status"] :covers {:commands ["sync status"]}}] output (with-out-str (main/test-sync! {:inventory sync-inventory :cases sync-cases :skip-build true :timings true :run-command (fn [_] {:exit 0 :out "" :err ""}) :run-case (fn [case _opts] {:id (:id case) :status :ok :timings [{:phase :setup :step-index 1 :step-total 1 :elapsed-ms 10 :status :ok :cmd "sync-setup"} {:phase :main :step-index 1 :step-total 1 :elapsed-ms 150 :status :ok :cmd "sync-main"} {:phase :cleanup :step-index 1 :step-total 1 :elapsed-ms 20 :status :ok :cmd "sync-cleanup"}]})}))] (is (string/includes? output "==> Running cli-e2e cases")) (is (string/includes? output "==> Step timing enabled (--timings)")) (is (string/includes? output "step timings:")) (is (string/includes? output "Slow steps (top 10):")) (is (string/includes? output "sync-main")))) (deftest test-help-prints-usage-and-skips-execution (let [ran? (atom false) result (atom nil) output (with-out-str (reset! result (main/test! {:help true :run-command (fn [_] (reset! ran? true) {:exit 0 :out "" :err ""}) :run-case (fn [_ _] (reset! ran? true) {:id "unexpected" :status :ok})}))) ] (is (= :help (:status @result))) (is (false? @ran?)) (is (string/includes? output "Usage: bb -f cli-e2e/bb.edn test [options]")) (is (string/includes? output "--skip-build")) (is (not (string/includes? output "--force-build"))) (is (string/includes? output "--include TAG")) (is (string/includes? output "--case ID")) (is (string/includes? output "--jobs N")) (is (string/includes? output "Run up to N non-sync cases in parallel")) (is (string/includes? output "--skip-build --jobs 4")) (is (string/includes? output "Default: 4")) (is (string/includes? output "--timings")) (is (not (string/includes? output "--e2ee-password"))))) (deftest test-sync-help-prints-usage-and-skips-execution (let [ran? (atom false) result (atom nil) output (with-out-str (reset! result (main/test-sync! {:help true :run-command (fn [_] (reset! ran? true) {:exit 0 :out "" :err ""}) :run-case (fn [_ _] (reset! ran? true) {:id "unexpected" :status :ok})})))] (is (= :help (:status @result))) (is (false? @ran?)) (is (string/includes? output "Usage: bb -f cli-e2e/bb.edn test-sync [options]")) (is (string/includes? output "--skip-build")) (is (not (string/includes? output "--force-build"))) (is (string/includes? output "--include TAG")) (is (string/includes? output "--case ID")) (is (string/includes? output "--jobs N")) (is (string/includes? output "Run up to N sync cases in parallel")) (is (string/includes? output "--jobs 4")) (is (string/includes? output "bb -f cli-e2e/bb.edn test-sync")) (is (string/includes? output "--case sync-upload-download-mvp")) (is (not (string/includes? output "--skip-build --case sync-upload-download-mvp"))) (is (string/includes? output "Default: 4")) (is (string/includes? output "--timings")) (is (string/includes? output "--e2ee-password VALUE")) (is (string/includes? output "Default: 11111")))) (deftest run-does-not-pass-force-build-to-preflight (let [preflight-call (atom nil)] (with-redefs [logseq.cli.e2e.preflight/run! (fn [opts] (reset! preflight-call opts) {:status :ok :commands [] :missing-artifacts []})] (let [result (main/run! {:inventory complete-inventory :cases sample-cases :include ["smoke"] :skip-build false :force-build true :run-command (fn [_] {:exit 0 :out "" :err ""}) :run-case (fn [case _opts] {:id (:id case) :status :ok})})] (is (= :ok (:status result))) (is (not (contains? @preflight-call :force-build))))))) (deftest test-single-case-enables-detailed-command-logging (let [command-opts (atom nil) output (with-out-str (main/test! {:inventory complete-inventory :cases [{:id "global-help" :cmds ["node static/logseq-cli.js --help"] :covers {:options {:global ["--help"]}}}] :case "global-help" :skip-build true :run-command (fn [{:keys [cmd] :as opts}] (reset! command-opts opts) {:cmd cmd :exit 0 :out "ok" :err ""}) :run-case (fn [case {:keys [run-command]}] (let [result (run-command {:cmd (first (:cmds case)) :phase :main :step-index 1 :step-total 1})] {:id (:id case) :status :ok :cmd (:cmd result)}) )}))] (is (string/includes? output "==> Detailed case logging enabled (--case global-help)")) (is (string/includes? output " [main 1/1] $ node static/logseq-cli.js --help")) (is (true? (:stream-output? @command-opts))))) (deftest cleanup-help-prints-usage (let [output (with-out-str (main/cleanup! {:help true}))] (is (string/includes? output "Usage: bb -f cli-e2e/bb.edn cleanup")) (is (string/includes? output "Terminate cli-e2e db-worker-node processes")) (is (string/includes? output "Terminate db-sync server listeners on port 18080")) (is (string/includes? output "Remove cli-e2e temp roots")) (is (string/includes? output "--dry-run")))) (deftest cleanup-prints-summary-and-returns-status (with-redefs [cleanup/cleanup-db-worker-processes! (fn [_] {:found-pids [101 202] :killed-pids [101] :failed-pids [202]}) cleanup/cleanup-db-sync-port-processes! (fn [_] {:found-pids [303] :killed-pids [303] :failed-pids []}) cleanup/cleanup-temp-roots! (fn [_] {:found-dirs ["/tmp/logseq-cli-e2e-a" "/tmp/logseq-cli-e2e-b"] :removed-dirs ["/tmp/logseq-cli-e2e-a"] :failed-dirs ["/tmp/logseq-cli-e2e-b"]})] (let [result (atom nil) output (with-out-str (reset! result (main/cleanup! {})))] (is (= :ok (:status @result))) (is (= [101] (get-in @result [:processes :killed-pids]))) (is (= [303] (get-in @result [:db-sync-port-processes :killed-pids]))) (is (= ["/tmp/logseq-cli-e2e-a"] (get-in @result [:temp-roots :removed-dirs]))) (is (string/includes? output "db-worker-node processes: found 2, killed 1, failed 1")) (is (string/includes? output "db-sync server processes (port 18080): found 1, killed 1, failed 0")) (is (string/includes? output "temp roots: found 2, removed 1, failed 1"))))) (deftest cleanup-dry-run-prints-summary-and-passes-option (let [process-opts (atom nil) db-sync-opts (atom nil) dir-opts (atom nil)] (with-redefs [cleanup/cleanup-db-worker-processes! (fn [opts] (reset! process-opts opts) {:dry-run? true :found-pids [101 202] :would-kill-pids [101 202] :killed-pids [] :failed-pids []}) cleanup/cleanup-db-sync-port-processes! (fn [opts] (reset! db-sync-opts opts) {:dry-run? true :found-pids [303] :would-kill-pids [303] :killed-pids [] :failed-pids []}) cleanup/cleanup-temp-roots! (fn [opts] (reset! dir-opts opts) {:dry-run? true :found-dirs ["/tmp/logseq-cli-e2e-a"] :would-remove-dirs ["/tmp/logseq-cli-e2e-a"] :removed-dirs [] :failed-dirs []})] (let [result (atom nil) output (with-out-str (reset! result (main/cleanup! {:dry-run true})))] (is (= {:dry-run true} @process-opts)) (is (= {:dry-run true} @db-sync-opts)) (is (= {:dry-run true} @dir-opts)) (is (= :ok (:status @result))) (is (true? (get-in @result [:processes :dry-run?]))) (is (true? (get-in @result [:db-sync-port-processes :dry-run?]))) (is (true? (get-in @result [:temp-roots :dry-run?]))) (is (string/includes? output "[dry-run] db-worker-node processes: found 2, would kill 2")) (is (string/includes? output "[dry-run] db-sync server processes (port 18080): found 1, would kill 1")) (is (string/includes? output "[dry-run] temp roots: found 1, would remove 1"))))))