From 94be3a0bbba10a75b5735cbd2cc54db4fa11e74e Mon Sep 17 00:00:00 2001 From: rcmerci Date: Fri, 10 Apr 2026 23:07:04 +0800 Subject: [PATCH] enhance(cli): remove 'server status'; add 'server cleanup' --- cli-e2e/spec/non_sync_cases.edn | 13 +- cli-e2e/spec/non_sync_inventory.edn | 2 +- .../084-logseq-cli-server-cleanup.md | 217 ++++++++++++++++++ docs/cli/logseq-cli.md | 4 +- skills/logseq-cli-maintenance/SKILL.md | 5 +- src/main/logseq/cli/command/server.cljs | 21 +- src/main/logseq/cli/commands.cljs | 6 +- src/main/logseq/cli/format.cljs | 44 +++- src/main/logseq/cli/server.cljs | 75 ++++-- src/test/logseq/cli/command/server_test.cljs | 27 +++ src/test/logseq/cli/commands_test.cljs | 44 ++-- src/test/logseq/cli/format_test.cljs | 58 +++-- src/test/logseq/cli/server_test.cljs | 74 ++++++ 13 files changed, 503 insertions(+), 87 deletions(-) create mode 100644 docs/agent-guide/084-logseq-cli-server-cleanup.md diff --git a/cli-e2e/spec/non_sync_cases.edn b/cli-e2e/spec/non_sync_cases.edn index 30c0065700..0ac3ea5dc3 100644 --- a/cli-e2e/spec/non_sync_cases.edn +++ b/cli-e2e/spec/non_sync_cases.edn @@ -735,16 +735,17 @@ :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] :tags [:remove]} - {:id "server-status-json" + {:id "server-cleanup-json" :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server status --graph {{graph-arg}}"] + :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server cleanup"] :expect {:exit 0 :stdout-json-paths {[:status] "ok" - [:data :repo] "{{graph}}" - [:data :status] "ready"}} - :covers {:commands ["server status"] + [:data :mismatched] 0 + [:data :eligible] 0 + [:data :skipped-owner] 0}} + :covers {:commands ["server cleanup"] :options {:global ["--config" "--graph" "--data-dir" "--output"] - :server ["--graph"]}} + :server []}} :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] :tags [:server]} diff --git a/cli-e2e/spec/non_sync_inventory.edn b/cli-e2e/spec/non_sync_inventory.edn index b7d22edf4c..29b87b8090 100644 --- a/cli-e2e/spec/non_sync_inventory.edn +++ b/cli-e2e/spec/non_sync_inventory.edn @@ -140,7 +140,7 @@ :server {:commands ["server list" - "server status" + "server cleanup" "server start" "server stop" "server restart"] diff --git a/docs/agent-guide/084-logseq-cli-server-cleanup.md b/docs/agent-guide/084-logseq-cli-server-cleanup.md new file mode 100644 index 0000000000..6968f71435 --- /dev/null +++ b/docs/agent-guide/084-logseq-cli-server-cleanup.md @@ -0,0 +1,217 @@ +# 084 — Replace `server status` with `server cleanup` for revision-mismatch processes + +## Summary + +This plan updates the CLI server command set by removing `server status` and adding `server cleanup`. + +`server cleanup` will terminate discovered `db-worker-node` processes that are both: + +- owned by `:cli` +- revision-mismatched with the current CLI revision + +The implementation will reuse current `logseq-cli` and `db-worker-node` lifecycle primitives: + +- server discovery from lock files (`logseq.cli.server/list-servers`) +- revision metadata persisted in locks by db-worker-node (`:revision`) +- shutdown/kill paths already used by server stop/restart flows + +## Requested behavior changes + +1. Remove `logseq server status --graph `. +2. Add `logseq server cleanup`. +3. `server cleanup` kills only discovered `:cli`-owned server processes where `server revision != current CLI revision`. + +## Current baseline + +- `server` command entries are defined in `src/main/logseq/cli/command/server.cljs` and currently include `list/status/start/stop/restart`. +- CLI parse/build/execute routing is centralized in `src/main/logseq/cli/commands.cljs`. +- Running server discovery already includes revision in `src/main/logseq/cli/server.cljs` (`list-servers` returns `:revision`). +- Revision mismatch semantics already exist and use exact string comparison (`compute-revision-mismatches`). +- db-worker-node writes revision into lock metadata in `src/main/frontend/worker/db_worker_node_lock.cljs`; legacy locks may have missing revision. + +## Product decisions (locked) + +1. **`server status` is removed, not deprecated.** + - No alias. + - Parsing `server status` should fail as unknown command after removal. + +2. **Mismatch rule remains exact string comparison.** + - If `cli-revision != server-revision`, it is a mismatch. + - `nil`/missing server revision is treated as mismatch. + +3. **Cleanup scope is discovered servers in current data-dir.** + - Source of truth is lock-file-backed discovery from `list-servers`. + - We do not add global OS-wide process scanning for all possible data dirs. + +4. **Cleanup ownership scope is `:cli` only.** + - Cleanup targets only servers where `:owner-source` is `:cli`. + - Mismatched servers owned by `:electron` (or other owners) are reported but not terminated. + +5. **Termination strategy is graceful-first for eligible targets.** + - Attempt graceful shutdown first. + - Fallback to process kill if needed. + +## CLI contract for `server cleanup` + +### Command surface + +- New command: `logseq server cleanup` +- No `--graph` required. +- Works with existing global options (`--data-dir`, `--config`, `--output`, etc.). + +### Result shape (proposed) + +Return structured data for stable JSON/EDN automation: + +- `:cli-revision` +- `:checked` (count of discovered servers) +- `:mismatched` (count) +- `:eligible` (count of mismatched servers with `:owner-source :cli`) +- `:skipped-owner` (mismatched servers not owned by `:cli`) +- `:killed` (servers/pids successfully terminated) +- `:failed` (servers/pids that could not be terminated) + +Human output should summarize counts and list failed targets if any. + +## Implementation design + +### 1) Remove `server status` wiring + +- Remove `server status` entry from `command/server.cljs`. +- Remove `:server-status` branches in: + - parse-time graph-required validation set + - build-action dispatch + - execute dispatch +- Remove status-specific formatter branch/tests. + +### 2) Add `server cleanup` command entry and action + +In `command/server.cljs`: + +- Add entry `['server' 'cleanup']` with description and example. +- Add `build-action` branch for `:server-cleanup` (no repo requirement). +- Add executor `execute-cleanup`. + +In `commands.cljs`: + +- Route `:server-cleanup` in build-action and execute. +- Ensure graph-required command set excludes cleanup. + +### 3) Add cleanup orchestration in `logseq.cli.server` + +Introduce a new function (e.g. `cleanup-revision-mismatched-servers!`) that: + +1. resolves CLI revision (`logseq.cli.version/revision`, passed from command layer) +2. calls `list-servers` +3. computes mismatches using existing exact compare helper +4. partitions mismatches into eligible (`:owner-source :cli`) vs skipped-owner targets +5. terminates each eligible mismatched server process +6. removes stale locks when PID no longer exists +7. returns structured result data + +Implementation should reuse existing stop/shutdown logic where possible to avoid duplicate lifecycle behavior. + +### 4) Ownership-aware termination behavior + +Cleanup must not bypass ownership rules globally; it should only target mismatched servers owned by `:cli`. + +Recommended approach: + +- Filter mismatch targets to `:owner-source :cli` before termination. +- Reuse existing stop/shutdown flow for eligible targets. +- Keep existing owner-aware behavior unchanged for `stop`/`restart`. +- Report non-CLI mismatched servers in `:skipped-owner` for visibility. + +### 5) Formatting and docs + +- Add human formatting branch for `:server-cleanup` in `format.cljs`. +- Remove obsolete `:server-status` human formatting and associated tests. +- Update CLI docs and examples in `docs/cli/logseq-cli.md`. + +## File scope (expected) + +- `src/main/logseq/cli/command/server.cljs` +- `src/main/logseq/cli/commands.cljs` +- `src/main/logseq/cli/server.cljs` +- `src/main/logseq/cli/format.cljs` +- `src/main/logseq/cli/command/core.cljs` (if help grouping text needs update) +- `src/test/logseq/cli/commands_test.cljs` +- `src/test/logseq/cli/command/server_test.cljs` +- `src/test/logseq/cli/server_test.cljs` +- `src/test/logseq/cli/format_test.cljs` +- `cli-e2e/spec/non_sync_inventory.edn` +- `cli-e2e/spec/non_sync_cases.edn` +- `docs/cli/logseq-cli.md` + +## TDD execution plan + +1. Add failing parse/build tests for removing `server status` and adding `server cleanup`. +2. Add failing command execute tests for cleanup action wiring. +3. Add failing server lifecycle tests for mismatch selection and termination behavior. +4. Add failing formatter tests for new human cleanup output and remove status output expectations. +5. Implement command and server orchestration changes. +6. Update docs and cli-e2e inventory/cases. +7. Run focused tests, then full lint+test. + +## Testing plan + +### Unit tests + +- `commands_test.cljs` + - `server status` becomes unknown command. + - `server cleanup` parses/builds/dispatches. + - cleanup does not require `--graph`. + +- `command/server_test.cljs` + - cleanup execute returns structured summary. + - mismatch semantics remain exact compare. + +- `server_test.cljs` + - cleanup kills only mismatched revision servers with `:owner-source :cli`. + - mismatched servers owned by non-CLI owners are reported in `:skipped-owner` and remain untouched. + - matching revision servers are untouched. + - missing revision is treated as mismatch. + - failures are reported in `:failed`. + +- `format_test.cljs` + - human cleanup output includes counts and failure lines. + - status formatting tests removed/replaced. + +### CLI E2E + +- Remove `server status` coverage case. +- Add `server cleanup` coverage case (at least smoke path with zero or controlled mismatches). +- Update non-sync inventory command list under `:server`. + +### Verification commands + +```bash +bb dev:test -v logseq.cli.commands-test +bb dev:test -v logseq.cli.command.server-test +bb dev:test -v logseq.cli.server-test +bb dev:test -v logseq.cli.format-test +bb -f cli-e2e/bb.edn test --skip-build +bb dev:lint-and-test +``` + +## Risks and mitigations + +- **Risk:** Ownership metadata may be missing/legacy (`:unknown`) and reduce cleanup coverage. + - **Mitigation:** keep cleanup intentionally strict (`:cli` only) and report skipped-owner targets explicitly for operator follow-up. + +- **Risk:** Legacy locks without revision cause aggressive cleanup. + - **Mitigation:** keep behavior intentional and documented (`missing revision = mismatch`). + +- **Risk:** Duplicate stop logic divergence. + - **Mitigation:** reuse existing stop/shutdown flow for `:cli`-owned targets and keep ownership filtering in one cleanup helper. + +## Acceptance criteria + +1. `logseq server status ...` is no longer available. +2. `logseq server cleanup` exists and executes without `--graph`. +3. Cleanup terminates only discovered mismatch servers with `:owner-source :cli` (`server revision != CLI revision`). +4. Mismatched servers with non-CLI ownership are not terminated and are reported as `:skipped-owner`. +5. Matching-revision servers are not terminated. +6. Structured JSON/EDN output reports `checked/mismatched/eligible/skipped-owner/killed/failed` summaries. +7. Human output is clear and actionable. +8. Unit tests and relevant cli-e2e coverage are updated and passing. diff --git a/docs/cli/logseq-cli.md b/docs/cli/logseq-cli.md index 52a4453990..add7a41d1f 100644 --- a/docs/cli/logseq-cli.md +++ b/docs/cli/logseq-cli.md @@ -113,7 +113,7 @@ Backup scope note: Server commands: - `server list` - list running db-worker-node servers -- `server status --graph ` - show server status for a graph +- `server cleanup` - terminate revision-mismatched CLI-owned db-worker-node servers discovered from lock files in the current data-dir - `server start --graph ` - start db-worker-node for a graph - `server stop --graph ` - stop db-worker-node for a graph - `server restart --graph ` - restart db-worker-node for a graph @@ -169,6 +169,8 @@ Server ownership behavior: - `server start` can return `server-start-timeout-orphan` when lock creation times out and orphan matching processes are detected. - `server list` human output includes both `OWNER` and `REVISION` columns. - `server list` prints a compatibility warning in human output when any server revision string is not exactly equal to the local CLI revision string. +- `server cleanup` checks discovered servers in the current data-dir, treats `revision != local CLI revision` (including missing revision) as mismatch, and attempts graceful-first termination only for `:owner-source :cli` targets. +- `server cleanup` structured output includes `checked`, `mismatched`, `eligible`, `skipped-owner`, `killed`, and `failed` summaries. - Structured output (`--output json|edn`) includes per-server `revision` data but does not include human warning text. Sync commands: diff --git a/skills/logseq-cli-maintenance/SKILL.md b/skills/logseq-cli-maintenance/SKILL.md index d96c0f9b4b..29e4a53556 100644 --- a/skills/logseq-cli-maintenance/SKILL.md +++ b/skills/logseq-cli-maintenance/SKILL.md @@ -93,7 +93,10 @@ If behavior must change, call it out explicitly and add tests. - Especially for error cases and output format. 5. **Run relevant CLI tests and linters** - Confirm no regressions. -6. **Document extension points** +6. **Run relevant cmds in logseq-cli** + - Use logseq-cli skill + - Confirm no regressions. +7. **Document extension points** - Show where future subcommands/options should be added. --- diff --git a/src/main/logseq/cli/command/server.cljs b/src/main/logseq/cli/command/server.cljs index f1a0310fc6..cead8d0820 100644 --- a/src/main/logseq/cli/command/server.cljs +++ b/src/main/logseq/cli/command/server.cljs @@ -7,8 +7,8 @@ (def entries [(core/command-entry ["server" "list"] :server-list "List db-worker-node servers" {}) - (core/command-entry ["server" "status"] :server-status "Show server status for a graph" {} - {:examples ["logseq server status --graph my-graph"]}) + (core/command-entry ["server" "cleanup"] :server-cleanup "Clean up revision-mismatched CLI-owned db-worker-node servers" {} + {:examples ["logseq server cleanup"]}) (core/command-entry ["server" "start"] :server-start "Start db-worker-node for a graph" {} {:examples ["logseq server start --graph my-graph"]}) (core/command-entry ["server" "stop"] :server-stop "Stop db-worker-node for a graph" {} @@ -23,14 +23,9 @@ {:ok? true :action {:type :server-list}} - :server-status - (if-not (seq repo) - {:ok? false - :error {:code :missing-repo - :message "repo is required for server status"}} - {:ok? true - :action {:type :server-status - :repo repo}}) + :server-cleanup + {:ok? true + :action {:type :server-cleanup}} :server-start (if-not (seq repo) @@ -80,9 +75,9 @@ revision-mismatch (assoc :human {:server-list {:revision-mismatch revision-mismatch}}))))) -(defn execute-status - [action config] - (-> (p/let [result (cli-server/server-status config (:repo action))] +(defn execute-cleanup + [_action config] + (-> (p/let [result (cli-server/cleanup-revision-mismatched-servers! config (version/revision))] (server-result->response result)))) (defn execute-start diff --git a/src/main/logseq/cli/commands.cljs b/src/main/logseq/cli/commands.cljs index 5fbc70c223..44603c9591 100644 --- a/src/main/logseq/cli/commands.cljs +++ b/src/main/logseq/cli/commands.cljs @@ -197,7 +197,7 @@ #{:remove-block :remove-page :remove-tag :remove-property}) (def ^:private server-graph-required-commands - #{:server-status :server-start :server-stop :server-restart}) + #{:server-start :server-stop :server-restart}) (def ^:private supported-completion-shells #{"zsh" "bash"}) @@ -593,7 +593,7 @@ import-type (graph-command/normalize-import-export-type (:type options))] (graph-command/build-import-action import-repo import-type (:input options))) - (:server-list :server-status :server-start :server-stop :server-restart) + (:server-list :server-cleanup :server-start :server-stop :server-restart) (server-command/build-action command server-repo) (:list-page :list-tag :list-property :list-task :list-node) @@ -718,7 +718,7 @@ :skill-install (skill-command/execute-skill-install action config) :example (example-command/execute-example action config) :server-list (server-command/execute-list action config) - :server-status (server-command/execute-status action config) + :server-cleanup (server-command/execute-cleanup action config) :server-start (server-command/execute-start action config) :server-stop (server-command/execute-stop action config) :server-restart (server-command/execute-restart action config) diff --git a/src/main/logseq/cli/format.cljs b/src/main/logseq/cli/format.cljs index 898d468629..1833e27a42 100644 --- a/src/main/logseq/cli/format.cljs +++ b/src/main/logseq/cli/format.cljs @@ -586,11 +586,43 @@ ["KV:" " (empty)"])] (string/join "\n" (into summary-lines kv-lines)))) -(defn- format-server-status - [{:keys [repo status host port]}] - (string/join "\n" - (cond-> [(str "Server " (name (or status :unknown)) ": " repo)] - (and host port) (conj (str "Host: " host " Port: " port))))) +(defn- format-server-cleanup-target + [{:keys [repo pid owner-source revision]}] + (str " - " (normalize-cell repo) + " (pid: " (normalize-cell pid) + ", owner: " (normalize-cell owner-source) + ", revision: " (normalize-cell revision) + ")")) + +(defn- format-server-cleanup-failed-target + [{:keys [repo pid owner-source revision error]}] + (str " - " (normalize-cell repo) + " (pid: " (normalize-cell pid) + ", owner: " (normalize-cell owner-source) + ", revision: " (normalize-cell revision) + ", error: " (normalize-cell (or (:code error) :unknown)) + " - " (normalize-cell (:message error)) + ")")) + +(defn- format-server-cleanup + [{:keys [cli-revision checked mismatched eligible skipped-owner skipped-owner-targets killed failed]}] + (let [failed (vec (or failed [])) + skipped-owner-targets (vec (or skipped-owner-targets [])) + header-lines [(str "Server cleanup summary") + (str "CLI revision: " (normalize-cell cli-revision)) + (str "Checked: " (or checked 0)) + (str "Mismatched: " (or mismatched 0)) + (str "Eligible (:cli owner): " (or eligible 0)) + (str "Skipped owner mismatch: " (or skipped-owner 0)) + (str "Killed: " (count (or killed []))) + (str "Failed: " (count failed))] + skipped-lines (when (seq skipped-owner-targets) + (into ["" "Skipped owner targets:"] + (mapv format-server-cleanup-target skipped-owner-targets))) + failed-lines (when (seq failed) + (into ["" "Failed targets:"] + (mapv format-server-cleanup-failed-target failed)))] + (string/join "\n" (concat header-lines skipped-lines failed-lines)))) (defn- format-server-action [command {:keys [repo status host port]}] @@ -880,7 +912,7 @@ (format-graph-action command context) :server-list (format-server-list (:servers data) (get-in human [:server-list :revision-mismatch])) - :server-status (format-server-status data) + :server-cleanup (format-server-cleanup data) (:server-start :server-stop :server-restart) (format-server-action command data) :sync-status (format-sync-status data) diff --git a/src/main/logseq/cli/server.cljs b/src/main/logseq/cli/server.cljs index c8b2e99d29..fc7f937634 100644 --- a/src/main/logseq/cli/server.cljs +++ b/src/main/logseq/cli/server.cljs @@ -271,25 +271,6 @@ (start-server! config repo) stop-result)))) -(defn server-status - [config repo] - (let [data-dir (resolve-data-dir config) - path (lock-path data-dir repo) - lock (read-lock path)] - (if-not lock - (p/resolved {:ok? true - :data {:repo repo - :status :stopped}}) - (p/let [ready (ready? lock)] - {:ok? true - :data {:repo repo - :status (if ready :ready :starting) - :host (:host lock) - :port (:port lock) - :pid (:pid lock) - :owner-source (lock-owner-source lock) - :started-at (:startedAt lock)}})))) - (defn list-servers [config] (let [data-dir (resolve-data-dir config) @@ -324,6 +305,62 @@ {:cli-revision cli-revision :servers mismatch-servers}))) +(defn- cleanup-target + [{:keys [repo pid owner-source revision]}] + {:repo repo + :pid pid + :owner-source owner-source + :revision revision}) + +(defn cleanup-revision-mismatched-servers! + [config cli-revision] + (p/let [servers (list-servers config) + mismatched (->> (or servers []) + (filter (fn [{:keys [revision]}] + (not= cli-revision revision))) + (vec)) + eligible (->> mismatched + (filter (fn [{:keys [owner-source]}] + (= :cli owner-source))) + (vec)) + skipped-owner-targets (->> mismatched + (remove (fn [{:keys [owner-source]}] + (= :cli owner-source))) + (mapv cleanup-target)) + stop-results (p/all + (for [server eligible] + (p/let [target (cleanup-target server) + result (stop-server! (assoc config :owner-source :cli) (:repo server))] + (cond + (:ok? result) + {:status :killed + :target target} + + (= :server-not-found (get-in result [:error :code])) + {:status :killed + :target target} + + :else + {:status :failed + :target target + :error (:error result)})))) + killed (->> stop-results + (filter (fn [{:keys [status]}] (= :killed status))) + (mapv :target)) + failed (->> stop-results + (filter (fn [{:keys [status]}] (= :failed status))) + (mapv (fn [{:keys [target error]}] + (assoc target :error error))))] + {:ok? true + :data {:cli-revision cli-revision + :checked (count (or servers [])) + :mismatched (count mismatched) + :eligible (count eligible) + :skipped-owner (count skipped-owner-targets) + :skipped-owner-targets skipped-owner-targets + :killed killed + :failed failed}})) + (def ^:private legacy-token-pattern #"(?:\+\+|\+3A\+|%)") (def ^:private backup-root-dir-name "backup") diff --git a/src/test/logseq/cli/command/server_test.cljs b/src/test/logseq/cli/command/server_test.cljs index d25914817e..eaeb1b3cf0 100644 --- a/src/test/logseq/cli/command/server_test.cljs +++ b/src/test/logseq/cli/command/server_test.cljs @@ -47,3 +47,30 @@ (p/catch (fn [e] (is false (str "unexpected error: " e)))) (p/finally done)))) + +(deftest build-action-server-cleanup-does-not-require-repo + (let [result (server-command/build-action :server-cleanup nil)] + (is (true? (:ok? result))) + (is (= :server-cleanup (get-in result [:action :type]))))) + +(deftest execute-cleanup-uses-cli-revision-and-server-cleanup + (async done + (-> (p/with-redefs [cli-version/revision (fn [] "cli-rev") + cli-server/cleanup-revision-mismatched-servers! (fn [config revision] + (is (= {:data-dir "/tmp/demo"} config)) + (is (= "cli-rev" revision)) + (p/resolved {:ok? true + :data {:checked 2 + :mismatched 1 + :eligible 1 + :skipped-owner 0 + :killed [{:repo "graph-a" :pid 11}] + :failed []}}))] + (server-command/execute-cleanup {:type :server-cleanup} {:data-dir "/tmp/demo"})) + (p/then (fn [result] + (is (= :ok (:status result))) + (is (= 2 (get-in result [:data :checked]))) + (is (= 1 (get-in result [:data :mismatched]))))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally done)))) diff --git a/src/test/logseq/cli/commands_test.cljs b/src/test/logseq/cli/commands_test.cljs index 7cfd2cce53..4ab5a643e6 100644 --- a/src/test/logseq/cli/commands_test.cljs +++ b/src/test/logseq/cli/commands_test.cljs @@ -5,6 +5,7 @@ [logseq.cli.command.graph :as graph-command] [logseq.cli.command.list :as list-command] [logseq.cli.command.query :as query-command] + [logseq.cli.command.server :as server-command] [logseq.db.frontend.rules :as rules] [logseq.cli.command.show :as show-command] [logseq.cli.command.sync :as sync-command] @@ -2071,17 +2072,20 @@ (is (false? (:ok? result))) (is (= :invalid-options (get-in result [:error :code]))))) - (testing "server status accepts prefix-like repo option values" - (let [result (commands/parse-args ["server" "status" - "--graph" "logseq_db_logseq_db_demo"])] - (is (true? (:ok? result))) - (is (= :server-status (:command result))) - (is (= "logseq_db_logseq_db_demo" (get-in result [:options :graph]))))) + (testing "server status is removed and parses as unknown command" + (let [result (commands/parse-args ["server" "status" "--graph" "demo"])] + (is (false? (:ok? result))) + (is (= :unknown-command (get-in result [:error :code]))))) - (testing "server status accepts global -g alias" - (let [result (commands/parse-args ["server" "status" "-g" "demo"])] + (testing "server cleanup parses without --graph" + (let [result (commands/parse-args ["server" "cleanup"])] (is (true? (:ok? result))) - (is (= :server-status (:command result))) + (is (= :server-cleanup (:command result))))) + + (testing "server cleanup accepts global -g alias but does not require graph" + (let [result (commands/parse-args ["server" "cleanup" "-g" "demo"])] + (is (true? (:ok? result))) + (is (= :server-cleanup (:command result))) (is (= "demo" (get-in result [:options :graph])))))) (deftest test-verb-subcommand-parse-graph-backup @@ -2314,12 +2318,11 @@ (is (true? (:ok? result))) (is (= :server-stop (get-in result [:action :type]))))) - (testing "server status canonicalizes multi-prefixed repo option" - (let [parsed {:ok? true :command :server-status :options {:graph "logseq_db_logseq_db_demo"}} + (testing "server cleanup builds action without requiring repo" + (let [parsed {:ok? true :command :server-cleanup :options {}} result (commands/build-action parsed {})] (is (true? (:ok? result))) - (is (= :server-status (get-in result [:action :type]))) - (is (= "logseq_db_demo" (get-in result [:action :repo])))))) + (is (= :server-cleanup (get-in result [:action :type])))))) (deftest test-build-action-doctor (testing "doctor builds action" @@ -3593,6 +3596,21 @@ (is false (str "unexpected error: " e)))) (p/finally done))))) +(deftest test-execute-server-cleanup-dispatch + (async done + (-> (p/with-redefs [server-command/execute-cleanup (fn [action config] + (is (= :server-cleanup (:type action))) + (is (= {:data-dir "/tmp/demo"} config)) + (p/resolved {:status :ok + :data {:checked 3}}))] + (p/let [result (commands/execute {:type :server-cleanup} {:data-dir "/tmp/demo"})] + (is (= :ok (:status result))) + (is (= :server-cleanup (:command result))) + (is (= 3 (get-in result [:data :checked]))))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally done)))) + (deftest test-execute-requires-existing-graph (async done (-> (p/with-redefs [cli-server/list-graphs (fn [_] []) diff --git a/src/test/logseq/cli/format_test.cljs b/src/test/logseq/cli/format_test.cljs index 908ad9acdb..5279a3c69a 100644 --- a/src/test/logseq/cli/format_test.cljs +++ b/src/test/logseq/cli/format_test.cljs @@ -860,32 +860,42 @@ (is (not (string/includes? json-result token))) (is (not (string/includes? edn-result token)))))) -(deftest test-human-output-server-status - (testing "server status includes repo, status, host, port" +(deftest test-human-output-server-cleanup + (testing "server cleanup includes counts and failed targets" (let [result (format/format-result {:status :ok - :command :server-status - :data {:repo "logseq_db_demo-repo" - :status :ready - :host "127.0.0.1" - :port 1234}} + :command :server-cleanup + :data {:cli-revision "cli-rev" + :checked 4 + :mismatched 3 + :eligible 2 + :skipped-owner 1 + :skipped-owner-targets [{:repo "logseq_db_graph-b" + :pid 22 + :owner-source :electron + :revision "worker-rev-b"}] + :killed [{:repo "logseq_db_graph-a" + :pid 11 + :owner-source :cli + :revision "worker-rev-a"}] + :failed [{:repo "logseq_db_graph-c" + :pid 33 + :owner-source :cli + :revision nil + :error {:code :server-stop-timeout + :message "timed out stopping server"}}]}} {:output-format nil})] - (is (= (str "Server ready: demo-repo\n" - "Host: 127.0.0.1 Port: 1234") - result)))) - - (testing "server status strips only one leading db prefix and keeps middle substrings" - (let [double-prefixed (format/format-result {:status :ok - :command :server-status - :data {:repo "logseq_db_logseq_db_demo" - :status :ready}} - {:output-format nil}) - middle-substring (format/format-result {:status :ok - :command :server-status - :data {:repo "my_logseq_db_notes" - :status :ready}} - {:output-format nil})] - (is (= "Server ready: logseq_db_demo" double-prefixed)) - (is (= "Server ready: my_logseq_db_notes" middle-substring))))) + (is (string/includes? result "Server cleanup summary")) + (is (string/includes? result "CLI revision: cli-rev")) + (is (string/includes? result "Checked: 4")) + (is (string/includes? result "Mismatched: 3")) + (is (string/includes? result "Eligible (:cli owner): 2")) + (is (string/includes? result "Skipped owner mismatch: 1")) + (is (string/includes? result "Killed: 1")) + (is (string/includes? result "Failed: 1")) + (is (string/includes? result "Skipped owner targets:")) + (is (string/includes? result "graph-b")) + (is (string/includes? result "Failed targets:")) + (is (string/includes? result "server-stop-timeout"))))) (deftest test-json-output-normalizes-graph-fields-with-single-leading-strip-only (let [result (format/format-result {:status :ok diff --git a/src/test/logseq/cli/server_test.cljs b/src/test/logseq/cli/server_test.cljs index 6b12a2112b..eab9dd91dd 100644 --- a/src/test/logseq/cli/server_test.cljs +++ b/src/test/logseq/cli/server_test.cljs @@ -329,6 +329,80 @@ (is false (str "unexpected error: " e)))) (p/finally done))))) +(deftest cleanup-revision-mismatched-servers-kills-only-cli-owned-targets + (async done + (let [stop-calls (atom [])] + (-> (p/with-redefs [cli-server/list-servers (fn [_] + (p/resolved [{:repo "logseq_db_a" + :pid 11 + :owner-source :cli + :revision "worker-rev-a"} + {:repo "logseq_db_b" + :pid 22 + :owner-source :electron + :revision "worker-rev-b"} + {:repo "logseq_db_c" + :pid 33 + :owner-source :cli + :revision "cli-rev"} + {:repo "logseq_db_nil" + :pid 44 + :owner-source :cli + :revision nil}])) + cli-server/stop-server! (fn [config repo] + (swap! stop-calls conj {:config config + :repo repo}) + (p/resolved {:ok? true + :data {:repo repo}}))] + (cli-server/cleanup-revision-mismatched-servers! {:data-dir "/tmp/graphs"} "cli-rev")) + (p/then (fn [result] + (is (= true (:ok? result))) + (is (= 4 (get-in result [:data :checked]))) + (is (= 3 (get-in result [:data :mismatched]))) + (is (= 2 (get-in result [:data :eligible]))) + (is (= 1 (get-in result [:data :skipped-owner]))) + (is (= ["logseq_db_a" "logseq_db_nil"] + (mapv :repo (get-in result [:data :killed])))) + (is (empty? (get-in result [:data :failed]))) + (is (= #{"logseq_db_a" "logseq_db_nil"} + (set (map :repo @stop-calls)))) + (is (every? #(= :cli (get-in % [:config :owner-source])) + @stop-calls)))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally done))))) + +(deftest cleanup-revision-mismatched-servers-reports-failures + (async done + (-> (p/with-redefs [cli-server/list-servers (fn [_] + (p/resolved [{:repo "logseq_db_a" + :pid 11 + :owner-source :cli + :revision "worker-rev-a"} + {:repo "logseq_db_b" + :pid 22 + :owner-source :cli + :revision "worker-rev-b"}])) + cli-server/stop-server! (fn [_ repo] + (p/resolved (if (= "logseq_db_a" repo) + {:ok? true + :data {:repo repo}} + {:ok? false + :error {:code :server-stop-timeout + :message "timed out stopping server"}})))] + (cli-server/cleanup-revision-mismatched-servers! {:data-dir "/tmp/graphs"} "cli-rev")) + (p/then (fn [result] + (is (= true (:ok? result))) + (is (= ["logseq_db_a"] + (mapv :repo (get-in result [:data :killed])))) + (is (= ["logseq_db_b"] + (mapv :repo (get-in result [:data :failed])))) + (is (= :server-stop-timeout + (get-in result [:data :failed 0 :error :code]))))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally done)))) + (deftest list-graph-items-ignores-non-graph-directories (let [data-dir (node-helper/create-tmp-dir "cli-list-graphs-ignore") _ (doseq [dir ["alpha"