From 194118b2988d5dec10f26a64f0c165841f710965 Mon Sep 17 00:00:00 2001 From: rcmerci Date: Fri, 17 Apr 2026 20:07:57 +0800 Subject: [PATCH] enhance(cli): atomic behavior for upsert commands --- cli-e2e/spec/non_sync_cases.edn | 20 ++ src/main/logseq/cli/command/add.cljs | 72 ++++--- src/main/logseq/cli/command/upsert.cljs | 213 ++++++++++++------- src/test/logseq/cli/command/show_test.cljs | 24 ++- src/test/logseq/cli/command/upsert_test.cljs | 110 ++++++++++ src/test/logseq/cli/commands_test.cljs | 25 ++- 6 files changed, 339 insertions(+), 125 deletions(-) diff --git a/cli-e2e/spec/non_sync_cases.edn b/cli-e2e/spec/non_sync_cases.edn index fb99ec2734..8e76d98849 100644 --- a/cli-e2e/spec/non_sync_cases.edn +++ b/cli-e2e/spec/non_sync_cases.edn @@ -271,6 +271,26 @@ :upsert ["--page" "--status" "--no-status"]}}, :tags [:upsert], :extends :non-sync/graph-json-env} + {:id "page-upsert-invalid-update-properties-atomic-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert tag --graph {{graph-arg}} --name AtomicTag >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page AtomicPage --update-tags '[\"AtomicTag\"]' --update-properties '{:missing-prop \"value\"}'"], + :expect + {:exit 1, + :stdout-json-paths + {[:status] "error", + [:error :code] "property-not-found", + [:error :option] "--update-properties", + [:error :phase] "resolve-options"}, + :stdout-contains ["missing-prop"]}, + :covers + {:commands ["upsert page"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert ["--page" "--update-tags" "--update-properties"]}}, + :tags [:upsert], + :extends :non-sync/graph-json-env} {:id "node-list-by-tags-properties-json", :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert tag --graph {{graph-arg}} --name NodeTag >/dev/null" diff --git a/src/main/logseq/cli/command/add.cljs b/src/main/logseq/cli/command/add.cljs index 4b682bb05a..36deba0888 100644 --- a/src/main/logseq/cli/command/add.cljs +++ b/src/main/logseq/cli/command/add.cljs @@ -1146,8 +1146,16 @@ (:blocks action)) blocks-for-insert (flatten-block-tree blocks) status (:status action) - tags (resolve-tags cfg (:repo action) (:tags action)) - properties (resolve-properties cfg (:repo action) (:properties action)) + tags (if (contains? action :resolved-tags) + (:resolved-tags action) + (resolve-tags cfg (:repo action) (:tags action))) + properties (if (contains? action :resolved-properties) + (:resolved-properties action) + (resolve-properties cfg (:repo action) (:properties action))) + remove-properties (if (contains? action :resolved-remove-properties) + (:resolved-remove-properties action) + (resolve-property-identifiers cfg (:repo action) (:remove-properties action) + {:allow-non-built-in? true})) pos (:pos action) keep-uuid? true opts (case pos @@ -1157,37 +1165,33 @@ opts (cond-> opts keep-uuid? (assoc :keep-uuid? true)) - ops [[:insert-blocks [blocks-for-insert - target-id - (assoc opts :outliner-op :insert-blocks)]]] - insert-result (transport/invoke cfg :thread-api/apply-outliner-ops false [(:repo action) ops {}]) - block-ids (->> blocks-for-insert - (map :block/uuid) - (remove nil?) - vec) - tag-ids (when (seq tags) - (->> tags (map :db/id) (remove nil?) vec)) - _ (when (and status (seq block-ids)) - (transport/invoke cfg :thread-api/apply-outliner-ops false - [(:repo action) - [[:batch-set-property [block-ids :logseq.property/status status {}]]] - {}])) - _ (when (and (seq tag-ids) (seq block-ids)) - (p/all - (map (fn [tag-id] - (transport/invoke cfg :thread-api/apply-outliner-ops false - [(:repo action) - [[:batch-set-property [block-ids :block/tags tag-id {}]]] - {}])) - tag-ids))) - _ (when (and (seq properties) (seq block-ids)) - (p/all - (map (fn [[k v]] - (transport/invoke cfg :thread-api/apply-outliner-ops false - [(:repo action) - [[:batch-set-property [block-ids k v {}]]] - {}])) - properties))) - created-ids (resolve-created-block-ids cfg (:repo action) blocks-for-insert insert-result)] + block-refs (->> blocks-for-insert + (map :block/uuid) + (remove nil?) + (mapv (fn [block-uuid] [:block/uuid block-uuid]))) + tag-ids (->> (or tags []) + (map :db/id) + (remove nil?) + distinct + vec) + ops (cond-> [[:insert-blocks [blocks-for-insert + target-id + (assoc opts :outliner-op :insert-blocks)]]] + (and (seq block-refs) (seq remove-properties)) + (into (map (fn [property-id] + [:batch-remove-property [block-refs property-id]]) + remove-properties)) + (and status (seq block-refs)) + (conj [:batch-set-property [block-refs :logseq.property/status status {}]]) + (and (seq tag-ids) (seq block-refs)) + (into (map (fn [tag-id] + [:batch-set-property [block-refs :block/tags tag-id {}]]) + tag-ids)) + (and (seq properties) (seq block-refs)) + (into (map (fn [[k v]] + [:batch-set-property [block-refs k v {}]]) + properties))) + apply-result (transport/invoke cfg :thread-api/apply-outliner-ops false [(:repo action) ops {}]) + created-ids (resolve-created-block-ids cfg (:repo action) blocks-for-insert apply-result)] {:status :ok :data {:result created-ids}}))) diff --git a/src/main/logseq/cli/command/upsert.cljs b/src/main/logseq/cli/command/upsert.cljs index b5eebfa2f0..cfb247f075 100644 --- a/src/main/logseq/cli/command/upsert.cljs +++ b/src/main/logseq/cli/command/upsert.cljs @@ -693,6 +693,39 @@ (def ^:private pull-tag-by-name add-command/pull-tag-by-name) (def ^:private pull-property-by-name add-command/pull-property-by-name) +(defn- enrich-exception + [error context] + (let [message (or (ex-message error) (str error)) + data (merge (or (ex-data error) {}) context) + cause (when (instance? js/Error error) error)] + (if cause + (ex-info message data cause) + (ex-info message data)))) + +(defn- with-error-context + [promise context] + (-> promise + (p/catch (fn [error] + (p/rejected (enrich-exception error context)))))) + +(defn- with-option-error-context + [promise option phase context] + (with-error-context promise (merge {:option option + :phase phase} + context))) + +(defn- exception->error + [error] + (let [data (or (ex-data error) {}) + code (or (:code data) :exception) + message (or (:message data) + (ex-message error) + (str error))] + (-> data + (dissoc :code :message) + (assoc :code code + :message message)))) + (defn- ensure-property-identifiers-exist! [config repo property-idents] (if (seq property-idents) @@ -947,24 +980,15 @@ (declare append-tag-and-property-ops) (defn- execute-upsert-task-ops! - [action cfg block-ids] + [repo cfg block-ids task-op-plan] (if (seq block-ids) - (p/let [task-tag-id (ensure-task-tag-id! cfg (:repo action)) - update-properties (merge (or (:update-properties action) {}) - (task-property-overrides action)) - clear-properties (vec (distinct (or (:clear-properties action) []))) - ;; Currently only built-in properties is supported. If user properties are - ;; supported, resolution needs to happen before this fn to avoid partial update failures - _ (assert (every? db-property/logseq-property? (keys update-properties))) - _ (assert (every? db-property/logseq-property? clear-properties)) - ops (append-tag-and-property-ops [] - block-ids - {:update-tag-ids [task-tag-id] - :update-properties update-properties - :remove-properties clear-properties})] - (when (seq ops) + (let [ops (append-tag-and-property-ops [] + block-ids + task-op-plan)] + (if (seq ops) (transport/invoke cfg :thread-api/apply-outliner-ops false - [(:repo action) ops {}]))) + [repo ops {}]) + (p/resolved nil))) (p/resolved nil))) (defn- append-tag-and-property-ops @@ -990,61 +1014,76 @@ [:batch-set-property [block-ids k v {}]]) update-properties)))) -(defn- execute-extra-upsert-block-ops! - [action config block-ids] - (if (seq block-ids) - (p/let [cfg (cli-server/ensure-server! config (:repo action)) - update-tags (add-command/resolve-tags cfg (:repo action) (:update-tags action)) - update-properties (add-command/resolve-properties - cfg (:repo action) (:update-properties action) - {:allow-non-built-in? true}) - update-property-idents (keys (or update-properties {})) - _ (ensure-property-identifiers-exist! cfg (:repo action) update-property-idents) - ops (append-tag-and-property-ops [] - block-ids - {:update-tag-ids (->> update-tags (map :db/id) (remove nil?) distinct vec) - :update-properties update-properties})] - (when (seq ops) - (transport/invoke cfg :thread-api/apply-outliner-ops false - [(:repo action) ops {}]))) - (p/resolved nil))) - (defn execute-upsert-block [action config] (-> (if (= :update (:mode action)) (update-command/execute-update (assoc action :type :update-block) config) (p/let [cfg (cli-server/ensure-server! config (:repo action)) - ;; Resolve tags/properties before creating block so creation is - ;; skipped when resolution fails (e.g. tag doesn't exist) - _ (add-command/resolve-tags cfg (:repo action) (:update-tags action)) - _ (add-command/resolve-properties - cfg (:repo action) (:update-properties action) - {:allow-non-built-in? true}) - result (add-command/execute-add-block (assoc action :type :add-block) config) - created-ids (vec (or (get-in result [:data :result]) [])) - _ (execute-extra-upsert-block-ops! action config created-ids)] + update-tags (with-option-error-context + (add-command/resolve-tags cfg (:repo action) (:update-tags action)) + "--update-tags" + :resolve-options + {:command :upsert-block}) + update-properties (with-option-error-context + (add-command/resolve-properties cfg (:repo action) (:update-properties action) + {:allow-non-built-in? true}) + "--update-properties" + :resolve-options + {:command :upsert-block}) + _ (with-option-error-context + (ensure-property-identifiers-exist! cfg (:repo action) (keys (or update-properties {}))) + "--update-properties" + :resolve-options + {:command :upsert-block}) + result (add-command/execute-add-block (-> action + (assoc :type :add-block + :resolved-tags update-tags + :resolved-properties update-properties)) + config) + created-ids (vec (or (get-in result [:data :result]) []))] {:status :ok :data {:result created-ids}})) - (p/catch (fn [e] + (p/catch (fn [error] {:status :error - :error (merge {:code (or (:code (ex-data e)) :exception) - :message (or (ex-message e) (str e))} - (when-let [candidates (:candidates (ex-data e))] - {:candidates candidates}))})))) + :error (exception->error error)})))) (defn execute-upsert-page [action config] (-> (p/let [cfg (cli-server/ensure-server! config (:repo action)) update-by-id? (= :update (:mode action)) - update-tags (add-command/resolve-tags cfg (:repo action) (:update-tags action)) - remove-tags (add-command/resolve-tags cfg (:repo action) (:remove-tags action)) - update-properties (add-command/resolve-properties cfg (:repo action) (:update-properties action) - {:allow-non-built-in? true}) - remove-properties (add-command/resolve-property-identifiers cfg (:repo action) - (:remove-properties action) - {:allow-non-built-in? true}) - _ (ensure-property-identifiers-exist! cfg (:repo action) (keys (or update-properties {}))) - _ (ensure-property-identifiers-exist! cfg (:repo action) remove-properties) + update-tags (with-option-error-context + (add-command/resolve-tags cfg (:repo action) (:update-tags action)) + "--update-tags" + :resolve-options + {:command :upsert-page}) + remove-tags (with-option-error-context + (add-command/resolve-tags cfg (:repo action) (:remove-tags action)) + "--remove-tags" + :resolve-options + {:command :upsert-page}) + update-properties (with-option-error-context + (add-command/resolve-properties cfg (:repo action) (:update-properties action) + {:allow-non-built-in? true}) + "--update-properties" + :resolve-options + {:command :upsert-page}) + remove-properties (with-option-error-context + (add-command/resolve-property-identifiers cfg (:repo action) + (:remove-properties action) + {:allow-non-built-in? true}) + "--remove-properties" + :resolve-options + {:command :upsert-page}) + _ (with-option-error-context + (ensure-property-identifiers-exist! cfg (:repo action) (keys (or update-properties {}))) + "--update-properties" + :resolve-options + {:command :upsert-page}) + _ (with-option-error-context + (ensure-property-identifiers-exist! cfg (:repo action) remove-properties) + "--remove-properties" + :resolve-options + {:command :upsert-page}) page (if update-by-id? (ensure-page-by-id! cfg (:repo action) (:id action)) (ensure-page-entity! cfg (:repo action) (:page action))) @@ -1067,12 +1106,9 @@ [(:repo action) ops {}]))] {:status :ok :data {:result [page-id]}}) - (p/catch (fn [e] + (p/catch (fn [error] {:status :error - :error (merge {:code (or (:code (ex-data e)) :exception) - :message (or (ex-message e) (str e))} - (when-let [candidates (:candidates (ex-data e))] - {:candidates candidates}))})))) + :error (exception->error error)})))) (defn- normalize-status-input [value] @@ -1106,39 +1142,66 @@ status-check (resolve-task-status-action action cfg)] (if-not (:ok? status-check) {:status :error - :error (:error status-check)} - (let [action* (:action status-check)] + :error (merge (:error status-check) + {:option "--status" + :phase :validate-options + :command :upsert-task})} + (p/let [action* (:action status-check) + update-properties (merge (or (:update-properties action*) {}) + (task-property-overrides action*)) + clear-properties (vec (distinct (or (:clear-properties action*) []))) + _ (with-option-error-context + (ensure-property-identifiers-exist! cfg (:repo action*) (keys update-properties)) + "--update-properties" + :resolve-options + {:command :upsert-task}) + _ (with-option-error-context + (ensure-property-identifiers-exist! cfg (:repo action*) clear-properties) + "--no-status/--no-priority/--no-scheduled/--no-deadline" + :resolve-options + {:command :upsert-task}) + task-tag-id (with-error-context + (ensure-task-tag-id! cfg (:repo action*)) + {:phase :resolve-options + :context :task-tag + :command :upsert-task}) + task-op-plan {:update-tag-ids [task-tag-id] + :update-properties update-properties + :remove-properties clear-properties}] (case (:mode action*) :create - (p/let [result (add-command/execute-add-block (assoc action* :type :add-block) config) - created-ids (vec (or (get-in result [:data :result]) [])) - _ (execute-upsert-task-ops! action* cfg created-ids)] + (p/let [result (add-command/execute-add-block + (-> action* + (assoc :type :add-block + :resolved-tags [{:db/id task-tag-id}] + :resolved-properties update-properties + :resolved-remove-properties clear-properties) + (dissoc :status)) + config) + created-ids (vec (or (get-in result [:data :result]) []))] {:status :ok :data {:result created-ids}}) :page (p/let [page (ensure-page-entity! cfg (:repo action*) (:page action*)) page-id (:db/id page) - _ (execute-upsert-task-ops! action* cfg [page-id])] + _ (execute-upsert-task-ops! (:repo action*) cfg [page-id] task-op-plan)] {:status :ok :data {:result [page-id]}}) :update (p/let [entity (ensure-task-node! cfg (:repo action*) action*) node-id (:db/id entity) - _ (execute-upsert-task-ops! action* cfg [node-id])] + _ (execute-upsert-task-ops! (:repo action*) cfg [node-id] task-op-plan)] {:status :ok :data {:result [node-id]}}) {:status :error :error {:code :invalid-options :message "invalid upsert task mode"}})))) - (p/catch (fn [e] + (p/catch (fn [error] {:status :error - :error (merge {:code (or (:code (ex-data e)) :exception) - :message (or (ex-message e) (str e))} - (when-let [candidates (:candidates (ex-data e))] - {:candidates candidates}))})))) + :error (exception->error error)})))) (defn- asset-file-exists? [path] diff --git a/src/test/logseq/cli/command/show_test.cljs b/src/test/logseq/cli/command/show_test.cljs index b295d83fe4..ac92a65772 100644 --- a/src/test/logseq/cli/command/show_test.cljs +++ b/src/test/logseq/cli/command/show_test.cljs @@ -282,23 +282,31 @@ (is (= (str "100 > Project Alpha\n" "101 > Milestone 2026\n" "102 > API rollout") - (render-line [{:db/id 100 :block/title "Project Alpha"} - {:db/id 101 :block/title "Milestone 2026"} - {:db/id 102 :block/title "API rollout"}])))) + (style/strip-ansi + (render-line [{:db/id 100 :block/title "Project Alpha"} + {:db/id 101 :block/title "Milestone 2026"} + {:db/id 102 :block/title "API rollout"}]))))) (testing "dims breadcrumb id column like normal block ids" (let [output (binding [style/*color-enabled?* true] (render-line [{:db/id 100 :block/title "Project Alpha"} - {:db/id 101 :block/title "Milestone 2026"}]))] - (is (string/includes? output (style/dim "100"))) - (is (string/includes? output (style/dim "101"))) + {:db/id 101 :block/title "Milestone 2026"}])) + plain-lines (-> output style/strip-ansi string/split-lines) + first-column-id (fn [line] + (some-> line + string/trim + (string/split #"\s+" 2) + first))] + (is (re-find style/ansi-pattern output)) + (is (= ["100" "101"] + (mapv first-column-id plain-lines))) (is (= (str "100 > Project Alpha\n" "101 > Milestone 2026") - (style/strip-ansi output))))) + (string/join "\n" plain-lines))))) (testing "falls back to db/id when title and name are absent" (is (= "42 > 42" - (render-line [{:db/id 42}])))))) + (style/strip-ansi (render-line [{:db/id 42}]))))))) (deftest test-execute-show-human-adds-breadcrumb-for-ordinary-block (async done diff --git a/src/test/logseq/cli/command/upsert_test.cljs b/src/test/logseq/cli/command/upsert_test.cljs index 6a56d47e8d..dfdbff6001 100644 --- a/src/test/logseq/cli/command/upsert_test.cljs +++ b/src/test/logseq/cli/command/upsert_test.cljs @@ -525,3 +525,113 @@ (p/catch (fn [e] (is false (str "unexpected error: " e)))) (p/finally done))))) + +(deftest test-execute-upsert-block-create-invalid-update-properties-is-atomic + (async done + (let [mutation-called?* (atom false) + action {:type :upsert-block + :mode :create + :repo "demo-repo" + :graph "demo-graph" + :target-page-name "Home" + :blocks [{:block/title "Atomic block" :block/uuid (random-uuid)}] + :update-tags ["TagOne"] + :update-properties {:missing-prop "value"} + :pos "last-child"}] + (-> (p/with-redefs [cli-server/ensure-server! (fn [config _repo] + (p/resolved (assoc config :base-url "http://example"))) + add-command/resolve-tags (fn [_ _ _] + (p/resolved [{:db/id 100}])) + add-command/resolve-properties (fn [_ _ _ _] + (p/rejected (ex-info "property not found" + {:code :property-not-found + :property :missing-prop}))) + add-command/execute-add-block (fn [_ _] + (reset! mutation-called?* true) + (p/resolved {:status :ok + :data {:result [1001]}}))] + (p/let [result (upsert-command/execute-upsert-block action {})] + (is (= :error (:status result))) + (is (= :property-not-found (get-in result [:error :code]))) + (is (= "--update-properties" (get-in result [:error :option]))) + (is (= :resolve-options (get-in result [:error :phase]))) + (is (= :missing-prop (get-in result [:error :property]))) + (is (false? @mutation-called?*) + "must not mutate when one option fails"))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally done))))) + +(deftest test-execute-upsert-page-create-invalid-update-properties-is-atomic + (async done + (let [mutation-called?* (atom false) + action {:type :upsert-page + :mode :create + :repo "demo-repo" + :graph "demo-graph" + :page "AtomicPage" + :update-tags ["TagOne"] + :update-properties {:missing-prop "value"}}] + (-> (p/with-redefs [cli-server/ensure-server! (fn [config _repo] + (p/resolved (assoc config :base-url "http://example"))) + add-command/resolve-tags (fn [_ _ tags] + (if (seq tags) + (p/resolved [{:db/id 100}]) + (p/resolved nil))) + add-command/resolve-properties (fn [_ _ _ _] + (p/rejected (ex-info "property not found" + {:code :property-not-found + :property :missing-prop}))) + transport/invoke (fn [_ method _ _] + (when (= :thread-api/apply-outliner-ops method) + (reset! mutation-called?* true)) + (throw (ex-info "unexpected invoke" + {:method method})))] + (p/let [result (upsert-command/execute-upsert-page action {})] + (is (= :error (:status result))) + (is (= :property-not-found (get-in result [:error :code]))) + (is (= "--update-properties" (get-in result [:error :option]))) + (is (= :resolve-options (get-in result [:error :phase]))) + (is (= :missing-prop (get-in result [:error :property]))) + (is (false? @mutation-called?*) + "must not mutate page when one option fails"))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally done))))) + +(deftest test-execute-upsert-task-page-invalid-status-fails-before-mutation + (async done + (let [calls* (atom []) + action {:type :upsert-task + :mode :page + :repo "demo-repo" + :graph "demo-graph" + :page "TaskHome" + :status-input "invalid-status" + :priority :logseq.property/priority.high}] + (-> (p/with-redefs [cli-server/ensure-server! (fn [config _repo] + (p/resolved (assoc config :base-url "http://example"))) + transport/invoke (fn [_ method _ _] + (swap! calls* conj method) + (case method + :thread-api/q + (p/resolved [:logseq.property/status.todo + :logseq.property/status.doing + :logseq.property/status.done]) + + (throw (ex-info "unexpected invoke" + {:method method}))))] + (p/let [result (upsert-command/execute-upsert-task action {}) + message (or (get-in result [:error :message]) "")] + (is (= :error (:status result))) + (is (= :invalid-options (get-in result [:error :code]))) + (is (= "--status" (get-in result [:error :option]))) + (is (= :validate-options (get-in result [:error :phase]))) + (is (string/includes? message "invalid-status")) + (is (= [:thread-api/q] @calls*) + "must fail before page resolution or mutation"))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally done))))) + + diff --git a/src/test/logseq/cli/commands_test.cljs b/src/test/logseq/cli/commands_test.cljs index dd4cfaf10f..6849171fa7 100644 --- a/src/test/logseq/cli/commands_test.cljs +++ b/src/test/logseq/cli/commands_test.cljs @@ -3309,15 +3309,19 @@ (p/catch (fn [e] (is false (str "unexpected error: " e)))) (p/finally done)))) -(deftest test-execute-upsert-block-create-applies-extra-tag-property-ops +(deftest test-execute-upsert-block-create-resolves-options-before-add-block (async done - (let [ops* (atom nil) + (let [add-action* (atom nil) + property-lookups* (atom []) + apply-called?* (atom false) action {:type :upsert-block :mode :create :repo "demo" :update-tags [:tag/new] :update-properties {:logseq.property/deadline "2026-01-25T12:00:00Z"}}] (-> (p/with-redefs [cli-server/list-graphs (fn [_] ["demo"]) cli-server/ensure-server! (fn [_ _] {:base-url "http://example"}) - add-command/execute-add-block (fn [_ _] (p/resolved {:status :ok :data {:result [11 12]}})) + add-command/execute-add-block (fn [add-action _] + (reset! add-action* add-action) + (p/resolved {:status :ok :data {:result [11 12]}})) add-command/resolve-tags (fn [_ _ tags] (p/resolved (cond (= tags [:tag/new]) [{:db/id 101}] :else nil))) @@ -3325,19 +3329,24 @@ transport/invoke (fn [_ method _ args] (case method :thread-api/pull (let [[_ _ lookup] args] + (swap! property-lookups* conj lookup) (if (and (vector? lookup) (= :db/ident (first lookup))) {:db/id 99} {})) - :thread-api/apply-outliner-ops (let [[_ ops _] args] - (reset! ops* ops) + :thread-api/apply-outliner-ops (do + (reset! apply-called?* true) {:result :ok}) (throw (ex-info "unexpected invoke" {:method method :args args}))))] (p/let [result (commands/execute action {})] (is (= :ok (:status result))) (is (= [11 12] (get-in result [:data :result]))) - (is (= [[:batch-set-property [[11 12] :block/tags 101 {}]] - [:batch-set-property [[11 12] :logseq.property/deadline "2026-01-25T12:00:00Z" {}]]] - @ops*)))) + (is (= :add-block (:type @add-action*))) + (is (= [{:db/id 101}] (:resolved-tags @add-action*))) + (is (= {:logseq.property/deadline "2026-01-25T12:00:00Z"} + (:resolved-properties @add-action*))) + (is (some #{[:db/ident :logseq.property/deadline]} @property-lookups*)) + (is (false? @apply-called?*) + "upsert layer should not perform extra post-create mutations"))) (p/catch (fn [e] (is false (str "unexpected error: " e)))) (p/finally done)))))