From ccbbd3c88ee99cdd25d83963f4c0ec9e5c2224bb Mon Sep 17 00:00:00 2001 From: rcmerci Date: Thu, 26 Feb 2026 22:59:23 +0800 Subject: [PATCH] 042-logseq-cli-add-tag-command.md --- .../042-logseq-cli-add-tag-command.md | 138 +++++++++++++++ src/main/logseq/cli/command/add.cljs | 73 +++++++- src/main/logseq/cli/commands.cljs | 15 ++ src/main/logseq/cli/format.cljs | 6 + src/test/logseq/cli/commands_test.cljs | 161 ++++++++++++++++-- src/test/logseq/cli/format_test.cljs | 9 + src/test/logseq/cli/integration_test.cljs | 105 ++++++++++++ 7 files changed, 494 insertions(+), 13 deletions(-) create mode 100644 docs/agent-guide/042-logseq-cli-add-tag-command.md diff --git a/docs/agent-guide/042-logseq-cli-add-tag-command.md b/docs/agent-guide/042-logseq-cli-add-tag-command.md new file mode 100644 index 0000000000..f5d3656573 --- /dev/null +++ b/docs/agent-guide/042-logseq-cli-add-tag-command.md @@ -0,0 +1,138 @@ +# Logseq CLI Add Tag Subcommand Implementation Plan + +Goal: Add `logseq add tag` so CLI users can create a tag entity before using that tag in `add block`, `add page`, and `update`. + +Architecture: Reuse the existing CLI to db-worker-node write path by sending `:create-page` with `{:class? true}` through `:thread-api/apply-outliner-ops`. +Architecture: Keep db-worker-node protocol and HTTP endpoints unchanged because the feature composes existing worker operations. +Architecture: Validate the result after write by pulling the page entity and asserting the page has `:logseq.class/Tag` semantics. + +Tech Stack: ClojureScript, babashka.cli, Promesa, Datascript, db-worker-node, outliner ops. + +Related: Builds on docs/agent-guide/018-logseq-cli-add-tags-builtin-properties.md and docs/agent-guide/041-logseq-cli-add-block-json-identifiers.md. + +## Problem statement + +Current CLI behavior can attach only existing tags because `resolve-tag-entity` in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs` fails with `:tag-not-found` when a tag is missing. + +Current CLI behavior has no `add tag` command entry in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs`, so users cannot create custom tags from CLI. + +Current db-worker-node flow already supports page and class creation through `:thread-api/apply-outliner-ops` and `:create-page`, so the missing capability is command orchestration in CLI instead of worker transport. + +`list tag` in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/common/mcp/tools.cljs` returns entities tagged with `:logseq.class/Tag`, so `add tag` must create that exact class shape instead of a plain page. + +## Testing Plan + +I will follow `@test-driven-development` and write parsing tests before changing command implementation code. + +I will add failing action-building tests that verify required options, normalized action payload, and explicit errors for invalid input. + +I will add failing execution tests that stub transport calls and verify emitted outliner ops include `:create-page` with `{:class? true}`. + +I will add failing format tests for human output so `:add-tag` has a stable success message contract. + +I will add one integration test that creates a new tag and then uses that tag in `add block` to prove end to end behavior through db-worker-node. + +I will add one integration test that confirms failure when the same title already exists as a non-tag page, so the command does not report false success. + +NOTE: I will write *all* tests before I add any implementation behavior. + +## Scope and non-goals + +This plan adds only `add tag` under the existing `add` command group. + +This plan does not add new db-worker-node endpoints or thread-api methods. + +This plan does not add editing features such as setting class extends, class properties, or tag description during creation. + +This plan does not change existing `add block`, `add page`, or `update` option syntax. + +`add tag` accepts `--name` only, and does not support `--tag` alias. + +## Integration overview + +``` +logseq add tag --name "Quote" + -> /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs + -> /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/transport.cljs + -> db-worker-node /v1/invoke + -> :thread-api/apply-outliner-ops + -> :create-page [title {:class? true}] + -> Datascript entity tagged as :logseq.class/Tag +``` + +## Detailed implementation plan + +1. Add a failing parser help test in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` that expects `add` group help to include `add tag`. +2. Add a failing parser test in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` for `["add" "tag" "--name" "Quote"]` to produce `:add-tag`. +3. Add a failing parser validation test in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` that missing `--name` returns `:missing-tag-name`. +4. Add a failing build-action test in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` that ensures `:add-tag` action contains `:type`, `:repo`, `:graph`, and normalized `:name`. +5. Add a failing execute test in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` that stubs transport and verifies `:create-page` options include `:class? true`. +6. Add a failing execute test in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` that simulates existing non-tag page conflict and expects a deterministic CLI error code. +7. Add a failing format test in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs` for `:add-tag` human output. +8. Add a failing integration test in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs` that runs `add tag`, validates `list tag` contains the new tag, and confirms `add block --tags` with that tag succeeds. +9. Add a failing integration test in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs` for duplicate title where a normal page exists and command must fail with clear error. +10. Run focused tests and confirm all new tests fail for behavior reasons, and use `@clojure-debug` only if failures are caused by test setup mistakes. +11. Extend `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs` with a new `add-tag` command spec, entry, action builder, and executor. +12. In `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs`, implement execution via `:thread-api/apply-outliner-ops` and `[:create-page [name {:class? true}]]`. +13. In `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs`, add a post-create pull check that verifies the resulting entity is class-tagged and raise an explicit error if not. +14. Extend `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs` parse validation with `:missing-tag-name` handling for `:add-tag`. +15. Extend `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs` action dispatch and execute dispatch for `:add-tag`. +16. Extend `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs` context propagation to include the new tag field used by formatter output. +17. Extend `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs` with `format-add-tag` and command routing for `:add-tag`. +18. Run focused unit and integration tests and confirm they pass without changing unrelated command behavior. +19. Run `bb dev:lint-and-test` and confirm the repository remains green after the feature. +20. Refactor only local helper naming and shared logic inside `add.cljs` while preserving behavior and keeping tests green. + +## Edge cases to cover + +Tag names with leading `#` should be normalized consistently, or rejected consistently, with one documented behavior. + +Tag names containing namespace separators like `A/B` should produce deterministic behavior aligned with existing page creation rules. + +Duplicate tag creation should be idempotent when the existing entity is already a tag class. + +If a page with the same name exists but is not a tag class, `add tag` should fail with a dedicated error instead of silently succeeding. + +Built-in tag names should remain valid and should return existing ids without creating duplicate entities. + +The command should reject blank names after trim. + +## Verification commands + +| Command | Expected result | +| --- | --- | +| `bb dev:test -v logseq.cli.commands-test/test-parse-args-help` | New `add tag` appears in add group help assertions. | +| `bb dev:test -v logseq.cli.commands-test/test-verb-subcommand-parse-add` | New parse and validation tests for `add tag` pass. | +| `bb dev:test -v logseq.cli.commands-test/test-build-action-inspect-edit` | Build action includes `:add-tag` cases and passes. | +| `bb dev:test -v logseq.cli.commands-test/test-execute-add-tag-builds-create-page-op` | Outliner op assertions pass with `{:class? true}`. | +| `bb dev:test -v logseq.cli.format-test/test-human-output-add-remove` | Human output for `:add-tag` matches expected string. | +| `bb dev:test -v logseq.cli.integration-test/test-cli-add-tag-create-and-use` | End to end creation and usage of a new tag passes. | +| `bb dev:test -v logseq.cli.integration-test/test-cli-add-tag-rejects-existing-non-tag-page` | Conflict behavior test passes with explicit error. | +| `bb dev:lint-and-test` | Full lint and unit suite pass. | + +## Testing Details + +The tests validate user-visible behavior at parser, action, executor, formatter, and integration boundaries. + +The tests assert command success and failure contracts, and they verify persisted graph behavior with CLI reads such as `list tag` and `show`. + +The tests avoid asserting implementation details that are not externally observable. + +## Implementation Details + +- Add `add tag` spec and entry in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs`. +- Add `build-add-tag-action` and `execute-add-tag` in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/add.cljs`. +- Use existing server bootstrap and transport invoke path from `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/server.cljs` and `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/transport.cljs`. +- Create tag through `:create-page` with `{:class? true}` and verify resulting entity semantics. +- Add parse validation and missing error mapping in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs`. +- Add build and execute routing for `:add-tag` in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs`. +- Add human formatter branch for `:add-tag` in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs`. +- Add parser and executor unit tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`. +- Add formatter test in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs`. +- Add integration coverage in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs`. + +## Question + +No open questions. + +--- diff --git a/src/main/logseq/cli/command/add.cljs b/src/main/logseq/cli/command/add.cljs index 90c858923b..b8017988ab 100644 --- a/src/main/logseq/cli/command/add.cljs +++ b/src/main/logseq/cli/command/add.cljs @@ -34,9 +34,13 @@ :tags {:desc "EDN vector of tags. Identifiers can be id, :db/ident, or :block/title."} :properties {:desc "EDN map of built-in properties. Identifiers can be id, :db/ident, or :block/title."}}) +(def ^:private add-tag-spec + {:name {:desc "Tag name"}}) + (def entries [(core/command-entry ["add" "block"] :add-block "Add blocks" content-add-spec) - (core/command-entry ["add" "page"] :add-page "Create page" add-page-spec)]) + (core/command-entry ["add" "page"] :add-page "Create page" add-page-spec) + (core/command-entry ["add" "tag"] :add-tag "Create tag" add-tag-spec)]) (defn- today-page-title [config repo] @@ -1007,6 +1011,43 @@ :error {:code :missing-page-name :message "page name is required"}})))) +(defn- normalize-tag-name-option + [value] + (let [normalized (normalize-tag-value value)] + (when (string? normalized) + (let [name (string/trim normalized)] + (when (seq name) + name))))) + +(defn build-add-tag-action + [options repo] + (if-not (seq repo) + {:ok? false + :error {:code :missing-repo + :message "repo is required for add"}} + (let [name (normalize-tag-name-option (:name options))] + (if (seq name) + {:ok? true + :action {:type :add-tag + :repo repo + :graph (core/repo->graph repo) + :name name}} + {:ok? false + :error {:code :missing-tag-name + :message "tag name is required"}})))) + +(defn- pull-page-by-name + [config repo page-name] + (pull-entity config repo + [:db/id :block/name :block/title :block/uuid + {:block/tags [:db/id :db/ident :block/name :block/title]}] + [:block/name (common-util/page-name-sanity-lc page-name)])) + +(defn- tag-entity? + [entity] + (some #(= :logseq.class/Tag (:db/ident %)) + (:block/tags entity))) + (defn execute-add-block [action config] (-> (p/let [cfg (cli-server/ensure-server! config (:repo action)) @@ -1093,3 +1134,33 @@ created-ids (resolve-created-page-ids cfg (:repo action) (:page action) create-result)] {:status :ok :data {:result created-ids}}))) + +(defn execute-add-tag + [action config] + (-> (p/let [cfg (cli-server/ensure-server! config (:repo action)) + existing (pull-page-by-name cfg (:repo action) (:name action)) + existing-id (:db/id existing) + _ (when (and existing-id (not (tag-entity? existing))) + (throw (ex-info "tag already exists as a page and is not a tag" + {:code :tag-name-conflict + :name (:name action)}))) + _ (when-not existing-id + (transport/invoke cfg :thread-api/apply-outliner-ops false + [(:repo action) + [[:create-page [(:name action) {:class? true}]]] + {}])) + page (or (when existing-id existing) + (pull-page-by-name cfg (:repo action) (:name action))) + page-id (:db/id page) + _ (when-not page-id + (throw (ex-info "tag not found after create" + {:code :tag-not-found + :name (:name action)}))) + _ (when-not (tag-entity? page) + (throw (ex-info "created entity is not tagged as :logseq.class/Tag" + {:code :tag-create-not-tag + :name (:name action) + :id page-id}))) + created-ids (normalize-created-ids [page-id])] + {:status :ok + :data {:result created-ids}}))) diff --git a/src/main/logseq/cli/commands.cljs b/src/main/logseq/cli/commands.cljs index 3e8a778dcb..b5668c728f 100644 --- a/src/main/logseq/cli/commands.cljs +++ b/src/main/logseq/cli/commands.cljs @@ -62,6 +62,13 @@ :message "page name is required"} :summary summary}) +(defn- missing-tag-name-result + [summary] + {:ok? false + :error {:code :missing-tag-name + :message "tag name is required"} + :summary summary}) + (defn- missing-type-result [summary] {:ok? false @@ -167,6 +174,9 @@ (and (= command :add-page) (not (seq (:page opts)))) (missing-page-name-result summary) + (and (= command :add-tag) (not (seq (some-> (:name opts) string/trim)))) + (missing-tag-name-result summary) + (and (= command :remove) (seq args)) (command-core/invalid-options-result summary "remove does not accept subcommands") @@ -362,6 +372,9 @@ :add-page (add-command/build-add-page-action options repo) + :add-tag + (add-command/build-add-tag-action options repo) + :update-block (update-command/build-action options repo) @@ -410,6 +423,7 @@ :list-property (list-command/execute-list-property action config) :add-block (add-command/execute-add-block action config) :add-page (add-command/execute-add-page action config) + :add-tag (add-command/execute-add-tag action config) :update-block (update-command/execute-update action config) :remove (remove-command/execute-remove action config) :query (query-command/execute-query action config) @@ -427,6 +441,7 @@ (assoc result :command (or (:command action) (:type action)) :context (select-keys action [:repo :graph :page :id :ids :uuid :block :blocks + :name :source :target :update-tags :update-properties :remove-tags :remove-properties :export-type :output :import-type :input]))))) diff --git a/src/main/logseq/cli/format.cljs b/src/main/logseq/cli/format.cljs index f70756b0a0..3ff9292e45 100644 --- a/src/main/logseq/cli/format.cljs +++ b/src/main/logseq/cli/format.cljs @@ -90,6 +90,7 @@ :missing-graph "Use --repo " :missing-repo "Use --repo " :missing-content "Use --content or pass content as args" + :missing-tag-name "Use --name " :missing-query "Use --query " :unknown-query "Use `logseq query list` to see available queries" :data-dir-permission "Check filesystem permissions or set LOGSEQ_CLI_DATA_DIR" @@ -243,6 +244,10 @@ [_context ids] (str "Added page:\n" (pr-str (vec (or ids []))))) +(defn- format-add-tag + [_context ids] + (str "Added tag:\n" (pr-str (vec (or ids []))))) + (defn- format-remove [{:keys [repo page uuid id ids]}] (cond @@ -313,6 +318,7 @@ (:list-tag :list-property) (format-list-tag-or-property (:items data) now-ms) :add-block (format-add-block context (:result data)) :add-page (format-add-page context (:result data)) + :add-tag (format-add-tag context (:result data)) :remove (format-remove context) :update-block (format-update-block context) :graph-export (format-graph-export context) diff --git a/src/test/logseq/cli/commands_test.cljs b/src/test/logseq/cli/commands_test.cljs index 0443c99ce1..252997f9e9 100644 --- a/src/test/logseq/cli/commands_test.cljs +++ b/src/test/logseq/cli/commands_test.cljs @@ -73,6 +73,7 @@ (is (contains-bold? summary "list property")) (is (contains-bold? summary "add block")) (is (contains-bold? summary "add page")) + (is (contains-bold? summary "add tag")) (is (contains-bold? summary "remove")) (is (contains-bold? summary "update")) (is (contains-bold? summary "query")) @@ -133,17 +134,6 @@ (is (string/includes? plain-summary "Global options:")) (is (string/includes? plain-summary "Command options:")))) - (testing "add group shows subcommands" - (let [result (binding [style/*color-enabled?* true] - (commands/parse-args ["add"])) - summary (:summary result) - plain-summary (strip-ansi summary)] - (is (true? (:help? result))) - (is (string/includes? plain-summary "add block")) - (is (string/includes? plain-summary "add page")) - (is (contains-bold? summary "add block")) - (is (contains-bold? summary "add page")))) - (testing "remove command shows help" (let [result (binding [style/*color-enabled?* true] (commands/parse-args ["remove" "--help"])) @@ -202,6 +192,20 @@ (is (seq lines)) (is (every? #(not (string/includes? % "[options]")) lines))))) +(deftest test-parse-args-help-add-group + (testing "add group shows subcommands" + (let [result (binding [style/*color-enabled?* true] + (commands/parse-args ["add"])) + summary (:summary result) + plain-summary (strip-ansi summary)] + (is (true? (:help? result))) + (is (string/includes? plain-summary "add block")) + (is (string/includes? plain-summary "add page")) + (is (string/includes? plain-summary "add tag")) + (is (contains-bold? summary "add block")) + (is (contains-bold? summary "add page")) + (is (contains-bold? summary "add tag"))))) + (deftest test-parse-args-help-alignment (testing "graph group aligns subcommand columns" (let [result (binding [style/*color-enabled?* true] @@ -967,7 +971,23 @@ (is (true? (:ok? result))) (is (= :add-page (:command result))) (is (= "[\"TagA\"]" (get-in result [:options :tags]))) - (is (= "{:logseq.property/publishing-public? true}" (get-in result [:options :properties])))))) + (is (= "{:logseq.property/publishing-public? true}" (get-in result [:options :properties]))))) + + (testing "add tag requires name" + (let [result (commands/parse-args ["add" "tag"])] + (is (false? (:ok? result))) + (is (= :missing-tag-name (get-in result [:error :code]))))) + + (testing "add tag parses with name" + (let [result (commands/parse-args ["add" "tag" "--name" "Quote"])] + (is (true? (:ok? result))) + (is (= :add-tag (:command result))) + (is (= "Quote" (get-in result [:options :name]))))) + + (testing "add tag rejects blank name" + (let [result (commands/parse-args ["add" "tag" "--name" " "])] + (is (false? (:ok? result))) + (is (= :missing-tag-name (get-in result [:error :code])))))) (deftest test-verb-subcommand-parse-update-target-page (testing "update parses with target page" @@ -1212,6 +1232,22 @@ (is (false? (:ok? result))) (is (= :missing-page-name (get-in result [:error :code]))))) + (testing "add tag requires name" + (let [parsed {:ok? true :command :add-tag :options {}} + result (commands/build-action parsed {:repo "demo"})] + (is (false? (:ok? result))) + (is (= :missing-tag-name (get-in result [:error :code]))))) + + (testing "add tag builds normalized action" + (let [parsed {:ok? true :command :add-tag :options {:name " #Quote "}} + result (commands/build-action parsed {:repo "demo"})] + (is (true? (:ok? result))) + (is (= {:type :add-tag + :repo "logseq_db_demo" + :graph "demo" + :name "Quote"} + (:action result))))) + (testing "remove requires target" (let [parsed {:ok? true :command :remove :options {}} result (commands/build-action parsed {:repo "demo"})] @@ -1350,6 +1386,107 @@ (is (false? (:ok? result))) (is (= :invalid-options (get-in result [:error :code])))))) +(deftest test-execute-add-tag-builds-create-page-op + (async done + (let [ops* (atom nil) + created?* (atom false) + orig-ensure-server! cli-server/ensure-server! + orig-invoke transport/invoke + action {:type :add-tag + :repo "demo" + :name "Quote"}] + (set! cli-server/ensure-server! (fn [_ _] {:base-url "http://example"})) + (set! transport/invoke (fn [_ method _ args] + (case method + :thread-api/pull (let [[_ _ lookup] args] + (if (= lookup [:block/name "quote"]) + (if @created?* + {:db/id 4242 + :block/name "quote" + :block/title "Quote" + :block/tags [{:db/ident :logseq.class/Tag}]} + {}) + {})) + :thread-api/apply-outliner-ops (let [[_ ops _] args] + (reset! created?* true) + (reset! ops* ops) + {:result :ok}) + (throw (ex-info "unexpected invoke" {:method method :args args}))))) + (-> (p/let [result (add-command/execute-add-tag action {})] + (is (= :ok (:status result))) + (is (= [4242] (get-in result [:data :result]))) + (is (= [[:create-page ["Quote" {:class? true}]]] + @ops*))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally (fn [] + (set! cli-server/ensure-server! orig-ensure-server!) + (set! transport/invoke orig-invoke) + (done))))))) + +(deftest test-execute-add-tag-rejects-existing-non-tag-page + (async done + (let [action {:type :add-tag + :repo "demo" + :name "Home"} + orig-ensure-server! cli-server/ensure-server! + orig-invoke transport/invoke] + (set! cli-server/ensure-server! (fn [_ _] {:base-url "http://example"})) + (set! transport/invoke (fn [_ method _ args] + (case method + :thread-api/pull (let [[_ _ lookup] args] + (if (= lookup [:block/name "home"]) + {:db/id 99 + :block/name "home" + :block/title "Home" + :block/tags [{:db/ident :logseq.class/Page}]} + {})) + :thread-api/apply-outliner-ops + (throw (ex-info "should not create tag" {:args args})) + (throw (ex-info "unexpected invoke" {:method method :args args}))))) + (-> (add-command/execute-add-tag action {}) + (p/then (fn [_] + (is false "expected add tag conflict error"))) + (p/catch (fn [e] + (is (= :tag-name-conflict (:code (ex-data e)))))) + (p/finally (fn [] + (set! cli-server/ensure-server! orig-ensure-server!) + (set! transport/invoke orig-invoke) + (done))))))) + +(deftest test-execute-add-tag-idempotent-when-tag-exists + (async done + (let [apply-calls* (atom 0) + orig-ensure-server! cli-server/ensure-server! + orig-invoke transport/invoke + action {:type :add-tag + :repo "demo" + :name "Quote"}] + (set! cli-server/ensure-server! (fn [_ _] {:base-url "http://example"})) + (set! transport/invoke (fn [_ method _ args] + (case method + :thread-api/pull (let [[_ _ lookup] args] + (if (= lookup [:block/name "quote"]) + {:db/id 4242 + :block/name "quote" + :block/title "Quote" + :block/tags [{:db/ident :logseq.class/Tag}]} + {})) + :thread-api/apply-outliner-ops (do + (swap! apply-calls* inc) + {:result :ok}) + (throw (ex-info "unexpected invoke" {:method method :args args}))))) + (-> (p/let [result (add-command/execute-add-tag action {})] + (is (= :ok (:status result))) + (is (= [4242] (get-in result [:data :result]))) + (is (= 0 @apply-calls*))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally (fn [] + (set! cli-server/ensure-server! orig-ensure-server!) + (set! transport/invoke orig-invoke) + (done))))))) + (deftest test-execute-update-builds-batch-ops (async done (let [ops* (atom nil) diff --git a/src/test/logseq/cli/format_test.cljs b/src/test/logseq/cli/format_test.cljs index ba581c4a9f..d6b0601446 100644 --- a/src/test/logseq/cli/format_test.cljs +++ b/src/test/logseq/cli/format_test.cljs @@ -109,6 +109,15 @@ {:output-format nil})] (is (= "Added page:\n[123]" result)))) + (testing "add tag renders ids in two lines" + (let [result (format/format-result {:status :ok + :command :add-tag + :context {:repo "demo-repo" + :name "Quote"} + :data {:result [321]}} + {:output-format nil})] + (is (= "Added tag:\n[321]" result)))) + (testing "remove page renders a succinct success line" (let [result (format/format-result {:status :ok :command :remove diff --git a/src/test/logseq/cli/integration_test.cljs b/src/test/logseq/cli/integration_test.cljs index b8f2a1b8ba..36f9aa0ed9 100644 --- a/src/test/logseq/cli/integration_test.cljs +++ b/src/test/logseq/cli/integration_test.cljs @@ -857,6 +857,111 @@ (is false (str "unexpected error: " e)) (done))))))) +(deftest ^:long test-cli-add-tag-create-and-use + (async done + (let [data-dir (node-helper/create-tmp-dir "db-worker-add-tag-create")] + (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") + repo "add-tag-create-graph" + _ (fs/writeFileSync cfg-path "{:output-format :json}") + _ (run-cli ["graph" "create" "--repo" repo] data-dir cfg-path) + _ (run-cli ["--repo" repo "add" "page" "--page" "Home"] data-dir cfg-path) + add-tag-result (run-cli ["--repo" repo + "add" "tag" + "--name" "CliQuote"] + data-dir cfg-path) + add-tag-payload (parse-json-output add-tag-result) + list-tag-result (run-cli ["--repo" repo "list" "tag"] data-dir cfg-path) + list-tag-payload (parse-json-output list-tag-result) + tag-names (->> (get-in list-tag-payload [:data :items]) + (map #(or (:block/title %) (:title %) (:name %))) + set) + add-block-result (run-cli ["--repo" repo + "add" "block" + "--target-page-name" "Home" + "--content" "Tagged by add tag" + "--tags" "[\"CliQuote\"]"] + data-dir cfg-path) + add-block-payload (parse-json-output add-block-result) + _ (p/delay 100) + block-tag-names (query-tags data-dir cfg-path repo "Tagged by add tag") + stop-result (run-cli ["server" "stop" "--repo" repo] data-dir cfg-path) + stop-payload (parse-json-output stop-result)] + (is (= 0 (:exit-code add-tag-result)) + (pr-str (:error add-tag-payload))) + (is (= "ok" (:status add-tag-payload))) + (is (= "ok" (:status list-tag-payload))) + (is (contains? tag-names "CliQuote")) + (is (= 0 (:exit-code add-block-result)) + (pr-str (:error add-block-payload))) + (is (= "ok" (:status add-block-payload))) + (is (contains? block-tag-names "CliQuote")) + (is (= "ok" (:status stop-payload))) + (done)) + (p/catch (fn [e] + (is false (str "unexpected error: " e)) + (done))))))) + +(deftest ^:long test-cli-add-tag-rejects-existing-non-tag-page + (async done + (let [data-dir (node-helper/create-tmp-dir "db-worker-add-tag-conflict")] + (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") + repo "add-tag-conflict-graph" + _ (fs/writeFileSync cfg-path "{:output-format :json}") + _ (run-cli ["graph" "create" "--repo" repo] data-dir cfg-path) + _ (run-cli ["--repo" repo "add" "page" "--page" "ConflictPage"] data-dir cfg-path) + add-tag-result (run-cli ["--repo" repo + "add" "tag" + "--name" "ConflictPage"] + data-dir cfg-path) + add-tag-payload (parse-json-output add-tag-result) + list-tag-result (run-cli ["--repo" repo "list" "tag"] data-dir cfg-path) + list-tag-payload (parse-json-output list-tag-result) + tag-names (->> (get-in list-tag-payload [:data :items]) + (map #(or (:block/title %) (:title %) (:name %))) + set) + stop-result (run-cli ["server" "stop" "--repo" repo] data-dir cfg-path) + stop-payload (parse-json-output stop-result)] + (is (= 1 (:exit-code add-tag-result))) + (is (= "error" (:status add-tag-payload))) + (is (string/includes? (get-in add-tag-payload [:error :message]) + "already exists as a page and is not a tag")) + (is (not (contains? tag-names "ConflictPage"))) + (is (= "ok" (:status stop-payload))) + (done)) + (p/catch (fn [e] + (is false (str "unexpected error: " e)) + (done))))))) + +(deftest ^:long test-cli-add-tag-idempotent-existing-tag + (async done + (let [data-dir (node-helper/create-tmp-dir "db-worker-add-tag-idempotent")] + (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") + repo "add-tag-idempotent-graph" + _ (fs/writeFileSync cfg-path "{:output-format :json}") + _ (run-cli ["graph" "create" "--repo" repo] data-dir cfg-path) + first-add-result (run-cli ["--repo" repo "add" "tag" "--name" "StableTag"] + data-dir cfg-path) + first-add-payload (parse-json-output first-add-result) + second-add-result (run-cli ["--repo" repo "add" "tag" "--name" "StableTag"] + data-dir cfg-path) + second-add-payload (parse-json-output second-add-result) + list-tag-result (run-cli ["--repo" repo "list" "tag"] data-dir cfg-path) + list-tag-payload (parse-json-output list-tag-result) + stable-tags (->> (get-in list-tag-payload [:data :items]) + (filter #(= "StableTag" (or (:block/title %) (:title %) (:name %))))) + stop-result (run-cli ["server" "stop" "--repo" repo] data-dir cfg-path) + stop-payload (parse-json-output stop-result)] + (is (= 0 (:exit-code first-add-result))) + (is (= "ok" (:status first-add-payload))) + (is (= 0 (:exit-code second-add-result))) + (is (= "ok" (:status second-add-payload))) + (is (= 1 (count stable-tags))) + (is (= "ok" (:status stop-payload))) + (done)) + (p/catch (fn [e] + (is false (str "unexpected error: " e)) + (done))))))) + (deftest ^:long test-cli-query (async done (let [data-dir (node-helper/create-tmp-dir "db-worker-query")