031-logseq-cli-doctor-command.md

This commit is contained in:
rcmerci
2026-02-07 15:38:27 +08:00
parent a8b118a721
commit 4c10100aa6
10 changed files with 581 additions and 9 deletions

View File

@@ -0,0 +1,183 @@
# Logseq CLI Doctor Command Implementation Plan
Goal: Add a `doctor` command that verifies logseq-cli runtime availability before normal command execution, including `db-worker-node.js` existence and `data-dir` read and write readiness.
Architecture: Add a dedicated `logseq.cli.command.doctor` namespace and wire it into the existing `parse-args` -> `build-action` -> `execute` pipeline in `logseq.cli.commands`.
Architecture: Reuse existing helpers in `logseq.cli.data-dir` and `logseq.cli.server` for permission checks and daemon liveness probes, then return one structured diagnostics report.
Tech Stack: ClojureScript, babashka.cli command table, Node.js `fs` and `path`, Promesa, existing CLI formatter and test harness.
Related: Builds on `docs/agent-guide/019-logseq-cli-data-dir-permissions.md`.
Related: Relates to `docs/agent-guide/015-logseq-cli-db-worker-node-housekeeping.md`.
Related: Relates to `docs/agent-guide/017-logseq-cli-db-worker-node-housekeeping-2.md`.
Related: Relates to `docs/agent-guide/030-logseq-cli-db-graph-default-dir-locking.md`.
## Problem statement
The current CLI fails only when a concrete command touches startup paths, so users discover environment problems late.
We need a fast explicit health command that confirms whether logseq-cli can run reliably in the current machine context.
The minimum required checks are the presence of `db-worker-node.js` and read and write access for `data-dir`.
Note: `dist/db-worker-node.js` is a thin entry wrapper that loads `static/db-worker-node.js`. Doctor should validate the actual runtime target in `static/` rather than only the `dist/` wrapper.
We should also surface practical runtime risks already modeled by current code, especially stale or unready db-worker instances discovered from lock files and health endpoints.
This plan keeps scope to diagnostics and does not change daemon lifecycle semantics, lock protocol, or graph migration behavior.
## Testing Plan
I will follow `@test-driven-development` and write all failing tests before adding implementation behavior.
I will add parser and action-dispatch tests for `doctor` in `commands_test` so command discovery and help output are guarded.
I will add dedicated `doctor` command tests that cover success, missing script file, and `data-dir` permission failure behavior.
I will add `format` tests to ensure human and machine-readable output for `doctor` are stable and useful.
I will run focused test namespaces first to validate RED and GREEN transitions, then run the full lint and test suite.
NOTE: I will write *all* tests before I add any implementation behavior.
## Current behavior map
| Area | Current implementation | Required change |
|---|---|---|
| Runtime script path | `spawn-server!` in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/server.cljs` starts `../dist/db-worker-node.js`, which delegates to `../static/db-worker-node.js`, but no explicit diagnostic command validates that runtime target path readiness. | Add `doctor` check that validates the effective script file existence and readability before startup commands fail. |
| Data-dir readiness | `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/data_dir.cljs` enforces directory creation and read or write access in `ensure-data-dir!`. | Reuse `ensure-data-dir!` inside `doctor` and report a dedicated failing check item. |
| Daemon liveness visibility | `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/server.cljs` has `list-servers`, `server-status`, `ready?`, and `healthy?`, but no consolidated health summary command. | Add optional runtime checks in `doctor` that flag non-ready running servers discovered from lock files. |
| CLI discoverability | Top-level help and command table in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs` and `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/core.cljs` do not include diagnostics entrypoint. | Add `doctor` to command entries and help summaries. |
## Proposed doctor checks
| Check id | Behavior | Existing helper to reuse | Failure signal |
|---|---|---|---|
| `db-worker-script` | Verify `../static/db-worker-node.js` exists and is readable as a file (and optionally verify `../dist/db-worker-node.js` wrapper exists). | New shared path helper in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/server.cljs` plus Node `fs` checks in doctor command. | `:doctor-script-missing` or `:doctor-script-unreadable`. |
| `data-dir` | Verify configured or default data dir can be created and is read and write accessible. | `logseq.cli.data-dir/ensure-data-dir!` in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/data_dir.cljs`. | Existing `:data-dir-permission` surfaced as doctor failure detail. |
| `running-servers` | Verify currently locked db-worker instances are reachable on readiness endpoint. | `logseq.cli.server/list-servers` status derivation in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/server.cljs`. | `:doctor-server-not-ready` for any server reported as `:starting`. |
## Integration sketch
```text
logseq doctor
-> parse-args (commands table)
-> build-action {:type :doctor}
-> execute-doctor
1) check effective db-worker-node.js runtime path (`static/db-worker-node.js`).
2) check data-dir accessibility.
3) inspect running server readiness.
-> format result for human/json/edn.
```
## Implementation plan
### Phase 1: RED for command plumbing.
1. Add failing assertions in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` that top-level help includes `doctor` and bold-styled `doctor` command text.
2. Add a failing parse test in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` for `commands/parse-args ["doctor"]` returning `:ok? true` with command `:doctor`.
3. Add a failing build-action test in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` for `{:command :doctor}` producing action type `:doctor`.
4. Run `bb dev:test -v 'logseq.cli.commands-test'` and confirm failures are specifically on new doctor assertions.
### Phase 2: RED for doctor behavior.
5. Create `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/doctor_test.cljs` with namespace and fixtures consistent with existing command tests.
6. Add a failing test that marks script check as failed when `static/db-worker-node.js` path does not exist.
7. Add a failing test that marks data-dir check as failed when `ensure-data-dir!` throws `:data-dir-permission`.
8. Add a failing test that returns all checks passed when script and data-dir are both valid and no running server is unready.
9. Add a failing test that reports runtime warning or failure when `list-servers` includes entries with status `:starting`.
10. Run `bb dev:test -v 'logseq.cli.command.doctor-test'` and confirm all new tests fail for expected reasons.
### Phase 3: GREEN for command integration.
11. Add `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/doctor.cljs` with `entries`, `build-action`, and `execute-doctor` returning structured check results.
12. Wire doctor namespace into `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs` requires and append `doctor-command/entries` into `table`.
13. Add `:doctor` branch in `build-action` inside `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs`.
14. Add `:doctor` branch in `execute` inside `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs`.
15. Update top-level command grouping in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/core.cljs` to show `doctor` in help output.
16. Run `bb dev:test -v 'logseq.cli.commands-test'` and confirm doctor parse and help tests are green.
### Phase 4: GREEN for doctor checks.
17. Extract or add a shared db-worker script path helper in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/server.cljs` so spawn and doctor share one source of truth.
18. Implement script existence and readability check in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/doctor.cljs` using Node `fs` metadata checks.
19. Implement data-dir check in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/doctor.cljs` by invoking `logseq.cli.data-dir/ensure-data-dir!`.
20. Implement running-server readiness check in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/doctor.cljs` using `logseq.cli.server/list-servers`.
21. Return deterministic check ordering and include actionable message per check in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/doctor.cljs`.
22. Re-run `bb dev:test -v 'logseq.cli.command.doctor-test'` and confirm all doctor behavior tests are green.
### Phase 5: RED and GREEN for formatting and docs.
23. Add failing output tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs` for human summary rendering of doctor checks.
24. Add failing output tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs` for json and edn output preserving structured check payload.
25. Implement doctor-specific human formatter in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs`.
26. Ensure `doctor` output includes overall status and per-check status in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs`.
27. Update `/Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.md` with `doctor` command description, examples, and expected failure hints.
28. Run `bb dev:test -v 'logseq.cli.format-test'` and confirm new doctor formatting tests are green.
### Phase 6: Verify RED to GREEN cycle completion, refactor, and full validation.
29. Run `bb dev:test -v 'logseq.cli.commands-test'` and ensure no regressions in help parsing and action dispatch.
30. Run `bb dev:test -v 'logseq.cli.command.doctor-test'` and ensure all doctor checks are behavior-driven and stable.
31. Run `bb dev:test -v 'logseq.cli.main-test'` to confirm entrypoint behavior remains compatible.
32. Run `bb dev:test -v 'logseq.cli.server-test'` to verify shared script path changes do not break server startup assumptions.
33. Run `bb dev:test -v 'logseq.cli.format-test'` to validate output contracts.
34. Run `bb dev:lint-and-test` and confirm zero failures and zero errors.
35. Review changed code against `@prompts/review.md` before merge.
## Edge cases to cover
| Scenario | Expected behavior |
|---|---|
| `static/db-worker-node.js` path exists but points to a directory. | `doctor` reports script check failure with explicit path and reason. |
| `data-dir` path points to a file. | `doctor` fails with `:data-dir-permission` detail and does not continue to misleading pass status. |
| `data-dir` is readable but not writable. | `doctor` fails data-dir check and returns actionable permission hint. |
| Running server lock exists but `/readyz` is not healthy. | `doctor` reports runtime check as failed for that repo. |
| No running server exists. | Runtime server check passes with empty server list and does not force daemon startup. |
| `--output json` is used. | Doctor returns stable machine-readable check list for scripts and automation. |
## Verification commands and expected outputs
```bash
bb dev:test -v 'logseq.cli.commands-test'
bb dev:test -v 'logseq.cli.command.doctor-test'
bb dev:test -v 'logseq.cli.server-test'
bb dev:test -v 'logseq.cli.format-test'
bb dev:test -v 'logseq.cli.main-test'
bb dev:lint-and-test
```
Each command should finish with zero failures and zero errors in GREEN phase.
Each RED phase run should fail on newly added doctor assertions and not on unrelated setup errors.
## Testing Details
The tests focus on command behavior and diagnostics outcomes through public parser and executor boundaries.
The tests avoid implementation-detail assertions and instead validate user-observable results for success and failure cases.
The formatter tests ensure the same doctor payload is usable for both human troubleshooting and automation output modes.
## Implementation Details
- Keep `doctor` as a first-class command in the existing CLI command table.
- Reuse `ensure-data-dir!` instead of reimplementing permission checks.
- Reuse server health status discovery through existing `list-servers` behavior.
- Keep check execution deterministic and output stable for CI parsing.
- Keep command scope read-only for diagnostics and avoid auto-remediation side effects.
- Return explicit error codes for script and runtime health failures.
- Preserve current graph and repo naming semantics and lock protocol behavior.
- Add targeted formatter support so human output is concise and actionable.
- Verify all changes via focused tests before full lint and test pass.
- Follow `@test-driven-development` and `@prompts/review.md` throughout implementation.
## Question
Resolved: `doctor` will fail fast on the first failed check.
Resolved: `doctor` will treat `:starting` servers as warnings when script and data-dir checks pass.
Resolved: `doctor` will support a future `--repo` scoped deep check that verifies per-graph lock path and repo directory access without starting the daemon.
---

