042-logseq-cli-add-tag-command.md

This commit is contained in:
rcmerci
2026-02-26 22:59:23 +08:00
parent 48c1f5374e
commit ccbbd3c88e
7 changed files with 494 additions and 13 deletions

View File

@@ -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.
---

View File

@@ -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}})))

View File

@@ -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])))))

View File

@@ -90,6 +90,7 @@
:missing-graph "Use --repo <name>"
:missing-repo "Use --repo <name>"
:missing-content "Use --content or pass content as args"
:missing-tag-name "Use --name <tag-name>"
:missing-query "Use --query <edn>"
: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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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")