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>
226 lines
11 KiB
Clojure
226 lines
11 KiB
Clojure
(ns logseq.cli.e2e.manifests-test
|
|
(:require [clojure.test :refer [deftest is testing]]
|
|
[logseq.cli.e2e.manifests :as manifests]))
|
|
|
|
(deftest load-cases-requires-new-map-format
|
|
(with-redefs [manifests/read-edn-file (fn [_]
|
|
[{:id "legacy-a"}
|
|
{:id "legacy-b"}])]
|
|
(let [error (try
|
|
(manifests/load-cases :non-sync)
|
|
nil
|
|
(catch clojure.lang.ExceptionInfo error
|
|
error))]
|
|
(is (some? error))
|
|
(is (re-find #"Invalid cli-e2e manifest format" (.getMessage error)))
|
|
(is (= "{:templates {...} :cases [...]}"
|
|
(:expected (ex-data error)))))))
|
|
|
|
(deftest load-cases-merges-templates-and-cases-by-rules
|
|
(with-redefs [manifests/read-edn-file
|
|
(fn [_]
|
|
{:templates
|
|
{:base {:setup ["setup-a"]
|
|
:cmds ["cmd-a"]
|
|
:cleanup ["cleanup-a"]
|
|
:tags [:base]
|
|
:vars {:nested {:left 1}
|
|
:only-base true}
|
|
:covers {:commands ["base-command"]
|
|
:options {:global ["--base"]}}
|
|
:expect {:stdout-json-paths {[:status] "ok"
|
|
[:data :base] 1}}
|
|
:graph "base-graph"}
|
|
:addon {:setup ["setup-b"]
|
|
:cmds ["cmd-b"]
|
|
:cleanup ["cleanup-b"]
|
|
:tags [:addon]
|
|
:vars {:nested {:right 2}}
|
|
:covers {:options {:graph ["--addon"]}}
|
|
:expect {:stdout-json-paths {[:data :addon] 2}}
|
|
:graph "addon-graph"}
|
|
:shared {:setup ["setup-shared"]
|
|
:cmds ["cmd-shared"]
|
|
:cleanup ["cleanup-shared"]
|
|
:tags [:shared]
|
|
:vars {:nested {:shared 9}}
|
|
:graph "shared-graph"}}
|
|
:cases
|
|
[{:id "single-parent"
|
|
:extends :base
|
|
:cmds ["cmd-case"]
|
|
:expect {:stdout-json-paths {[:data :case] 3}}
|
|
:graph "single-parent-graph"}
|
|
{:id "multi-parent"
|
|
:extends [:base :addon]
|
|
:setup ["setup-case"]
|
|
:cmds ["cmd-case"]
|
|
:cleanup ["cleanup-case"]
|
|
:tags [:case]
|
|
:vars {:nested {:leaf 3}
|
|
:only-case true}
|
|
:covers {:commands ["case-command"]
|
|
:options {:graph ["--case"]}}
|
|
:expect {:stdout-json-paths {[:data :case] 3}}
|
|
:graph "case-graph"}
|
|
{:id "negative-case"
|
|
:extends :shared
|
|
:expect {:exit 1
|
|
:stdout-json-paths {[:error :code] "boom"}}
|
|
:graph "negative-graph"}]})]
|
|
(let [[single-parent multi-parent negative-case] (manifests/load-cases :sync)]
|
|
(testing "supports :extends keyword"
|
|
(is (= "single-parent" (:id single-parent)))
|
|
(is (= ["cmd-a" "cmd-case"] (:cmds single-parent)))
|
|
(is (= "single-parent-graph" (:graph single-parent))))
|
|
(testing "append merge keys"
|
|
(is (= ["setup-a" "setup-b" "setup-case"] (:setup multi-parent)))
|
|
(is (= ["cmd-a" "cmd-b" "cmd-case"] (:cmds multi-parent)))
|
|
(is (= ["cleanup-a" "cleanup-b" "cleanup-case"] (:cleanup multi-parent)))
|
|
(is (= [:base :addon :case] (:tags multi-parent))))
|
|
(testing "deep merge keys"
|
|
(is (= {:nested {:left 1 :right 2 :leaf 3}
|
|
:only-base true
|
|
:only-case true}
|
|
(:vars multi-parent)))
|
|
(is (= {:commands ["case-command"]
|
|
:options {:global ["--base"]
|
|
:graph ["--case"]}}
|
|
(:covers multi-parent)))
|
|
(is (= {[:status] "ok"
|
|
[:data :base] 1
|
|
[:data :addon] 2
|
|
[:data :case] 3}
|
|
(get-in multi-parent [:expect :stdout-json-paths]))))
|
|
(testing "child expect overrides inherited maps when parent omits them"
|
|
(is (= {:exit 1
|
|
:stdout-json-paths {[:error :code] "boom"}}
|
|
(:expect negative-case))))
|
|
(testing "base templates can omit happy-path defaults"
|
|
(is (= ["setup-shared"] (:setup negative-case)))
|
|
(is (= ["cmd-shared"] (:cmds negative-case)))
|
|
(is (= ["cleanup-shared"] (:cleanup negative-case))))
|
|
(testing "scalar keys are overridden by child"
|
|
(is (= "case-graph" (:graph multi-parent)))))))
|
|
|
|
(deftest load-cases-detects-circular-template-inheritance-with-cycle-path
|
|
(with-redefs [manifests/read-edn-file (fn [_]
|
|
{:templates
|
|
{:a {:extends :b
|
|
:setup ["a"]}
|
|
:b {:extends :a
|
|
:setup ["b"]}}
|
|
:cases [{:id "cycle" :extends :a}]})]
|
|
(let [error (try
|
|
(manifests/load-cases :sync)
|
|
nil
|
|
(catch clojure.lang.ExceptionInfo error
|
|
error))]
|
|
(is (some? error))
|
|
(is (re-find #"Circular template inheritance" (.getMessage error)))
|
|
(is (= [:a :b :a] (:cycle (ex-data error)))))))
|
|
|
|
(deftest load-cases-validates-extends-entries
|
|
(with-redefs [manifests/read-edn-file (fn [_]
|
|
{:templates {:base {:cmds ["base"]}}
|
|
:cases [{:id "invalid-extends"
|
|
:extends [:base "bad"]
|
|
:cmds ["case"]}]})]
|
|
(let [error (try
|
|
(manifests/load-cases :non-sync)
|
|
nil
|
|
(catch clojure.lang.ExceptionInfo error
|
|
error))]
|
|
(is (some? error))
|
|
(is (re-find #"Invalid :extends entries" (.getMessage error)))
|
|
(is (= ["bad"] (:invalid-entries (ex-data error)))))))
|
|
|
|
(deftest load-cases-lint-detects-invalid-extends-references
|
|
(with-redefs [manifests/read-edn-file (fn [_]
|
|
{:templates {:base {:cmds ["base"]}
|
|
:unused {:extends :missing-template
|
|
:cmds ["unused"]}}
|
|
:cases [{:id "valid" :extends :base :cmds ["case"]}
|
|
{:id "invalid" :extends :also-missing :cmds ["case"]}]})]
|
|
(let [error (try
|
|
(manifests/load-cases :sync)
|
|
nil
|
|
(catch clojure.lang.ExceptionInfo error
|
|
error))
|
|
issues (:issues (ex-data error))]
|
|
(is (some? error))
|
|
(is (re-find #"manifest lint failed" (.getMessage error)))
|
|
(is (= #{:also-missing :missing-template}
|
|
(set (map :target (filter #(= :invalid-extends (:type %)) issues))))))))
|
|
|
|
(deftest load-cases-lint-detects-duplicate-case-ids
|
|
(with-redefs [manifests/read-edn-file (fn [_]
|
|
{:templates {:base {:cmds ["base"]}}
|
|
:cases [{:id "dup" :extends :base :cmds ["case-a"]}
|
|
{:id "dup" :extends :base :cmds ["case-b"]}]})]
|
|
(let [error (try
|
|
(manifests/load-cases :non-sync)
|
|
nil
|
|
(catch clojure.lang.ExceptionInfo error
|
|
error))
|
|
duplicate-issues (filter #(= :duplicate-case-id (:type %))
|
|
(:issues (ex-data error)))]
|
|
(is (some? error))
|
|
(is (= [{:type :duplicate-case-id :id "dup" :count 2}]
|
|
(vec duplicate-issues))))))
|
|
|
|
(deftest load-cases-lint-detects-unused-templates
|
|
(with-redefs [manifests/read-edn-file (fn [_]
|
|
{:templates {:base {:cmds ["base"]}
|
|
:unused {:cmds ["unused"]}}
|
|
:cases [{:id "only" :extends :base :cmds ["case"]}]})]
|
|
(let [error (try
|
|
(manifests/load-cases :sync)
|
|
nil
|
|
(catch clojure.lang.ExceptionInfo error
|
|
error))
|
|
unused-issues (filter #(= :unused-template (:type %))
|
|
(:issues (ex-data error)))]
|
|
(is (some? error))
|
|
(is (= [{:type :unused-template :template :unused}]
|
|
(vec unused-issues))))))
|
|
|
|
(deftest sync-multi-batch-operations-uses-state-driven-waits-instead-of-fixed-sleeps
|
|
(let [cases (manifests/load-cases :sync)
|
|
multi-batch (some #(when (= "sync-multi-batch-operations" (:id %)) %) cases)
|
|
commands (:cmds multi-batch)
|
|
upload-commands (filter #(re-find #"sync upload --graph" %) commands)
|
|
wait-commands (filter #(re-find #"wait_sync_status\.py" %) commands)]
|
|
(is (some? multi-batch))
|
|
(is (not-any? #(= "sleep 1" %) commands))
|
|
(is (= 1 (count upload-commands)))
|
|
(is (= 5 (count wait-commands)))
|
|
(is (= 3 (count (filter #(re-find #"--root-dir '\{\{tmp-dir\}\}/graphs-b'.+--timeout-s 30 --interval-s 1" %) wait-commands))))))
|
|
|
|
(deftest sync-manifest-includes-duplicate-upload-negative-case
|
|
(let [cases (manifests/load-cases :sync)
|
|
duplicate-upload (some #(when (= "sync-upload-rejects-duplicate-remote-graph" (:id %)) %) cases)
|
|
json-paths (get-in duplicate-upload [:expect :stdout-json-paths])
|
|
setup-commands (:setup duplicate-upload)
|
|
main-commands (:cmds duplicate-upload)]
|
|
(is (some? duplicate-upload))
|
|
(is (= 1 (get-in duplicate-upload [:expect :exit])))
|
|
(is (= "graph-already-exists"
|
|
(get-in duplicate-upload [:expect :stdout-json-paths [:error :code]])))
|
|
(is (= ["delete it before uploading again"]
|
|
(get-in duplicate-upload [:expect :stdout-contains])))
|
|
(is (= 1 (count (filter #(re-find #"sync upload --graph" %) setup-commands))))
|
|
(is (= 1 (count (filter #(re-find #"sync upload --graph" %) main-commands))))
|
|
(is (false? (contains? json-paths [:data :pending-local])))
|
|
(is (false? (contains? json-paths [:data :pending-asset])))
|
|
(is (false? (contains? json-paths [:data :pending-server])))
|
|
(is (false? (contains? json-paths [:data :last-error])))))
|
|
|
|
(deftest sync-status-steady-state-does-not-repeat-identical-b-side-steady-state-waits
|
|
(let [cases (manifests/load-cases :sync)
|
|
steady-state (some #(when (= "sync-status-steady-state" (:id %)) %) cases)
|
|
steady-waits (filter #(re-find #"wait_sync_status\.py.+--root-dir '\{\{tmp-dir\}\}/graphs-b'.+--timeout-s 30 --interval-s 1" %) (:cmds steady-state))]
|
|
(is (some? steady-state))
|
|
(is (= 1 (count steady-waits)))
|
|
(is (= 1 (count (filter #(re-find #"sync status --graph" %) (:cmds steady-state)))))))
|