View File

@@ -71,6 +71,7 @@ Server commands:
- `server start --repo <name>` - start db-worker-node for a graph
- `server stop --repo <name>` - stop db-worker-node for a graph
- `server restart --repo <name>` - restart db-worker-node for a graph
- `doctor` - run runtime diagnostics for `db-worker-node.js`, `data-dir` permissions, and running server readiness
Inspect and edit commands:
- `list page [--expand] [--limit <n>] [--offset <n>] [--sort <field>] [--order asc|desc]` - list pages
@@ -120,6 +121,12 @@ Output formats:
- Global `--output <human|json|edn>` applies to all commands
- For `graph export`, `--output` refers to the destination file path. Output formatting is controlled via `:output-format` in config or `LOGSEQ_CLI_OUTPUT`.
- 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. 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.
- `doctor` output includes overall status (`ok`, `warning`, `error`) and per-check rows for `db-worker-script`, `data-dir`, and `running-servers`. For scripting, `--output json|edn` keeps the structured check payload.
- Common doctor failures:
- `doctor-script-missing`: `db-worker-node.js` runtime target is missing (typically `static/db-worker-node.js`; `dist/db-worker-node.js` is only the wrapper entry).
- `doctor-script-unreadable`: script path exists but is not a readable file.
- `data-dir-permission`: configured data dir is not readable or writable.
- `doctor-server-not-ready`: one or more lock-discovered servers are still in `:starting` state (warning).
- `query` human output returns a plain string (the query result rendered via `pr-str`), which is convenient for pipelines like `logseq query ... | xargs logseq show --id`.
- Built-in named queries currently include `block-search`, `task-search`, `recent-updated`, `list-status`, and `list-priority`. Use `query list` to see the full set for your config.
- Show and search outputs resolve block reference UUIDs inside text, replacing `[[<uuid>]]` with the referenced block content. Nested references are resolved recursively up to 10 levels to avoid excessive expansion. For example: `[[<uuid1>]]``[[some text [[<uuid2>]]]]` and then `<uuid2>` is also replaced.
@@ -147,4 +154,6 @@ node ./dist/logseq.js move --uuid <uuid> --target-page TargetPage
node ./dist/logseq.js search "hello"
node ./dist/logseq.js show --page TestPage --output json
node ./dist/logseq.js server list
node ./dist/logseq.js doctor
node ./dist/logseq.js doctor --output json
```

View File

@@ -95,7 +95,7 @@
(let [groups [{:title "Graph Inspect and Edit"
:commands #{"list" "add" "remove" "update" "query" "show"}}
{:title "Graph Management"
:commands #{"graph" "server"}}]
:commands #{"graph" "server" "doctor"}}]
render-group (fn [{:keys [title commands]}]
(let [entries (filter #(contains? commands (first (:cmds %))) table)]
(string/join "\n" [title (format-commands entries)])))]

View File

@@ -0,0 +1,140 @@
(ns logseq.cli.command.doctor
"Doctor command for CLI runtime diagnostics."
(:require ["fs" :as fs]
[clojure.string :as string]
[logseq.cli.command.core :as core]
[logseq.cli.data-dir :as data-dir]
[logseq.cli.server :as cli-server]
[promesa.core :as p]))
(def entries
[(core/command-entry ["doctor"] :doctor "Run runtime diagnostics" {})])
(defn build-action
[]
{:ok? true
:action {:type :doctor}})
(defn- doctor-error
[checks code message]
{:status :error
:error {:code code
:message message
:checks checks}
:data {:status :error
:checks checks}})
(defn- check-db-worker-script
[action]
(let [path (or (:script-path action)
(cli-server/db-worker-runtime-script-path))]
(try
(cond
(not (fs/existsSync path))
{:ok? false
:check {:id :db-worker-script
:status :error
:code :doctor-script-missing
:path path
:message (str "db-worker script is missing: " path)}}
:else
(let [stat (fs/statSync path)]
(if-not (.isFile stat)
{:ok? false
:check {:id :db-worker-script
:status :error
:code :doctor-script-unreadable
:path path
:message (str "db-worker script path is not a file: " path)}}
(let [constants (.-constants fs)]
(fs/accessSync path (.-R_OK constants))
{:ok? true
:check {:id :db-worker-script
:status :ok
:path path
:message (str "Found readable file: " path)}}))))
(catch :default e
{:ok? false
:check {:id :db-worker-script
:status :error
:code :doctor-script-unreadable
:path path
:cause (.-code e)
:message (str "db-worker script is not readable: " path)}}))))
(defn- check-data-dir
[config]
(try
(let [path (data-dir/ensure-data-dir! (:data-dir config))]
{:ok? true
:check {:id :data-dir
:status :ok
:path path
:message (str "Read/write access confirmed: " path)}})
(catch :default e
(let [data (ex-data e)
code (or (:code data) :data-dir-permission)
path (or (:path data) (:data-dir config))
message (or (.-message e)
"data-dir check failed")]
{:ok? false
:check {:id :data-dir
:status :error
:code code
:path path
:cause (:cause data)
:message message}}))))
(defn- check-running-servers
[config]
(-> (p/let [servers (or (cli-server/list-servers config) [])
starting (vec (filter #(= :starting (:status %)) servers))]
(if (seq starting)
{:ok? true
:warning? true
:check {:id :running-servers
:status :warning
:code :doctor-server-not-ready
:servers starting
:message (str (count starting)
" server"
(when (> (count starting) 1) "s")
" still starting: "
(string/join ", " (map :repo starting)))}}
{:ok? true
:warning? false
:check {:id :running-servers
:status :ok
:servers servers
:message (if (seq servers)
"All running servers are ready"
"No running db-worker servers detected")}}))
(p/catch (fn [e]
{:ok? false
:check {:id :running-servers
:status :error
:code :doctor-server-check-failed
:message (or (.-message e)
"running server check failed")}}))))
(defn execute-doctor
[action config]
(p/let [script-check (check-db-worker-script action)]
(if-not (:ok? script-check)
(let [check (:check script-check)]
(doctor-error [check] (:code check) (:message check)))
(let [checks [(:check script-check)]
data-dir-check (check-data-dir config)]
(if-not (:ok? data-dir-check)
(let [check (:check data-dir-check)
checks (conj checks check)]
(doctor-error checks (:code check) (:message check)))
(p/let [server-check (check-running-servers config)
checks (conj checks (:check data-dir-check) (:check server-check))]
(if-not (:ok? server-check)
(let [check (:check server-check)]
(doctor-error checks (:code check) (:message check)))
{:status :ok
:data {:status (if (:warning? server-check) :warning :ok)
:checks checks}})))))))

View File

@@ -4,6 +4,7 @@
[clojure.string :as string]
[logseq.cli.command.add :as add-command]
[logseq.cli.command.core :as command-core]
[logseq.cli.command.doctor :as doctor-command]
[logseq.cli.command.graph :as graph-command]
[logseq.cli.command.list :as list-command]
[logseq.cli.command.query :as query-command]
@@ -101,7 +102,8 @@
remove-command/entries
update-command/entries
query-command/entries
show-command/entries)))
show-command/entries
doctor-command/entries)))
;; Global option parsing lives in logseq.cli.command.core.
@@ -375,6 +377,9 @@
:show
(show-command/build-action options repo)
:doctor
(doctor-command/build-action)
{:ok? false
:error {:code :unknown-command
:message (str "unknown command: " command)}}))))
@@ -410,6 +415,7 @@
:query (query-command/execute-query action config)
:query-list (query-command/execute-query-list action config)
:show (show-command/execute-show action config)
:doctor (doctor-command/execute-doctor 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)

View File

@@ -15,7 +15,7 @@
value))
(defn- ->json
[{:keys [status data error]}]
[{:keys [status data error command]}]
(let [obj (js-obj)]
(set! (.-status obj) (name status))
(cond
@@ -23,7 +23,10 @@
(set! (.-data obj) (clj->js (normalize-json data)))
(= status :error)
(set! (.-error obj) (clj->js (normalize-json (update error :code name)))))
(do
(set! (.-error obj) (clj->js (normalize-json (update error :code name))))
(when (and (= :doctor command) (some? data))
(set! (.-data obj) (clj->js (normalize-json data))))))
(js/JSON.stringify obj)))
(defn- pad-right
@@ -277,6 +280,18 @@
"updated")]
(str "Graph " verb ": " graph)))
(defn- format-doctor
[status checks]
(let [header (str "Doctor: " (name (or status :unknown)))
check-lines (mapv (fn [{:keys [id status message]}]
(str "[" (name (or status :unknown))
"] "
(name (or id :unknown))
(when (seq message)
(str " - " message))))
(or checks []))]
(string/join "\n" (into [header] check-lines))))
(defn- ->human
[{:keys [status data error command context]} {:keys [now-ms]}]
(let [now-ms (or now-ms (js/Date.now))]
@@ -302,20 +317,27 @@
:query (format-query-results (:result data))
:query-list (format-query-list (:queries data))
:show (or (:message data) (pr-str data))
:doctor (format-doctor (:status data) (:checks data))
(if (and (map? data) (contains? data :message))
(:message data)
(pr-str data)))
:error
(format-error error)
(if (= :doctor command)
(format-doctor (or (get-in data [:status]) :error)
(or (get-in data [:checks])
(get-in error [:checks])))
(format-error error))
(pr-str {:status status :data data :error error}))))
(defn- ->edn
[{:keys [status data error]}]
[{:keys [status data error command]}]
(pr-str (cond-> {:status status}
(= status :ok) (assoc :data data)
(= status :error) (assoc :error error))))
(= status :error) (assoc :error error)
(and (= status :error) (= :doctor command) (some? data))
(assoc :data data))))
(defn- normalize-graph-result
[result]

View File

@@ -51,6 +51,14 @@
[data-dir repo]
(node-path/join (repo-dir data-dir repo) "db-worker.lock"))
(defn db-worker-script-path
[]
(node-path/join js/__dirname "../dist/db-worker-node.js"))
(defn db-worker-runtime-script-path
[]
(node-path/join js/__dirname "../static/db-worker-node.js"))
(defn- pid-status
[pid]
(when (number? pid)
@@ -205,7 +213,7 @@
(defn- spawn-server!
[{:keys [repo data-dir]}]
(let [script (node-path/join js/__dirname "../dist/db-worker-node.js")
(let [script (db-worker-script-path)
args #js ["--repo" repo "--data-dir" data-dir]
child (.spawn child-process script args #js {:detached true
:stdio "ignore"})]

View File

@@ -0,0 +1,135 @@
(ns logseq.cli.command.doctor-test
(:require [cljs.test :refer [async deftest is]]
[clojure.string :as string]
[logseq.cli.commands :as commands]
[logseq.cli.data-dir :as data-dir]
[logseq.cli.server :as cli-server]
[promesa.core :as p]))
(deftest test-execute-doctor-script-missing
(async done
(let [orig-ensure-data-dir! data-dir/ensure-data-dir!
orig-list-servers cli-server/list-servers
ensure-data-dir-called? (atom false)
list-servers-called? (atom false)]
(set! data-dir/ensure-data-dir! (fn [_]
(reset! ensure-data-dir-called? true)
"/tmp/logseq-doctor"))
(set! cli-server/list-servers (fn [_]
(reset! list-servers-called? true)
(p/resolved [])))
(-> (p/let [result (commands/execute {:type :doctor
:script-path "/tmp/logseq-cli-missing-db-worker-node.js"}
{})]
(is (= :error (:status result)))
(is (= :doctor-script-missing (get-in result [:error :code])))
(is (= :db-worker-script
(get-in result [:error :checks 0 :id])))
(is (= :error
(get-in result [:error :checks 0 :status])))
(is (false? @ensure-data-dir-called?))
(is (false? @list-servers-called?)))
(p/catch (fn [e]
(is false (str "unexpected error: " e))))
(p/finally (fn []
(set! data-dir/ensure-data-dir! orig-ensure-data-dir!)
(set! cli-server/list-servers orig-list-servers)
(done)))))))
(deftest test-execute-doctor-data-dir-permission
(async done
(let [orig-ensure-data-dir! data-dir/ensure-data-dir!
orig-list-servers cli-server/list-servers
list-servers-called? (atom false)]
(set! data-dir/ensure-data-dir! (fn [_]
(throw (ex-info "data-dir is not readable/writable: /tmp/nope"
{:code :data-dir-permission
:path "/tmp/nope"
:cause "EACCES"}))))
(set! cli-server/list-servers (fn [_]
(reset! list-servers-called? true)
(p/resolved [])))
(-> (p/let [result (commands/execute {:type :doctor
:script-path "src/main/logseq/cli/commands.cljs"}
{:data-dir "/tmp/nope"})]
(is (= :error (:status result)))
(is (= :data-dir-permission (get-in result [:error :code])))
(is (= [:db-worker-script :data-dir]
(mapv :id (get-in result [:error :checks]))))
(is (= [:ok :error]
(mapv :status (get-in result [:error :checks]))))
(is (false? @list-servers-called?)))
(p/catch (fn [e]
(is false (str "unexpected error: " e))))
(p/finally (fn []
(set! data-dir/ensure-data-dir! orig-ensure-data-dir!)
(set! cli-server/list-servers orig-list-servers)
(done)))))))
(deftest test-execute-doctor-all-checks-pass
(async done
(let [orig-ensure-data-dir! data-dir/ensure-data-dir!
orig-list-servers cli-server/list-servers]
(set! data-dir/ensure-data-dir! (fn [_] "/tmp/logseq-doctor"))
(set! cli-server/list-servers (fn [_] (p/resolved [])))
(-> (p/let [result (commands/execute {:type :doctor
:script-path "src/main/logseq/cli/commands.cljs"}
{:data-dir "/tmp/logseq-doctor"})]
(is (= :ok (:status result)))
(is (= :ok (get-in result [:data :status])))
(is (= [:db-worker-script :data-dir :running-servers]
(mapv :id (get-in result [:data :checks]))))
(is (= [:ok :ok :ok]
(mapv :status (get-in result [:data :checks])))))
(p/catch (fn [e]
(is false (str "unexpected error: " e))))
(p/finally (fn []
(set! data-dir/ensure-data-dir! orig-ensure-data-dir!)
(set! cli-server/list-servers orig-list-servers)
(done)))))))
(deftest test-execute-doctor-starting-server-warning
(async done
(let [orig-ensure-data-dir! data-dir/ensure-data-dir!
orig-list-servers cli-server/list-servers]
(set! data-dir/ensure-data-dir! (fn [_] "/tmp/logseq-doctor"))
(set! cli-server/list-servers (fn [_]
(p/resolved [{:repo "logseq_db_demo"
:status :starting
:host "127.0.0.1"
:port 9010}])))
(-> (p/let [result (commands/execute {:type :doctor
:script-path "src/main/logseq/cli/commands.cljs"}
{:data-dir "/tmp/logseq-doctor"})]
(is (= :ok (:status result)))
(is (= :warning (get-in result [:data :status])))
(is (= :running-servers
(get-in result [:data :checks 2 :id])))
(is (= :warning
(get-in result [:data :checks 2 :status])))
(is (= :doctor-server-not-ready
(get-in result [:data :checks 2 :code]))))
(p/catch (fn [e]
(is false (str "unexpected error: " e))))
(p/finally (fn []
(set! data-dir/ensure-data-dir! orig-ensure-data-dir!)
(set! cli-server/list-servers orig-list-servers)
(done)))))))
(deftest test-execute-doctor-default-script-checks-static-runtime-target
(async done
(let [orig-ensure-data-dir! data-dir/ensure-data-dir!
orig-list-servers cli-server/list-servers]
(set! data-dir/ensure-data-dir! (fn [_] "/tmp/logseq-doctor"))
(set! cli-server/list-servers (fn [_] (p/resolved [])))
(-> (p/let [result (commands/execute {:type :doctor}
{:data-dir "/tmp/logseq-doctor"})
checked-path (get-in result [:data :checks 0 :path])]
(is (= :ok (:status result)))
(is (string/ends-with? checked-path "/static/db-worker-node.js")))
(p/catch (fn [e]
(is false (str "unexpected error: " e))))
(p/finally (fn []
(set! data-dir/ensure-data-dir! orig-ensure-data-dir!)
(set! cli-server/list-servers orig-list-servers)
(done)))))))

View File

@@ -64,6 +64,7 @@
(is (string/includes? plain-summary "update"))
(is (string/includes? plain-summary "query"))
(is (string/includes? plain-summary "show"))
(is (string/includes? plain-summary "doctor"))
(is (string/includes? plain-summary "graph"))
(is (string/includes? plain-summary "server"))
(is (string/includes? plain-summary "Path to db-worker data dir (default ~/logseq/graphs)"))
@@ -77,6 +78,7 @@
(is (contains-bold? summary "query"))
(is (contains-bold? summary "query list"))
(is (contains-bold? summary "show"))
(is (contains-bold? summary "doctor"))
(is (contains-bold? summary "graph list"))
(is (contains-bold? summary "graph create"))
(is (contains-bold? summary "server list"))
@@ -284,6 +286,12 @@
(is (true? (:ok? result)))
(is (= "json" (get-in result [:options :output]))))))
(deftest test-parse-args-doctor
(testing "doctor command parses"
(let [result (commands/parse-args ["doctor"])]
(is (true? (:ok? result)))
(is (= :doctor (:command result))))))
(deftest test-tree->text-format
(testing "show tree text uses db/id with tree glyphs"
(let [tree->text #'show-command/tree->text
@@ -1139,6 +1147,13 @@
(is (true? (:ok? result)))
(is (= :server-stop (get-in result [:action :type]))))))
(deftest test-build-action-doctor
(testing "doctor builds action"
(let [parsed {:ok? true :command :doctor :options {}}
result (commands/build-action parsed {})]
(is (true? (:ok? result)))
(is (= :doctor (get-in result [:action :type]))))))
(deftest test-build-action-inspect-edit
(testing "list page requires repo"
(let [parsed {:ok? true :command :list-page :options {}}

View File

@@ -1,5 +1,6 @@
(ns logseq.cli.format-test
(:require [cljs.test :refer [deftest is testing]]
(:require [cljs.reader :as reader]
[cljs.test :refer [deftest is testing]]
[clojure.string :as string]
[logseq.cli.command.show :as show-command]
[logseq.cli.format :as format]
@@ -307,3 +308,56 @@
(is (= (str "Error (missing-graph): graph name is required\n"
"Hint: Use --repo <name>")
result)))))
(deftest test-human-output-doctor
(testing "doctor renders concise check summary"
(let [result (format/format-result {:status :ok
:command :doctor
:data {:status :warning
:checks [{:id :db-worker-script
:status :ok
:message "Found readable file: /tmp/db-worker-node.js"}
{:id :data-dir
:status :ok
:message "Read/write access confirmed: /tmp/logseq/graphs"}
{:id :running-servers
:status :warning
:message "1 server is still starting"}]}}
{:output-format nil})]
(is (= (str "Doctor: warning\n"
"[ok] db-worker-script - Found readable file: /tmp/db-worker-node.js\n"
"[ok] data-dir - Read/write access confirmed: /tmp/logseq/graphs\n"
"[warning] running-servers - 1 server is still starting")
result)))))
(deftest test-doctor-json-edn-output
(testing "doctor json and edn keep structured checks for failed runs"
(let [payload {:checks [{:id :db-worker-script
:status :ok
:message "Found readable file: /tmp/db-worker-node.js"}
{:id :data-dir
:status :error
:code :data-dir-permission
:message "data-dir is not readable/writable: /tmp/logseq"}]}
json-result (format/format-result {:status :error
:command :doctor
:error {:code :data-dir-permission
:message "data-dir check failed"}
:data payload}
{:output-format :json})
edn-result (format/format-result {:status :error
:command :doctor
:error {:code :data-dir-permission
:message "data-dir check failed"}
:data payload}
{:output-format :edn})
parsed-json (js->clj (js/JSON.parse json-result) :keywordize-keys true)
parsed-edn (reader/read-string edn-result)]
(is (= "error" (:status parsed-json)))
(is (= "data-dir-permission" (get-in parsed-json [:error :code])))
(is (= "data-dir" (get-in parsed-json [:data :checks 1 :id])))
(is (= "error" (get-in parsed-json [:data :checks 1 :status])))
(is (= :error (:status parsed-edn)))
(is (= :data-dir-permission (get-in parsed-edn [:error :code])))
(is (= :data-dir (get-in parsed-edn [:data :checks 1 :id])))
(is (= :error (get-in parsed-edn [:data :checks 1 :status]))))))