From 4c10100aa6bdb38cd5d205e86fe608b8fd040f3a Mon Sep 17 00:00:00 2001 From: rcmerci Date: Sat, 7 Feb 2026 15:38:27 +0800 Subject: [PATCH] 031-logseq-cli-doctor-command.md --- .../031-logseq-cli-doctor-command.md | 183 ++++++++++++++++++ docs/cli/logseq-cli.md | 9 + src/main/logseq/cli/command/core.cljs | 2 +- src/main/logseq/cli/command/doctor.cljs | 140 ++++++++++++++ src/main/logseq/cli/commands.cljs | 8 +- src/main/logseq/cli/format.cljs | 32 ++- src/main/logseq/cli/server.cljs | 10 +- src/test/logseq/cli/command/doctor_test.cljs | 135 +++++++++++++ src/test/logseq/cli/commands_test.cljs | 15 ++ src/test/logseq/cli/format_test.cljs | 56 +++++- 10 files changed, 581 insertions(+), 9 deletions(-) create mode 100644 docs/agent-guide/031-logseq-cli-doctor-command.md create mode 100644 src/main/logseq/cli/command/doctor.cljs create mode 100644 src/test/logseq/cli/command/doctor_test.cljs diff --git a/docs/agent-guide/031-logseq-cli-doctor-command.md b/docs/agent-guide/031-logseq-cli-doctor-command.md new file mode 100644 index 0000000000..13cc97b068 --- /dev/null +++ b/docs/agent-guide/031-logseq-cli-doctor-command.md @@ -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. + +--- diff --git a/docs/cli/logseq-cli.md b/docs/cli/logseq-cli.md index c3e3b51039..bb10461a59 100644 --- a/docs/cli/logseq-cli.md +++ b/docs/cli/logseq-cli.md @@ -71,6 +71,7 @@ Server commands: - `server start --repo ` - start db-worker-node for a graph - `server stop --repo ` - stop db-worker-node for a graph - `server restart --repo ` - 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 ] [--offset ] [--sort ] [--order asc|desc]` - list pages @@ -120,6 +121,12 @@ Output formats: - Global `--output ` 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 `[[]]` with the referenced block content. Nested references are resolved recursively up to 10 levels to avoid excessive expansion. For example: `[[]]` → `[[some text [[]]]]` and then `` is also replaced. @@ -147,4 +154,6 @@ node ./dist/logseq.js move --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 ``` diff --git a/src/main/logseq/cli/command/core.cljs b/src/main/logseq/cli/command/core.cljs index fc8f6908e0..94ab0db180 100644 --- a/src/main/logseq/cli/command/core.cljs +++ b/src/main/logseq/cli/command/core.cljs @@ -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)])))] diff --git a/src/main/logseq/cli/command/doctor.cljs b/src/main/logseq/cli/command/doctor.cljs new file mode 100644 index 0000000000..001545c896 --- /dev/null +++ b/src/main/logseq/cli/command/doctor.cljs @@ -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}}))))))) diff --git a/src/main/logseq/cli/commands.cljs b/src/main/logseq/cli/commands.cljs index 9f76fe29d0..cde338c4ec 100644 --- a/src/main/logseq/cli/commands.cljs +++ b/src/main/logseq/cli/commands.cljs @@ -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) diff --git a/src/main/logseq/cli/format.cljs b/src/main/logseq/cli/format.cljs index c03b804204..d5d40642aa 100644 --- a/src/main/logseq/cli/format.cljs +++ b/src/main/logseq/cli/format.cljs @@ -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] diff --git a/src/main/logseq/cli/server.cljs b/src/main/logseq/cli/server.cljs index 1bda79284b..3bea561842 100644 --- a/src/main/logseq/cli/server.cljs +++ b/src/main/logseq/cli/server.cljs @@ -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"})] diff --git a/src/test/logseq/cli/command/doctor_test.cljs b/src/test/logseq/cli/command/doctor_test.cljs new file mode 100644 index 0000000000..b785083125 --- /dev/null +++ b/src/test/logseq/cli/command/doctor_test.cljs @@ -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))))))) diff --git a/src/test/logseq/cli/commands_test.cljs b/src/test/logseq/cli/commands_test.cljs index 3ca8b1267f..7f19013492 100644 --- a/src/test/logseq/cli/commands_test.cljs +++ b/src/test/logseq/cli/commands_test.cljs @@ -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 {}} diff --git a/src/test/logseq/cli/format_test.cljs b/src/test/logseq/cli/format_test.cljs index 429fc24a9c..9da6987cb3 100644 --- a/src/test/logseq/cli/format_test.cljs +++ b/src/test/logseq/cli/format_test.cljs @@ -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 ") 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]))))))