mirror of
https://github.com/logseq/logseq.git
synced 2026-05-17 09:22:21 +00:00
031-logseq-cli-doctor-command.md
This commit is contained in:
183
docs/agent-guide/031-logseq-cli-doctor-command.md
Normal file
183
docs/agent-guide/031-logseq-cli-doctor-command.md
Normal 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.
|
||||
|
||||
---
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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)])))]
|
||||
|
||||
140
src/main/logseq/cli/command/doctor.cljs
Normal file
140
src/main/logseq/cli/command/doctor.cljs
Normal 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}})))))))
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"})]
|
||||
|
||||
135
src/test/logseq/cli/command/doctor_test.cljs
Normal file
135
src/test/logseq/cli/command/doctor_test.cljs
Normal 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)))))))
|
||||
@@ -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 {}}
|
||||
|
||||
@@ -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]))))))
|
||||
|
||||
Reference in New Issue
Block a user