mirror of
https://github.com/logseq/logseq.git
synced 2026-05-18 01:42:19 +00:00
enhance(cli): add example subcmd
This commit is contained in:
@@ -381,6 +381,117 @@
|
||||
:cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"]
|
||||
:tags [:query]}
|
||||
|
||||
{:id "example-show-human"
|
||||
:cmds ["{{cli}} --output human example show"]
|
||||
:expect {:exit 0
|
||||
:stdout-contains ["Selector: show"
|
||||
"Matched commands:"
|
||||
"show"
|
||||
"Examples:"]}
|
||||
:covers {:commands ["example show"]
|
||||
:options {:global ["--output"]}}
|
||||
:tags [:example]}
|
||||
|
||||
{:id "example-upsert-page-json"
|
||||
:cmds ["{{cli}} --output json example upsert page"]
|
||||
:expect {:exit 0
|
||||
:stdout-json-paths {[:status] "ok"
|
||||
[:data :selector] "upsert page"
|
||||
[:data :matched-commands 0] "upsert page"}}
|
||||
:covers {:commands ["example upsert page"]
|
||||
:options {:global ["--output"]}}
|
||||
:tags [:example]}
|
||||
|
||||
{:id "example-upsert-prefix-json"
|
||||
:cmds ["{{cli}} --output json example upsert"]
|
||||
:expect {:exit 0
|
||||
:stdout-json-paths {[:status] "ok"
|
||||
[:data :selector] "upsert"
|
||||
[:data :matched-commands 0] "upsert block"}}
|
||||
:covers {:commands ["example upsert"]
|
||||
:options {:global ["--output"]}}
|
||||
:tags [:example]}
|
||||
|
||||
{:id "example-list-prefix-json"
|
||||
:cmds ["{{cli}} --output json example list"]
|
||||
:expect {:exit 0
|
||||
:stdout-json-paths {[:status] "ok"
|
||||
[:data :selector] "list"
|
||||
[:data :matched-commands 0] "list page"}}
|
||||
:covers {:commands ["example list"]
|
||||
:options {:global ["--output"]}}
|
||||
:tags [:example]}
|
||||
|
||||
{:id "example-list-page-json"
|
||||
:cmds ["{{cli}} --output json example list page"]
|
||||
:expect {:exit 0
|
||||
:stdout-json-paths {[:status] "ok"
|
||||
[:data :selector] "list page"
|
||||
[:data :matched-commands] ["list page"]}}
|
||||
:covers {:commands ["example list page"]
|
||||
:options {:global ["--output"]}}
|
||||
:tags [:example]}
|
||||
|
||||
{:id "example-query-prefix-json"
|
||||
:cmds ["{{cli}} --output json example query"]
|
||||
:expect {:exit 0
|
||||
:stdout-json-paths {[:status] "ok"
|
||||
[:data :selector] "query"
|
||||
[:data :matched-commands 0] "query"}}
|
||||
:covers {:commands ["example query"]
|
||||
:options {:global ["--output"]}}
|
||||
:tags [:example]}
|
||||
|
||||
{:id "example-query-list-json"
|
||||
:cmds ["{{cli}} --output json example query list"]
|
||||
:expect {:exit 0
|
||||
:stdout-json-paths {[:status] "ok"
|
||||
[:data :selector] "query list"
|
||||
[:data :matched-commands] ["query list"]}}
|
||||
:covers {:commands ["example query list"]
|
||||
:options {:global ["--output"]}}
|
||||
:tags [:example]}
|
||||
|
||||
{:id "example-remove-prefix-json"
|
||||
:cmds ["{{cli}} --output json example remove"]
|
||||
:expect {:exit 0
|
||||
:stdout-json-paths {[:status] "ok"
|
||||
[:data :selector] "remove"
|
||||
[:data :matched-commands 0] "remove block"}}
|
||||
:covers {:commands ["example remove"]
|
||||
:options {:global ["--output"]}}
|
||||
:tags [:example]}
|
||||
|
||||
{:id "example-remove-page-json"
|
||||
:cmds ["{{cli}} --output json example remove page"]
|
||||
:expect {:exit 0
|
||||
:stdout-json-paths {[:status] "ok"
|
||||
[:data :selector] "remove page"
|
||||
[:data :matched-commands] ["remove page"]}}
|
||||
:covers {:commands ["example remove page"]
|
||||
:options {:global ["--output"]}}
|
||||
:tags [:example]}
|
||||
|
||||
{:id "example-search-prefix-json"
|
||||
:cmds ["{{cli}} --output json example search"]
|
||||
:expect {:exit 0
|
||||
:stdout-json-paths {[:status] "ok"
|
||||
[:data :selector] "search"
|
||||
[:data :matched-commands 0] "search block"}}
|
||||
:covers {:commands ["example search"]
|
||||
:options {:global ["--output"]}}
|
||||
:tags [:example]}
|
||||
|
||||
{:id "example-search-block-edn"
|
||||
:cmds ["{{cli}} --output edn example search block"]
|
||||
:expect {:exit 0
|
||||
:stdout-edn-paths {[:status] :ok
|
||||
[:data :selector] "search block"
|
||||
[:data :matched-commands] ["search block"]}}
|
||||
:covers {:commands ["example search block"]
|
||||
:options {:global ["--output"]}}
|
||||
:tags [:example]}
|
||||
|
||||
{:id "show-id-human"
|
||||
:setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"
|
||||
"{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null"
|
||||
|
||||
@@ -96,6 +96,20 @@
|
||||
"--level"
|
||||
"stdin:--id"]}
|
||||
|
||||
:example
|
||||
{:commands ["example list"
|
||||
"example list page"
|
||||
"example upsert"
|
||||
"example upsert page"
|
||||
"example remove"
|
||||
"example remove page"
|
||||
"example query"
|
||||
"example query list"
|
||||
"example search"
|
||||
"example search block"
|
||||
"example show"]
|
||||
:options []}
|
||||
|
||||
:server
|
||||
{:commands ["server list"
|
||||
"server status"
|
||||
|
||||
224
docs/agent-guide/070-logseq-cli-example-subcommand.md
Normal file
224
docs/agent-guide/070-logseq-cli-example-subcommand.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# Logseq CLI `example` Subcommand Implementation Plan
|
||||
|
||||
Goal: Add a new `example` command group so users can ask for runnable command examples by command path or command prefix, for example:
|
||||
- `logseq example upsert page`
|
||||
- `logseq example upsert`
|
||||
- `logseq example show`
|
||||
- `logseq example search block`
|
||||
|
||||
Phase 1 scope is limited to **all commands in the current `Graph Inspect and Edit` group**.
|
||||
|
||||
Architecture: Keep this feature CLI-only in phase 1. Reuse command metadata already defined in `logseq-cli` command entries (`:examples`) and do not add any new `db-worker-node` invoke methods.
|
||||
|
||||
Tech Stack: ClojureScript, `babashka.cli` dispatch table, existing `logseq.cli.command.*` entry model, existing formatter/completion/test stack.
|
||||
|
||||
Related:
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/core.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/list.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/remove.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/query.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/search.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/completion_generator.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_worker_node.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/completion_generator_test.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_inventory.edn`
|
||||
- `/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_cases.edn`
|
||||
- `/Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.md`
|
||||
|
||||
## Problem statement
|
||||
|
||||
Today users can see examples only via `--help` on each specific command entry.
|
||||
|
||||
That creates two UX gaps:
|
||||
1. There is no dedicated way to request examples directly by command path.
|
||||
2. There is no single command namespace for future automation/docs workflows that want examples on demand.
|
||||
|
||||
We need `example` as a first-class command group that resolves an existing command path and prints curated example lines.
|
||||
|
||||
## Current baseline
|
||||
|
||||
- Command registration is centralized in `src/main/logseq/cli/commands.cljs` with a single dispatch table built from `command/*` namespaces.
|
||||
- Command help rendering already supports per-entry `:examples` metadata through `logseq.cli.command.core/command-summary`.
|
||||
- `Graph Inspect and Edit` currently includes top-level commands:
|
||||
- `list`
|
||||
- `upsert`
|
||||
- `remove`
|
||||
- `query`
|
||||
- `search`
|
||||
- `show`
|
||||
- `db-worker-node` is an HTTP invoke daemon with no concept of CLI examples; this feature does not require worker protocol changes.
|
||||
|
||||
## Phase 1 target coverage (Graph Inspect and Edit)
|
||||
|
||||
Phase 1 must support `logseq example <target...>` for all of these command paths:
|
||||
|
||||
| Group | Target command path |
|
||||
| --- | --- |
|
||||
| list | `list page`, `list tag`, `list property` |
|
||||
| upsert | `upsert block`, `upsert page`, `upsert tag`, `upsert property` |
|
||||
| remove | `remove block`, `remove page`, `remove tag`, `remove property` |
|
||||
| query | `query`, `query list` |
|
||||
| search | `search block`, `search page`, `search property`, `search tag` |
|
||||
| show | `show` |
|
||||
|
||||
Additionally, phase 1 supports group-level prefix selectors for covered groups (`example list`, `example upsert`, `example remove`, `example query`, `example search`, `example show`).
|
||||
|
||||
## Target UX
|
||||
|
||||
### Main usage
|
||||
|
||||
```text
|
||||
logseq example <command-or-prefix...>
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
logseq example upsert page
|
||||
logseq example upsert
|
||||
logseq example show
|
||||
logseq example search block
|
||||
```
|
||||
|
||||
Selector semantics:
|
||||
- Exact command path: `example upsert page` returns examples only for `upsert page`.
|
||||
- Prefix command path: `example upsert` returns merged examples for all covered `upsert *` subcommands in a stable order.
|
||||
|
||||
### Help behavior
|
||||
|
||||
- `logseq example` shows available phase-1 example selectors.
|
||||
- `logseq example <target...> --help` shows command help for that example selector.
|
||||
- `logseq example upsert --help` and `logseq example upsert page --help` are both valid.
|
||||
|
||||
### Output behavior
|
||||
|
||||
- Human output: clear text block with selected target and example lines.
|
||||
- JSON/EDN output (**required**): include machine-readable fields, at minimum:
|
||||
- `selector` (requested selector, e.g. `"upsert"` or `"upsert page"`)
|
||||
- `matched-commands` (resolved command paths)
|
||||
- `examples` (flattened example lines)
|
||||
- `message` (human-readable summary string)
|
||||
|
||||
## Design
|
||||
|
||||
### 1) Create an `example` command namespace
|
||||
|
||||
Add a new namespace, e.g. `src/main/logseq/cli/command/example.cljs`, responsible for:
|
||||
- declaring phase-1 target selector (Graph Inspect and Edit only)
|
||||
- generating mirrored `example` entries from existing command paths
|
||||
- validating requested target path
|
||||
- building non-worker action payloads
|
||||
- executing as pure local logic
|
||||
|
||||
### 2) Mirror both exact paths and prefix selectors
|
||||
|
||||
Use generated entries like:
|
||||
- exact: `example upsert page`, `example show`, `example search block`
|
||||
- prefix: `example upsert`, `example list`, `example remove`, `example query`, `example search`
|
||||
|
||||
This keeps behavior aligned with existing dispatch/help/completion generation while also supporting grouped output (`example upsert`) without introducing ad-hoc free-form parsing.
|
||||
|
||||
### 3) Reuse `:examples` metadata as source of truth
|
||||
|
||||
For each supported target command entry:
|
||||
- pull `:examples` from the original entry metadata
|
||||
- for exact selectors, return only that entry's examples
|
||||
- for prefix selectors, aggregate examples from all matched covered subcommands
|
||||
|
||||
If any matched target has missing/empty examples, return a clear CLI error (new code such as `:missing-examples`) so metadata coverage remains enforceable.
|
||||
|
||||
### 4) Ensure metadata completeness for phase-1 targets
|
||||
|
||||
Add/normalize `:examples` metadata on phase-1 target entries where missing (notably `query list`).
|
||||
|
||||
### 5) Integrate into central command table
|
||||
|
||||
In `commands.cljs`:
|
||||
- build a `base-table` from existing command namespaces
|
||||
- generate exact-selector and prefix-selector `example` entries from that base table
|
||||
- create final `table = base-table + example entries`
|
||||
|
||||
Then wire command handling for example commands in:
|
||||
- parse/finalize branch (if needed)
|
||||
- `build-action`
|
||||
- `execute`
|
||||
|
||||
No server ensure/invoke is needed for `example` actions.
|
||||
|
||||
### 6) Update top-level help groups
|
||||
|
||||
In `command/core.cljs` top-level summary groups, include `example` under `Utilities` so the command is visible in `logseq --help`.
|
||||
|
||||
### 7) Completion support
|
||||
|
||||
Because example commands are mirrored table entries, existing completion generation should include them automatically once table wiring is updated.
|
||||
|
||||
## Testing plan (TDD)
|
||||
|
||||
1. Add failing parser/help tests in `src/test/logseq/cli/commands_test.cljs`:
|
||||
- `example` group help appears
|
||||
- exact selectors: `example upsert page`, `example show`, `example search block`
|
||||
- prefix selectors: `example upsert`, `example list`, `example query`
|
||||
- unknown/uncovered selector returns expected error
|
||||
|
||||
2. Add focused tests for new namespace (new test file):
|
||||
- entry generation from base table
|
||||
- phase-1 filtering only allows Graph Inspect and Edit targets
|
||||
- missing examples metadata handling
|
||||
|
||||
3. Extend completion tests in `src/test/logseq/cli/completion_generator_test.cljs`:
|
||||
- example group appears
|
||||
- mirrored example command functions are generated
|
||||
|
||||
4. Add format assertions for required structured output:
|
||||
- human output includes selector, matched commands, and example lines
|
||||
- json/edn include `selector`, `matched-commands`, `examples`, and `message`
|
||||
|
||||
5. Extend CLI e2e inventory/spec:
|
||||
- add `example` command scope and selectors
|
||||
- add non-sync cases for representative exact and prefix selectors (`example show`, `example upsert page`, `example upsert`, `example search block`)
|
||||
|
||||
6. Run focused and full checks:
|
||||
- `bb dev:test -v logseq.cli.commands-test`
|
||||
- `bb dev:test -v logseq.cli.completion-generator-test`
|
||||
- `bb -f cli-e2e/bb.edn test --skip-build`
|
||||
- `bb dev:lint-and-test`
|
||||
|
||||
## File-by-file change map
|
||||
|
||||
| File | Change |
|
||||
| --- | --- |
|
||||
| `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/example.cljs` | New namespace: target filtering, mirrored entries, build/execute helpers. |
|
||||
| `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs` | Build base table, append generated example entries, wire build/execute for example actions. |
|
||||
| `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/core.cljs` | Add `example` to top-level help groups (Utilities). |
|
||||
| `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/query.cljs` | Add missing `:examples` metadata for `query list` (if absent). |
|
||||
| `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs` | Add explicit example formatting branch for human and structured (`json`/`edn`) output contract. |
|
||||
| `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` | Add parse/help/build/execute coverage for `example` commands. |
|
||||
| `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/example_test.cljs` | New unit tests for entry generation and target validation. |
|
||||
| `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/completion_generator_test.cljs` | Assert completion output includes example command tree. |
|
||||
| `/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_inventory.edn` | Add `example` command inventory coverage. |
|
||||
| `/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_cases.edn` | Add non-sync runtime cases for representative example commands. |
|
||||
| `/Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.md` | Document `example` command usage and phase-1 coverage. |
|
||||
|
||||
## db-worker-node impact
|
||||
|
||||
No `db-worker-node` API, transport, or thread-api changes are required in phase 1.
|
||||
|
||||
`example` is resolved and rendered entirely in CLI command metadata/action flow.
|
||||
|
||||
## Rollout / extension plan
|
||||
|
||||
- Phase 1 (this plan): only Graph Inspect and Edit coverage.
|
||||
- Phase 2: extend target selector to include Graph Management, Authentication, and Utilities commands.
|
||||
- Keep coverage policy explicit in tests so newly added CLI commands either:
|
||||
- automatically receive `example` coverage, or
|
||||
- are intentionally excluded with a documented reason.
|
||||
|
||||
## Open questions
|
||||
|
||||
1. For commands with sensitive placeholders, should we define a metadata convention to mark examples as redacted/non-runnable?
|
||||
@@ -110,8 +110,11 @@ Auth commands:
|
||||
- `login` - authenticate this machine and create/update `~/logseq/auth.json`
|
||||
- `logout` - remove persisted CLI auth from `~/logseq/auth.json`
|
||||
|
||||
Shell completion:
|
||||
Shell completion and examples:
|
||||
- `completion <zsh|bash>` - generate shell completion script to stdout
|
||||
- `example <command-or-prefix...>` - show runnable command examples for a command path or command prefix (phase 1 covers Graph Inspect and Edit commands)
|
||||
- exact selector example: `logseq example upsert page`
|
||||
- prefix selector example: `logseq example upsert`
|
||||
|
||||
Setup for zsh (add to `~/.zshrc`):
|
||||
```bash
|
||||
@@ -240,6 +243,7 @@ Output formats:
|
||||
- Output formatting is controlled via global `--output`, `:output-format` in config, or `LOGSEQ_CLI_OUTPUT`.
|
||||
- Global `--profile` enables stage timing output to **stderr**. This is for debugging latency and does not change command stdout payloads.
|
||||
- Human output is plain text. List/search commands render tables with a final `Count: N` line. For list and search subcommands, the ID column uses `:db/id` (not UUID). If `:db/ident` exists, an `IDENT` column is included. `list property` includes dedicated `TYPE` and `CARDINALITY` columns. Search table columns are `ID` and `TITLE`. Block titles can include multiple lines; multi-line rows align additional lines under the `TITLE` column. Times such as list `UPDATED-AT`/`CREATED-AT` and `graph info` `Created at` are shown in human-friendly relative form. Errors include error codes and may include a `Hint:` line. Use `--output json|edn` for structured output.
|
||||
- `example` human output includes `Selector`, `Matched commands`, and `Examples` sections. Structured output (`json`/`edn`) includes `selector`, `matched-commands`, `examples`, and `message` fields under `data`.
|
||||
- `sync download` progress lines are streamed to stdout only when progress is enabled. In `json`/`edn` mode, progress is disabled by default unless `--progress true` is provided.
|
||||
- JSON machine output preserves namespaced keyword semantics:
|
||||
- Namespaced keyword keys are emitted as canonical string keys in `namespace/name` form (for example `:block/title` -> `"block/title"`).
|
||||
|
||||
@@ -115,10 +115,28 @@
|
||||
{:title "Authentication"
|
||||
:commands #{"login" "logout"}}
|
||||
{:title "Utilities"
|
||||
:commands #{"completion"}}]
|
||||
render-group (fn [{:keys [title commands]}]
|
||||
(let [entries (filter #(contains? commands (first (:cmds %))) table)]
|
||||
(string/join "\n" [title (format-commands entries)])))]
|
||||
:commands #{"completion" "example"}
|
||||
:top-level-only? true
|
||||
:desc-overrides {"example" "Show command examples"}}]
|
||||
to-top-level-entries (fn [entries commands desc-overrides]
|
||||
(->> commands
|
||||
sort
|
||||
(keep (fn [command]
|
||||
(let [command-entries (filter #(= command (first (:cmds %))) entries)
|
||||
leaf-entry (first (filter #(= 1 (count (:cmds %)))
|
||||
command-entries))
|
||||
desc (or (get desc-overrides command)
|
||||
(:desc leaf-entry)
|
||||
(:desc (first command-entries)))]
|
||||
(when (seq command-entries)
|
||||
{:cmds [command]
|
||||
:desc desc}))))))
|
||||
render-group (fn [{:keys [title commands top-level-only? desc-overrides]}]
|
||||
(let [entries (filter #(contains? commands (first (:cmds %))) table)
|
||||
entries* (if top-level-only?
|
||||
(to-top-level-entries entries commands (or desc-overrides {}))
|
||||
entries)]
|
||||
(string/join "\n" [title (format-commands entries*)])))]
|
||||
(string/join "\n"
|
||||
["Usage: logseq <command> [options]"
|
||||
""
|
||||
|
||||
132
src/main/logseq/cli/command/example.cljs
Normal file
132
src/main/logseq/cli/command/example.cljs
Normal file
@@ -0,0 +1,132 @@
|
||||
(ns logseq.cli.command.example
|
||||
"Example command generation and execution."
|
||||
(:require [clojure.string :as string]
|
||||
[logseq.cli.command.core :as core]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def ^:private phase1-groups
|
||||
["list" "upsert" "remove" "query" "search" "show"])
|
||||
|
||||
(defn- command-path->label
|
||||
[cmds]
|
||||
(string/join " " cmds))
|
||||
|
||||
(defn- normalize-example-lines
|
||||
[examples]
|
||||
(->> (or examples [])
|
||||
(keep (fn [example]
|
||||
(let [line (some-> example str string/trim)]
|
||||
(when (seq line)
|
||||
line))))
|
||||
vec))
|
||||
|
||||
(defn phase1-target-entries
|
||||
[base-table]
|
||||
(->> base-table
|
||||
(filter (fn [entry]
|
||||
(contains? (set phase1-groups)
|
||||
(first (:cmds entry)))))
|
||||
vec))
|
||||
|
||||
(defn- selector-definitions
|
||||
[base-table]
|
||||
(let [targets (phase1-target-entries base-table)
|
||||
by-group (group-by (comp first :cmds) targets)
|
||||
prefix-defs (->> phase1-groups
|
||||
(keep (fn [group]
|
||||
(when-let [matches (seq (get by-group group))]
|
||||
{:selector [group]
|
||||
:matches (vec matches)}))))
|
||||
prefix-selector-set (set (map :selector prefix-defs))
|
||||
exact-defs (->> targets
|
||||
(mapv (fn [entry]
|
||||
{:selector (:cmds entry)
|
||||
:matches [entry]}))
|
||||
(remove (fn [{:keys [selector]}]
|
||||
(contains? prefix-selector-set selector))))]
|
||||
(vec (concat prefix-defs exact-defs))))
|
||||
|
||||
(defn- selector-entry
|
||||
[{:keys [selector matches]}]
|
||||
(let [selector-label (command-path->label selector)
|
||||
matched-labels (mapv (comp command-path->label :cmds) matches)
|
||||
matched-count (count matched-labels)
|
||||
command-cmds (into ["example"] selector)
|
||||
examples (->> matches
|
||||
(mapcat (comp normalize-example-lines :examples))
|
||||
vec)
|
||||
desc (if (> matched-count 1)
|
||||
(str "Show examples for " selector-label " subcommands")
|
||||
(str "Show examples for " selector-label))]
|
||||
(core/command-entry command-cmds :example desc {}
|
||||
{:examples examples
|
||||
:long-desc (str "Show runnable command examples for selector `"
|
||||
selector-label
|
||||
"`." )})))
|
||||
|
||||
(defn build-example-entries
|
||||
[base-table]
|
||||
(->> (selector-definitions base-table)
|
||||
(mapv selector-entry)))
|
||||
|
||||
(defn resolve-selector
|
||||
[base-table selector-cmds]
|
||||
(let [targets (phase1-target-entries base-table)
|
||||
selector-cmds (vec selector-cmds)
|
||||
prefix? (= 1 (count selector-cmds))
|
||||
matches (if prefix?
|
||||
(filterv #(= (first selector-cmds)
|
||||
(first (:cmds %)))
|
||||
targets)
|
||||
(filterv #(= selector-cmds (:cmds %))
|
||||
targets))
|
||||
missing-example-commands (->> matches
|
||||
(filter #(empty? (normalize-example-lines (:examples %))))
|
||||
(mapv (comp command-path->label :cmds)))
|
||||
matched-commands (mapv (comp command-path->label :cmds) matches)
|
||||
examples (->> matches
|
||||
(mapcat (comp normalize-example-lines :examples))
|
||||
vec)
|
||||
selector (command-path->label selector-cmds)]
|
||||
{:selector selector
|
||||
:matched-commands matched-commands
|
||||
:examples examples
|
||||
:missing-example-commands missing-example-commands}))
|
||||
|
||||
(defn build-action
|
||||
[base-table cmds]
|
||||
(let [selector-cmds (vec (rest (or cmds [])))
|
||||
{:keys [selector matched-commands examples missing-example-commands]}
|
||||
(resolve-selector base-table selector-cmds)]
|
||||
(cond
|
||||
(empty? selector-cmds)
|
||||
{:ok? false
|
||||
:error {:code :missing-example-selector
|
||||
:message "example selector is required"}}
|
||||
|
||||
(empty? matched-commands)
|
||||
{:ok? false
|
||||
:error {:code :unknown-command
|
||||
:message (str "unknown example selector: " selector)}}
|
||||
|
||||
(seq missing-example-commands)
|
||||
{:ok? false
|
||||
:error {:code :missing-examples
|
||||
:message (str "missing examples metadata for: "
|
||||
(string/join ", " missing-example-commands))}}
|
||||
|
||||
:else
|
||||
{:ok? true
|
||||
:action {:type :example
|
||||
:selector selector
|
||||
:matched-commands matched-commands
|
||||
:examples examples
|
||||
:message (str "Found "
|
||||
(count examples)
|
||||
" examples for selector "
|
||||
selector)}})))
|
||||
|
||||
(defn execute-example
|
||||
[action _config]
|
||||
(p/resolved {:status :ok
|
||||
:data (select-keys action [:selector :matched-commands :examples :message])}))
|
||||
@@ -20,7 +20,8 @@
|
||||
[(core/command-entry ["query"] :query "Run a Datascript query" query-spec
|
||||
{:examples ["logseq query --graph my-graph --name block-search --inputs '[\"daily\"]'"
|
||||
"logseq query --graph my-graph --query '[:find [?e ...] :where [?e :block/name]]'"]})
|
||||
(core/command-entry ["query" "list"] :query-list "List available queries" query-list-spec)])
|
||||
(core/command-entry ["query" "list"] :query-list "List available queries" query-list-spec
|
||||
{:examples ["logseq query list --graph my-graph"]})])
|
||||
|
||||
(def ^:private built-in-query-specs
|
||||
{"block-search"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
[logseq.cli.command.completion :as completion-command]
|
||||
[logseq.cli.command.core :as command-core]
|
||||
[logseq.cli.command.doctor :as doctor-command]
|
||||
[logseq.cli.command.example :as example-command]
|
||||
[logseq.cli.command.graph :as graph-command]
|
||||
[logseq.cli.command.list :as list-command]
|
||||
[logseq.cli.command.query :as query-command]
|
||||
@@ -105,7 +106,7 @@
|
||||
|
||||
;; Command-specific validation and entries are in subcommand namespaces.
|
||||
|
||||
(def ^:private table
|
||||
(def ^:private base-table
|
||||
(vec (concat graph-command/entries
|
||||
server-command/entries
|
||||
list-command/entries
|
||||
@@ -119,6 +120,10 @@
|
||||
auth-command/entries
|
||||
completion-command/entries)))
|
||||
|
||||
(def ^:private table
|
||||
(vec (concat base-table
|
||||
(example-command/build-example-entries base-table))))
|
||||
|
||||
;; Global option parsing lives in logseq.cli.command.core.
|
||||
|
||||
(defn- index-of
|
||||
@@ -178,7 +183,8 @@
|
||||
cmd-summary (command-core/command-summary {:cmds cmds
|
||||
:spec spec
|
||||
:long-desc long-desc
|
||||
:examples examples})
|
||||
:examples (when (= command :example)
|
||||
examples)})
|
||||
graph (:graph opts)
|
||||
has-args? (seq args)
|
||||
has-content? (or (seq (:content opts))
|
||||
@@ -293,7 +299,8 @@
|
||||
"missing shell argument; usage: logseq completion <zsh|bash>")))
|
||||
|
||||
:else
|
||||
(command-core/ok-result command opts args summary))))
|
||||
(cond-> (command-core/ok-result command opts args summary)
|
||||
(= command :example) (assoc :cmds cmds)))))
|
||||
|
||||
;; CLI error handling is in logseq.cli.command.core.
|
||||
|
||||
@@ -404,7 +411,7 @@
|
||||
[parsed config]
|
||||
(if-not (:ok? parsed)
|
||||
parsed
|
||||
(let [{:keys [command options args]} parsed
|
||||
(let [{:keys [command options args cmds]} parsed
|
||||
graph (command-core/pick-graph options args config)
|
||||
repo (command-core/resolve-repo graph)
|
||||
server-repo (command-core/resolve-repo (:graph options))]
|
||||
@@ -470,6 +477,9 @@
|
||||
:action {:type :completion
|
||||
:shell (or (:shell options) (first args))}}
|
||||
|
||||
:example
|
||||
(example-command/build-action base-table cmds)
|
||||
|
||||
{:ok? false
|
||||
:error {:code :unknown-command
|
||||
:message (str "unknown command: " command)}}))))
|
||||
@@ -519,6 +529,7 @@
|
||||
{:status :ok
|
||||
:data {:message (completion-gen/generate-completions
|
||||
(:shell action) table)}})
|
||||
:example (example-command/execute-example action config)
|
||||
:server-list (server-command/execute-list action config)
|
||||
:server-status (server-command/execute-status action config)
|
||||
:server-start (server-command/execute-start action config)
|
||||
|
||||
@@ -408,6 +408,25 @@
|
||||
(or doc "-")])
|
||||
(or queries []))))
|
||||
|
||||
(defn- format-example
|
||||
[{:keys [selector matched-commands examples message]}]
|
||||
(let [selector (or selector "-")
|
||||
matched-commands (vec (or matched-commands []))
|
||||
examples (vec (or examples []))
|
||||
matched-lines (if (seq matched-commands)
|
||||
(mapv #(str " - " %) matched-commands)
|
||||
[" - (none)"])
|
||||
example-lines (if (seq examples)
|
||||
(mapv #(str " - " %) examples)
|
||||
[" - (none)"])]
|
||||
(string/join "\n"
|
||||
(concat (when (seq message) [message ""])
|
||||
[(str "Selector: " selector)
|
||||
"Matched commands:"]
|
||||
matched-lines
|
||||
["Examples:"]
|
||||
example-lines))))
|
||||
|
||||
(declare kv-key->string
|
||||
graph-info-human-max-string-length
|
||||
graph-info-truncated-suffix)
|
||||
@@ -724,6 +743,7 @@
|
||||
:graph-import (format-graph-import context data)
|
||||
:query (format-query-results (:result data))
|
||||
:query-list (format-query-list (:queries data))
|
||||
:example (format-example data)
|
||||
:show (or (:message data) (pr-str data))
|
||||
:doctor (format-doctor (:status data) (:checks data))
|
||||
(if (and (map? data) (contains? data :message))
|
||||
|
||||
82
src/test/logseq/cli/command/example_test.cljs
Normal file
82
src/test/logseq/cli/command/example_test.cljs
Normal file
@@ -0,0 +1,82 @@
|
||||
(ns logseq.cli.command.example-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[logseq.cli.command.example :as example-command]
|
||||
[logseq.cli.command.graph :as graph-command]
|
||||
[logseq.cli.command.list :as list-command]
|
||||
[logseq.cli.command.query :as query-command]
|
||||
[logseq.cli.command.remove :as remove-command]
|
||||
[logseq.cli.command.search :as search-command]
|
||||
[logseq.cli.command.show :as show-command]
|
||||
[logseq.cli.command.upsert :as upsert-command]))
|
||||
|
||||
(def ^:private phase1-base-table
|
||||
(vec (concat graph-command/entries
|
||||
list-command/entries
|
||||
upsert-command/entries
|
||||
remove-command/entries
|
||||
query-command/entries
|
||||
search-command/entries
|
||||
show-command/entries)))
|
||||
|
||||
(deftest test-phase1-target-filter
|
||||
(let [targets (example-command/phase1-target-entries phase1-base-table)
|
||||
groups (set (map (comp first :cmds) targets))]
|
||||
(testing "phase1 includes inspect/edit groups"
|
||||
(is (contains? groups "list"))
|
||||
(is (contains? groups "upsert"))
|
||||
(is (contains? groups "remove"))
|
||||
(is (contains? groups "query"))
|
||||
(is (contains? groups "search"))
|
||||
(is (contains? groups "show")))
|
||||
|
||||
(testing "phase1 excludes graph management commands"
|
||||
(is (not (contains? groups "graph"))))))
|
||||
|
||||
(deftest test-build-example-entries
|
||||
(let [entries (example-command/build-example-entries phase1-base-table)
|
||||
cmds-set (set (map :cmds entries))]
|
||||
(testing "builds prefix selectors"
|
||||
(is (contains? cmds-set ["example" "upsert"]))
|
||||
(is (contains? cmds-set ["example" "query"]))
|
||||
(is (contains? cmds-set ["example" "show"])))
|
||||
|
||||
(testing "builds exact selectors"
|
||||
(is (contains? cmds-set ["example" "upsert" "page"]))
|
||||
(is (contains? cmds-set ["example" "search" "block"]))
|
||||
(is (contains? cmds-set ["example" "query" "list"])))
|
||||
|
||||
(testing "does not build uncovered selectors"
|
||||
(is (not (contains? cmds-set ["example" "graph"])))))
|
||||
|
||||
(testing "all generated entries use :example command keyword"
|
||||
(is (every? #(= :example (:command %))
|
||||
(example-command/build-example-entries phase1-base-table)))))
|
||||
|
||||
(deftest test-build-action
|
||||
(testing "builds exact selector action"
|
||||
(let [result (example-command/build-action phase1-base-table ["example" "upsert" "page"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :example (get-in result [:action :type])))
|
||||
(is (= "upsert page" (get-in result [:action :selector])))
|
||||
(is (= ["upsert page"] (get-in result [:action :matched-commands])))
|
||||
(is (seq (get-in result [:action :examples])))))
|
||||
|
||||
(testing "builds prefix selector action"
|
||||
(let [result (example-command/build-action phase1-base-table ["example" "upsert"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= "upsert" (get-in result [:action :selector])))
|
||||
(is (<= 2 (count (get-in result [:action :matched-commands]))))))
|
||||
|
||||
(testing "rejects unknown selector"
|
||||
(let [result (example-command/build-action phase1-base-table ["example" "graph"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :unknown-command (get-in result [:error :code])))))
|
||||
|
||||
(testing "rejects matched commands with missing examples metadata"
|
||||
(let [mock-base-table [{:cmds ["upsert" "page"]
|
||||
:examples ["logseq upsert page --graph my-graph --page Home"]}
|
||||
{:cmds ["upsert" "tag"]
|
||||
:examples []}]
|
||||
result (example-command/build-action mock-base-table ["example" "upsert"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :missing-examples (get-in result [:error :code]))))))
|
||||
@@ -3,6 +3,14 @@
|
||||
[clojure.string :as string]
|
||||
[logseq.cli.command.query :as query-command]))
|
||||
|
||||
(deftest test-query-entries-examples
|
||||
(let [query-entry (first (filter #(= :query (:command %)) query-command/entries))
|
||||
query-list-entry (first (filter #(= :query-list (:command %)) query-command/entries))]
|
||||
(testing "query command has examples metadata"
|
||||
(is (seq (:examples query-entry))))
|
||||
(testing "query list command has examples metadata"
|
||||
(is (seq (:examples query-list-entry))))))
|
||||
|
||||
(deftest test-build-action-parses-query
|
||||
(testing "query parses query and inputs"
|
||||
(let [result (query-command/build-action {:query "[:find ?e :in $ ?title :where [?e :block/title ?title]]"
|
||||
|
||||
@@ -78,6 +78,8 @@
|
||||
(is (string/includes? plain-summary "sync"))
|
||||
(is (string/includes? plain-summary "login"))
|
||||
(is (string/includes? plain-summary "logout"))
|
||||
(is (string/includes? plain-summary "example"))
|
||||
(is (not (string/includes? plain-summary "example upsert")))
|
||||
(is (string/includes? plain-summary "Path to db-worker data dir (default ~/logseq/graphs)"))
|
||||
(is (contains-bold? summary "list page"))
|
||||
(is (contains-bold? summary "list tag"))
|
||||
@@ -106,6 +108,8 @@
|
||||
(is (contains-bold? summary "sync start"))
|
||||
(is (contains-bold? summary "login"))
|
||||
(is (contains-bold? summary "logout"))
|
||||
(is (contains-bold? summary "example"))
|
||||
(is (not (contains-bold? summary "example upsert")))
|
||||
(is (contains-bold? summary "--help"))
|
||||
(is (contains-bold? summary "--graph"))
|
||||
(is (re-find #"\u001b\[[0-9;]*mCommands\u001b\[[0-9;]*m:" summary))
|
||||
@@ -204,6 +208,18 @@
|
||||
(is (contains-bold? summary "search property"))
|
||||
(is (contains-bold? summary "search tag"))))
|
||||
|
||||
(testing "example group shows selectors"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["example"]))
|
||||
summary (:summary result)
|
||||
plain-summary (strip-ansi summary)]
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "example upsert"))
|
||||
(is (string/includes? plain-summary "example upsert page"))
|
||||
(is (string/includes? plain-summary "example show"))
|
||||
(is (contains-bold? summary "example upsert"))
|
||||
(is (contains-bold? summary "example show"))))
|
||||
|
||||
(testing "group help command list omits [options]"
|
||||
(let [summary (:summary (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["list"])))
|
||||
@@ -212,7 +228,7 @@
|
||||
(is (every? #(not (string/includes? % "[options]")) lines)))))
|
||||
|
||||
(deftest test-parse-args-help-command-examples
|
||||
(testing "remove block command shows help"
|
||||
(testing "remove block command help no longer shows examples"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["remove" "block" "--help"]))
|
||||
summary (:summary result)
|
||||
@@ -220,22 +236,20 @@
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "Usage: logseq remove block"))
|
||||
(is (string/includes? plain-summary "Command options:"))
|
||||
(is (string/includes? plain-summary "Examples:"))
|
||||
(is (string/includes? plain-summary "logseq remove block --graph my-graph --id 123"))
|
||||
(is (not (string/includes? plain-summary "Examples:")))
|
||||
(is (contains-bold? summary "--id"))
|
||||
(is (contains-bold? summary "--uuid"))))
|
||||
|
||||
(testing "sync config set help limits examples to five lines"
|
||||
(testing "sync config set command help no longer shows examples"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["sync" "config" "set" "--help"]))
|
||||
plain-summary (strip-ansi (:summary result))]
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "Examples:"))
|
||||
(is (string/includes? plain-summary "logseq sync config set ws-url wss://sync.logseq.com"))
|
||||
(is (string/includes? plain-summary "logseq sync config set http-base http://localhost:8080"))
|
||||
(is (not (string/includes? plain-summary "logseq sync config set ws-url wss://example.com/socket")))))
|
||||
(is (not (string/includes? plain-summary "Examples:")))
|
||||
(is (not (string/includes? plain-summary "logseq sync config set ws-url wss://sync.logseq.com")))
|
||||
(is (not (string/includes? plain-summary "logseq sync config set http-base http://localhost:8080")))))
|
||||
|
||||
(testing "upsert block command shows help"
|
||||
(testing "upsert block command help no longer shows examples"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["upsert" "block" "--help"]))
|
||||
summary (:summary result)
|
||||
@@ -243,7 +257,7 @@
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "Usage: logseq upsert block"))
|
||||
(is (string/includes? plain-summary "Command options:"))
|
||||
(is (string/includes? plain-summary "Examples:"))
|
||||
(is (not (string/includes? plain-summary "Examples:")))
|
||||
(is (contains-bold? summary "--id"))
|
||||
(is (contains-bold? summary "--uuid"))
|
||||
(is (contains-bold? summary "--content"))
|
||||
@@ -252,11 +266,20 @@
|
||||
(is (contains-bold? summary "--update-tags"))
|
||||
(is (contains-bold? summary "--update-properties"))
|
||||
(is (contains-bold? summary "--remove-tags"))
|
||||
(is (contains-bold? summary "--remove-properties")))))
|
||||
(is (contains-bold? summary "--remove-properties"))))
|
||||
|
||||
(testing "example command help is the place that shows examples"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["example" "upsert" "--help"]))
|
||||
plain-summary (strip-ansi (:summary result))]
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "Usage: logseq example upsert"))
|
||||
(is (string/includes? plain-summary "Examples:"))
|
||||
(is (string/includes? plain-summary "logseq upsert block --graph my-graph --target-page Home --content \"New block\"")))))
|
||||
|
||||
(deftest test-parse-args-group-help-flags
|
||||
(testing "all groups show group help with -h and --help"
|
||||
(doseq [group ["graph" "server" "list" "upsert" "remove" "query" "search" "sync"]
|
||||
(doseq [group ["graph" "server" "list" "upsert" "remove" "query" "search" "sync" "example"]
|
||||
help-flag ["-h" "--help"]]
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args [group help-flag]))
|
||||
@@ -274,6 +297,28 @@
|
||||
|
||||
)
|
||||
|
||||
(deftest test-parse-args-example-selectors
|
||||
(testing "example supports exact selectors"
|
||||
(doseq [args [["example" "upsert" "page"]
|
||||
["example" "show"]
|
||||
["example" "search" "block"]]]
|
||||
(let [result (commands/parse-args args)]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :example (:command result))))))
|
||||
|
||||
(testing "example supports prefix selectors"
|
||||
(doseq [args [["example" "upsert"]
|
||||
["example" "list"]
|
||||
["example" "query"]]]
|
||||
(let [result (commands/parse-args args)]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :example (:command result))))))
|
||||
|
||||
(testing "example rejects uncovered selectors"
|
||||
(let [result (commands/parse-args ["example" "graph"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :unknown-command (get-in result [:error :code]))))))
|
||||
|
||||
(deftest test-parse-args-help-auth-commands
|
||||
(testing "login command shows help"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
@@ -1602,6 +1647,18 @@
|
||||
(is (= (cli-server/db-worker-dev-script-path)
|
||||
(get-in result [:action :script-path]))))))
|
||||
|
||||
(deftest test-build-action-example
|
||||
(testing "example builds local action"
|
||||
(let [parsed {:ok? true
|
||||
:command :example
|
||||
:cmds ["example" "upsert" "page"]
|
||||
:options {}
|
||||
:args []}
|
||||
result (commands/build-action parsed {})]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :example (get-in result [:action :type])))
|
||||
(is (= "upsert page" (get-in result [:action :selector]))))))
|
||||
|
||||
(deftest test-build-action-inspect-edit-add-upsert
|
||||
(testing "list page requires repo"
|
||||
(let [parsed {:ok? true :command :list-page :options {}}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
[logseq.cli.command.completion :as completion-command]
|
||||
[logseq.cli.command.core :as core]
|
||||
[logseq.cli.command.doctor :as doctor-command]
|
||||
[logseq.cli.command.example :as example-command]
|
||||
[logseq.cli.command.graph :as graph-command]
|
||||
[logseq.cli.command.list :as list-command]
|
||||
[logseq.cli.command.query :as query-command]
|
||||
@@ -14,7 +15,7 @@
|
||||
[logseq.cli.command.upsert :as upsert-command]
|
||||
[logseq.cli.completion-generator :as gen]))
|
||||
|
||||
(def ^:private full-table
|
||||
(def ^:private base-table
|
||||
(vec (concat graph-command/entries
|
||||
server-command/entries
|
||||
list-command/entries
|
||||
@@ -26,6 +27,10 @@
|
||||
doctor-command/entries
|
||||
completion-command/entries)))
|
||||
|
||||
(def ^:private full-table
|
||||
(vec (concat base-table
|
||||
(example-command/build-example-entries base-table))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Phase 1 — Spec enrichment tests
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -149,13 +154,14 @@
|
||||
(testing "show and doctor are leaves"
|
||||
(is (contains? leaf-names "show"))
|
||||
(is (contains? leaf-names "doctor")))
|
||||
(testing "graph, server, list, upsert, remove, search are groups"
|
||||
(testing "graph, server, list, upsert, remove, search, example are groups"
|
||||
(is (contains? group-names "graph"))
|
||||
(is (contains? group-names "server"))
|
||||
(is (contains? group-names "list"))
|
||||
(is (contains? group-names "upsert"))
|
||||
(is (contains? group-names "remove"))
|
||||
(is (contains? group-names "search")))))
|
||||
(is (contains? group-names "search"))
|
||||
(is (contains? group-names "example")))))
|
||||
|
||||
(deftest test-spec->token
|
||||
(testing "boolean spec → :flag type"
|
||||
@@ -204,12 +210,14 @@
|
||||
(is (string/includes? output "_logseq_json_names data items block/title")))
|
||||
(testing "output contains per-command functions"
|
||||
(is (string/includes? output "_logseq_graph_export()"))
|
||||
(is (string/includes? output "_logseq_show()")))
|
||||
(is (string/includes? output "_logseq_show()"))
|
||||
(is (string/includes? output "_logseq_example_upsert_page()")))
|
||||
(testing "output contains group dispatchers"
|
||||
(is (string/includes? output "_logseq_graph()"))
|
||||
(is (string/includes? output "_logseq_list()"))
|
||||
(is (string/includes? output "_logseq_search()"))
|
||||
(is (string/includes? output "_logseq_upsert()")))
|
||||
(is (string/includes? output "_logseq_upsert()"))
|
||||
(is (string/includes? output "_logseq_example()")))
|
||||
(testing "output contains top-level dispatcher"
|
||||
(is (string/includes? output "_logseq()")))
|
||||
(testing "output ends with compdef _logseq logseq"
|
||||
|
||||
@@ -29,6 +29,36 @@
|
||||
:json? true})]
|
||||
(is (= "ok" result)))))
|
||||
|
||||
(deftest test-format-example-output
|
||||
(let [base-result {:status :ok
|
||||
:command :example
|
||||
:data {:selector "upsert"
|
||||
:matched-commands ["upsert block" "upsert page"]
|
||||
:examples ["logseq upsert block --graph my-graph --content \"hello\""
|
||||
"logseq upsert page --graph my-graph --page Home"]
|
||||
:message "Found 2 examples for selector upsert"}}
|
||||
human-result (format/format-result base-result {:output-format nil})
|
||||
json-result (format/format-result base-result {:output-format :json})
|
||||
edn-result (format/format-result base-result {:output-format :edn})
|
||||
parsed-json (js->clj (js/JSON.parse json-result) :keywordize-keys true)
|
||||
parsed-edn (reader/read-string edn-result)]
|
||||
(testing "human output includes selector, matched commands and examples"
|
||||
(is (string/includes? human-result "Selector: upsert"))
|
||||
(is (string/includes? human-result "Matched commands:"))
|
||||
(is (string/includes? human-result "upsert block"))
|
||||
(is (string/includes? human-result "Examples:"))
|
||||
(is (string/includes? human-result "logseq upsert page --graph my-graph --page Home")))
|
||||
|
||||
(testing "json output keeps required structured fields"
|
||||
(is (= "upsert" (get-in parsed-json [:data :selector])))
|
||||
(is (= ["upsert block" "upsert page"] (get-in parsed-json [:data :matched-commands])))
|
||||
(is (= "Found 2 examples for selector upsert" (get-in parsed-json [:data :message]))))
|
||||
|
||||
(testing "edn output keeps required structured fields"
|
||||
(is (= "upsert" (get-in parsed-edn [:data :selector])))
|
||||
(is (= ["upsert block" "upsert page"] (get-in parsed-edn [:data :matched-commands])))
|
||||
(is (= "Found 2 examples for selector upsert" (get-in parsed-edn [:data :message]))))))
|
||||
|
||||
(deftest test-format-error
|
||||
(testing "json error via output-format"
|
||||
(let [result (format/format-result {:status :error :error {:code :boom :message "nope"}}
|
||||
|
||||
Reference in New Issue
Block a user