mirror of
https://github.com/logseq/logseq.git
synced 2026-05-29 23:19:38 +00:00
042-logseq-cli-add-tag-command.md
This commit is contained in:
138
docs/agent-guide/042-logseq-cli-add-tag-command.md
Normal file
138
docs/agent-guide/042-logseq-cli-add-tag-command.md
Normal 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.
|
||||
|
||||
---
|
||||
@@ -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}})))
|
||||
|
||||
@@ -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])))))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user