enhance(cli): humanize human-mode output formatting

This commit is contained in:
rcmerci
2026-04-12 21:24:01 +08:00
parent 88e0f91bd9
commit 1db53d14fb
13 changed files with 512 additions and 76 deletions

View File

@@ -23,6 +23,7 @@
cljs-http/cljs-http {:mvn/version "0.1.49"}
org.babashka/sci {:mvn/version "0.12.51"}
org.clj-commons/hickory {:mvn/version "0.7.7"}
org.clj-commons/humanize {:mvn/version "1.2"}
org.babashka/cli {:mvn/version "0.8.67"}
hiccups/hiccups {:mvn/version "0.3.0"}
tongue/tongue {:mvn/version "0.4.4"}

View File

@@ -0,0 +1,209 @@
# 086 — Introduce `org.clj-commons/humanize` for logseq-cli human output
## Goal
Adopt `org.clj-commons/humanize` in `logseq-cli` to improve readability and consistency of **human-mode** output while keeping `json` and `edn` output stable.
This plan is based on the current implementation of:
- CLI output layer: `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs`
- CLI command message producers: `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/*.cljs`
- db-worker-node sync progress producers used by CLI: `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/download.cljs` and `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/upload.cljs`
---
## Scope and constraints
### In scope
1. Add `org.clj-commons/humanize` dependency for CLI/runtime code paths.
2. Replace ad-hoc human text formatting in CLI human output with library-backed helpers where it improves readability.
3. Audit all practical human-output points that can benefit from this library and prioritize them.
4. Keep output changes focused on `--output human` behavior.
### Out of scope
1. Changing `json` or `edn` payload schemas.
2. Any `db-worker-node` code change (including sync progress message producers and thread-api payload changes).
3. Rewriting CLI table layout/alignment logic (`string-width`, truncation, padding).
4. Internationalization (the library is English-first and current CLI output is also English).
### Locked decisions
1. Keep relative time in short style (compatible with current `10s ago` / `1m ago` style).
2. `list asset` `SIZE` displays human-readable size only (no extra raw-byte suffix).
3. Apply grouped-number formatting consistently for `Count:` values.
4. Formatter-only scope: do not modify any `db-worker-node` code.
---
## Why `org.clj-commons/humanize`
Useful functions for current CLI needs:
- `clj-commons.humanize/intcomma` for grouped numbers (`12,345`).
- `clj-commons.humanize/filesize` for byte formatting (`2.0KiB`, `3.0MB`).
- `clj-commons.humanize/relative-datetime` for relative time strings.
- `clj-commons.humanize.inflect/pluralize-noun` for singular/plural grammar.
- `clj-commons.humanize/oxford` for readable list joining in error/help text.
---
## Current baseline and optimization inventory
## A) Central formatter (`format.cljs`) — highest impact
| Area | Current behavior | Candidate improvement with `humanize` | Priority |
|---|---|---|---|
| `human-ago` for `UPDATED-AT`/`CREATED-AT` columns and graph metadata | Custom `s/m/h/d/mo/y ago` calculation with fixed month/year heuristics | Replace with `relative-datetime` brief mode via a wrapper to keep compact style | P0 |
| `format-counted-table` (`Count: N`) used by list/search/query/server/graph tables | Raw integer | Use `intcomma` consistently for all `Count:` values | P0 |
| `list asset` `SIZE` column | Raw bytes integer | Show human-readable size only (`filesize`, e.g. `2.0KiB`) | P0 |
| `format-sync-status` pending counters and tx values | Raw numbers | `intcomma` for counters/tx ids | P1 |
| `format-server-cleanup` summary counters | Raw numbers | `intcomma` + optional noun pluralization cleanup | P1 |
| `format-upsert-block` change counts and `format-remove-block` multi-id count | Raw counts | `intcomma` for counts | P1 |
| Graph-list legacy warning line (`Warning: N legacy graph directories detected.`) | Raw count with hardcoded noun | `intcomma` + `pluralize-noun` | P1 |
## B) Command-level message producers (`command/*.cljs`)
| File | Current behavior | Candidate improvement | Priority |
|---|---|---|---|
| `command/doctor.cljs` (`check-running-servers`, `check-server-revision-mismatch`) | Manual pluralization (`server`/`servers`, `uses`/`use`) | Use `pluralize-noun` (and centralized verb helper if needed) | P0 |
| `command/graph.cljs` (`format-validation-errors`) | Manual `entity/entities` + raw count | `intcomma` + `pluralize-noun` | P0 |
| `command/example.cljs` (`Found N examples ...`) | Raw count | `intcomma` + `pluralize-noun` | P1 |
| `command/show.cljs` (`Linked References (N)`, `Referenced Entities (N)`) | Raw count | `intcomma` for large refs count | P1 |
| `command/sync.cljs` (`missing required sync config ...`) | Comma-joined key list | Optionally use `oxford` for clearer list text | P2 |
## C) Audited but excluded by current scope decision
| File | Current behavior | Candidate improvement | Decision |
|---|---|---|---|
| `frontend/worker/sync/download.cljs` (`Importing data X/Y`) | Raw counters in message string | Could use `intcomma` for `X` and `Y` | Excluded (no db-worker changes) |
| `frontend/worker/sync/upload.cljs` (`Uploading X/Y`) | Raw counters in message string | Could use `intcomma` for `X` and `Y` | Excluded (no db-worker changes) |
## D) Candidate but likely keep as-is for now
| File | Current behavior | Decision |
|---|---|---|
| `src/main/logseq/cli/profile.cljs` | Technical `Nms` tree output for profiling | Keep as technical output; do not humanize in phase 1 |
---
## Proposed design
### 1) Add a CLI-local adapter namespace
Create a small wrapper namespace (e.g. `src/main/logseq/cli/humanize.cljs`) that centralizes:
- `format-count` (uses `intcomma`)
- `pluralize` (uses `pluralize-noun`)
- `format-filesize` (uses `filesize`, configurable binary/decimal)
- `relative-ago` (uses `relative-datetime` with fixed CLI options)
- Optional list helper (`oxford`)
This avoids scattering direct library calls and keeps style decisions local to CLI.
### 2) Preserve machine-output invariants
All changes must keep:
- `format-result` behavior for `:json` and `:edn`
- existing keys in command `:data` and `:error`
- transport payload compatibility
### 3) Strict boundary: CLI formatter only
- Do not modify any `db-worker-node` files.
- Do not change sync event payload structure.
- Limit all behavior changes to CLI human-output formatting paths.
---
## Implementation plan
### Phase 1 — dependency + adapter (foundation)
1. Add dependency in root project deps for CLI compile/runtime usage:
- `/Users/rcmerci/gh-repos/logseq/deps.edn`
2. Add wrapper namespace:
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/humanize.cljs`
3. Add unit tests for wrapper behavior (counts, pluralization, relative time, filesize).
### Phase 2 — migrate high-impact formatter paths
1. Replace `human-ago` implementation in `format.cljs` with wrapper call.
2. Update `format-counted-table` count rendering with grouped numbers.
3. Update list-asset size rendering to human-readable filesize.
4. Update graph-list/server-cleanup/sync-status numeric display paths.
Primary file:
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs`
### Phase 3 — migrate command message producers
1. `command/doctor.cljs`: pluralization cleanup.
2. `command/graph.cljs`: validation summary noun/count formatting.
3. `command/example.cljs`: example count formatting.
4. `command/show.cljs`: referenced count formatting.
Primary files:
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/doctor.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/graph.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/example.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs`
### Phase 4 — tests and docs
1. Update/extend formatter tests for expected human-output changes:
- `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs`
2. Update command tests where message text is asserted:
- `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/doctor_test.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/graph_test.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/show_test.cljs`
- `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/example_test.cljs`
3. Update CLI documentation examples if human-mode samples are shown:
- `/Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.md`
---
## Verification plan
Run targeted tests first:
```bash
bb dev:test -v logseq.cli.format-test
bb dev:test -v logseq.cli.command.doctor-test
bb dev:test -v logseq.cli.command.graph-test
bb dev:test -v logseq.cli.command.show-test
bb dev:test -v logseq.cli.command.example-test
```
Then broader checks:
```bash
bb dev:lint-and-test
bb -f cli-e2e/bb.edn test --skip-build
```
---
## Acceptance criteria
1. `org.clj-commons/humanize` is integrated and used by CLI human-output formatting paths.
2. High-impact areas (relative time, `Count:` rendering with grouped numbers, asset filesize, pluralization in doctor/graph) are migrated.
3. Relative time remains in compact short style compatible with current CLI behavior.
4. `json` and `edn` outputs remain schema-compatible.
5. No `db-worker-node` code changes are introduced.
6. Existing CLI tests are updated and passing.
---
## Risks and mitigations
- **Risk: output churn breaks exact-string tests.**
- Mitigation: migrate in phases and update tests in the same commit; avoid changing machine outputs.
- **Risk: bundle-size increase from adding library + transitive time utilities.**
- Mitigation: route usage through wrapper, prefer only needed functions, and measure `static/logseq-cli.js` size delta.
- **Risk: semantic drift in relative-time wording.**
- Mitigation: pin wrapper options to compact short style and keep compatibility snapshots in `format_test.cljs`.

View File

@@ -4,6 +4,7 @@
[clojure.string :as string]
[logseq.cli.command.core :as core]
[logseq.cli.data-dir :as data-dir]
[logseq.cli.humanize :as cli-humanize]
[logseq.cli.server :as cli-server]
[logseq.cli.version :as version]
[promesa.core :as p]))
@@ -108,9 +109,7 @@
:status :warning
:code :doctor-server-not-ready
:servers starting
:message (str (count starting)
" server"
(when (> (count starting) 1) "s")
:message (str (cli-humanize/format-count-with-noun (count starting) "server")
" still starting: "
(string/join ", " (map :repo starting)))}}
{:ok? true
@@ -147,9 +146,7 @@
:code :doctor-server-revision-mismatch
:cli-revision cli-revision
:servers mismatch-servers
:message (str mismatch-count
" server"
(when (> mismatch-count 1) "s")
:message (str (cli-humanize/format-count-with-noun mismatch-count "server")
" "
(if (= 1 mismatch-count) "uses" "use")
" a different revision than this CLI")}}

View File

@@ -2,6 +2,7 @@
"Example command generation and execution."
(:require [clojure.string :as string]
[logseq.cli.command.core :as core]
[logseq.cli.humanize :as cli-humanize]
[promesa.core :as p]))
(def ^:private phase1-groups
@@ -122,8 +123,10 @@
:matched-commands matched-commands
:examples examples
:message (str "Found "
(count examples)
" examples for selector "
(cli-humanize/format-count (count examples))
" "
(cli-humanize/pluralize-noun (count examples) "example")
" for selector "
selector)}})))
(defn execute-example

View File

@@ -7,6 +7,7 @@
[logseq.cli.command.core :as core]
[logseq.cli.common :as cli-common]
[logseq.cli.config :as cli-config]
[logseq.cli.humanize :as cli-humanize]
[logseq.cli.server :as cli-server]
[logseq.cli.transport :as transport]
[logseq.common.graph :as common-graph]
@@ -421,10 +422,13 @@
(defn- format-validation-errors
[errors]
(str "Graph invalid. Found " (count errors)
(if (= 1 (count errors)) " entity" " entities")
" with errors:\n"
(with-out-str (pprint/pprint errors))))
(let [error-count (count errors)]
(str "Graph invalid. Found "
(cli-humanize/format-count error-count)
" "
(cli-humanize/pluralize-noun error-count "entity")
" with errors:\n"
(with-out-str (pprint/pprint errors)))))
(defn- graph-validate-result
[result]

View File

@@ -6,6 +6,7 @@
[clojure.walk :as walk]
[logseq.cli.command.core :as core]
[logseq.cli.command.id :as id-command]
[logseq.cli.humanize :as cli-humanize]
[logseq.cli.output-mode :as output-mode]
[logseq.cli.server :as cli-server]
[logseq.cli.style :as style]
@@ -855,7 +856,9 @@
(if (seq refs)
(str tree-text
"\n\n"
"Linked References (" count ")\n"
"Linked References ("
(cli-humanize/format-count count)
")\n"
(linked-refs->text refs uuid->label property-titles property-value-labels))
tree-text)))
@@ -870,7 +873,9 @@
[ordered-uuids uuid->entity]
(let [ordered-uuids (vec (distinct (remove string/blank? ordered-uuids)))]
(when (seq ordered-uuids)
(str "Referenced Entities (" (count ordered-uuids) ")\n"
(str "Referenced Entities ("
(cli-humanize/format-count (count ordered-uuids))
")\n"
(string/join "\n" (map #(referenced-entity-row % uuid->entity) ordered-uuids))))))
(defn build-action

View File

@@ -4,6 +4,7 @@
[clojure.string :as string]
[clojure.walk :as walk]
[logseq.cli.command.core :as command-core]
[logseq.cli.humanize :as cli-humanize]
[logseq.cli.output-mode :as output-mode]
[logseq.cli.style :as style]
[logseq.common.util :as common-util]
@@ -162,7 +163,7 @@
(str (render-table headers rows) "\n")
(str (string/join "\n" (map (comp string/trimr first) rows)) "\n"))
"Count: "
(count rows)))
(cli-humanize/format-count (count rows))))
(defn- missing-search-query-hint
[command]
@@ -222,22 +223,20 @@
(defn- human-ago
[value now-ms]
(if-let [ts (parse-ts value)]
(let [diff-ms (max 0 (- now-ms ts))
secs (js/Math.floor (/ diff-ms 1000))
mins (js/Math.floor (/ secs 60))
hours (js/Math.floor (/ mins 60))
days (js/Math.floor (/ hours 24))
months (js/Math.floor (/ days 30))
years (js/Math.floor (/ days 365))]
(cond
(< secs 60) (str secs "s ago")
(< mins 60) (str mins "m ago")
(< hours 24) (str hours "h ago")
(< days 30) (str days "d ago")
(< months 12) (str months "mo ago")
:else (str years "y ago")))
(cli-humanize/relative-ago ts now-ms)
"-"))
(defn- format-task-datetime
[value now-ms]
(if-let [ts (parse-ts value)]
(cli-humanize/relative-datetime ts now-ms)
(if (string? value)
(let [text (string/trim value)]
(if (seq text)
text
"-"))
"-")))
(defn- items-have-key?
[items & ks]
(some (fn [item] (some #(contains? item %) ks)) items))
@@ -349,9 +348,9 @@
[:status :logseq.property/status]]
["PRIORITY" (fn [item _] (format-task-choice (or (:priority item) (:logseq.property/priority item)) "priority."))
[:priority :logseq.property/priority]]
["SCHEDULED" (fn [item _] (or (:scheduled item) (:logseq.property/scheduled item) "-"))
["SCHEDULED" (fn [item now-ms] (format-task-datetime (or (:scheduled item) (:logseq.property/scheduled item)) now-ms))
[:scheduled :logseq.property/scheduled]]
["DEADLINE" (fn [item _] (or (:deadline item) (:logseq.property/deadline item) "-"))
["DEADLINE" (fn [item now-ms] (format-task-datetime (or (:deadline item) (:logseq.property/deadline item)) now-ms))
[:deadline :logseq.property/deadline]]
["UPDATED-AT" (fn [item now-ms] (human-ago (or (:updated-at item) (:block/updated-at item)) now-ms)) [:updated-at :block/updated-at]]
["CREATED-AT" (fn [item now-ms] (human-ago (or (:created-at item) (:block/created-at item)) now-ms)) [:created-at :block/created-at]]])
@@ -395,7 +394,7 @@
[["ID" (fn [item _] (or (:db/id item) (:id item))) [:db/id :id]]
["TITLE" (fn [item _] (or (:title item) (:block/title item) (:name item))) [:title :block/title :name]]
["ASSET-TYPE" (fn [item _] (normalize-asset-type (:logseq.property.asset/type item))) [:logseq.property.asset/type]]
["SIZE" (fn [item _] (or (:logseq.property.asset/size item) "-")) [:logseq.property.asset/size]]
["SIZE" (fn [item _] (cli-humanize/format-filesize (:logseq.property.asset/size item))) [:logseq.property.asset/size]]
["UPDATED-AT" (fn [item now-ms] (human-ago (or (:updated-at item) (:block/updated-at item)) now-ms)) [:updated-at :block/updated-at]]
["CREATED-AT" (fn [item now-ms] (human-ago (or (:created-at item) (:block/created-at item)) now-ms)) [:created-at :block/created-at]]])
@@ -496,7 +495,12 @@
base-output (format-counted-table nil rows)
legacy-items (filterv legacy-graph-item? graph-items)]
(if (seq legacy-items)
(let [warning-lines (vec (concat [(str "Warning: " (count legacy-items) " legacy graph directories detected.")]
(let [legacy-count (count legacy-items)
warning-lines (vec (concat [(str "Warning: "
(cli-humanize/format-count legacy-count)
" legacy graph "
(cli-humanize/pluralize-noun legacy-count "directory")
" detected.")]
(mapcat #(format-legacy-warning-lines % data-dir) legacy-items)))]
(str base-output "\n\n" (string/join "\n" warning-lines)))
base-output)))
@@ -631,12 +635,12 @@
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))]
(str "Checked: " (cli-humanize/format-count (or checked 0)))
(str "Mismatched: " (cli-humanize/format-count (or mismatched 0)))
(str "Eligible (:cli owner): " (cli-humanize/format-count (or eligible 0)))
(str "Skipped owner mismatch: " (cli-humanize/format-count (or skipped-owner 0)))
(str "Killed: " (cli-humanize/format-count (count (or killed []))))
(str "Failed: " (cli-humanize/format-count (count failed)))]
skipped-lines (when (seq skipped-owner-targets)
(into ["" "Skipped owner targets:"]
(mapv format-server-cleanup-target skipped-owner-targets)))
@@ -721,11 +725,15 @@
(str "repo: " (or repo "-"))
(str "graph-id: " (or graph-id "-"))
(str "ws-state: " (or ws-state :unknown))
(str "pending-local: " (or pending-local 0))
(str "pending-asset: " (or pending-asset 0))
(str "pending-server: " (or pending-server 0))
(str "local-tx: " (or local-tx "-"))
(str "remote-tx: " (or remote-tx "-"))]
(str "pending-local: " (cli-humanize/format-count (or pending-local 0)))
(str "pending-asset: " (cli-humanize/format-count (or pending-asset 0)))
(str "pending-server: " (cli-humanize/format-count (or pending-server 0)))
(str "local-tx: " (if (number? local-tx)
(cli-humanize/format-count local-tx)
"-"))
(str "remote-tx: " (if (number? remote-tx)
(cli-humanize/format-count remote-tx)
"-"))]
last-error-line (conj last-error-line)))))
(defn- format-sync-remote-graphs
@@ -794,10 +802,10 @@
(if (vector? result)
(str "Upserted blocks:\n" (pr-str (vec (or result []))))
(let [change-parts (cond-> []
(seq update-tags) (conj (str "tags:+" (count update-tags)))
(seq update-properties) (conj (str "properties:+" (count update-properties)))
(seq remove-tags) (conj (str "remove-tags:+" (count remove-tags)))
(seq remove-properties) (conj (str "remove-properties:+" (count remove-properties))))
(seq update-tags) (conj (str "tags:+" (cli-humanize/format-count (count update-tags))))
(seq update-properties) (conj (str "properties:+" (cli-humanize/format-count (count update-properties))))
(seq remove-tags) (conj (str "remove-tags:+" (cli-humanize/format-count (count remove-tags))))
(seq remove-properties) (conj (str "remove-properties:+" (cli-humanize/format-count (count remove-properties)))))
changes (when (seq change-parts)
(str ", " (string/join ", " change-parts)))
move-fragment (when (seq target)
@@ -828,7 +836,7 @@
[{:keys [repo uuid id ids]}]
(cond
(seq uuid) (str "Removed block: " uuid " (repo: " repo ")")
(seq ids) (str "Removed blocks: " (count ids) " (repo: " repo ")")
(seq ids) (str "Removed blocks: " (cli-humanize/format-count (count ids)) " (repo: " repo ")")
(some? id) (str "Removed block: " id " (repo: " repo ")")
:else (str "Removed block (repo: " repo ")")))

View File

@@ -0,0 +1,82 @@
(ns logseq.cli.humanize
"CLI-local wrappers around clj-commons humanize helpers."
(:require [clj-commons.humanize :as humanize]
[clj-commons.humanize.inflect :as humanize-inflect]
[clojure.string :as string]))
(def ^:private relative-unit->abbr
{"second" "s"
"minute" "m"
"hour" "h"
"day" "d"
"week" "w"
"month" "mo"
"year" "y"})
(defn format-count
[value]
(let [n (if (number? value) value 0)]
(humanize/intcomma (js/Math.floor n))))
(defn pluralize-noun
[count noun]
(humanize-inflect/pluralize-noun (or count 0) noun))
(defn format-count-with-noun
[count noun]
(str (format-count count)
" "
(pluralize-noun count noun)))
(defn format-filesize
[byte-count]
(if (number? byte-count)
(humanize/filesize byte-count :binary true :format "%.1f")
"-"))
(defn- unit->abbr
[unit]
(let [unit* (-> unit string/lower-case (string/replace #"s$" ""))]
(get relative-unit->abbr unit* unit*)))
(defn- abbreviate-relative-datetime
[result]
(let [result (string/trim (or result ""))]
(or (when-let [[_ n unit] (re-matches #"^(\d+)\s+([a-zA-Z]+)\s+ago$" result)]
(str n (unit->abbr unit) " ago"))
(when-let [[_ n unit] (re-matches #"^in\s+(\d+)\s+([a-zA-Z]+)$" result)]
(str "in " n (unit->abbr unit)))
(when (= "in 0s" result)
result)
result)))
(defn relative-datetime
[then-ms now-ms]
(cond
(not (number? then-ms)) "-"
(not (number? now-ms)) "-"
(<= then-ms 0) "-"
:else
(-> (humanize/relative-datetime (js/Date. then-ms)
:now-dt (js/Date. now-ms)
:max-terms 1
:number-format str
:short-text "0s"
:prefix "in"
:suffix "ago")
abbreviate-relative-datetime)))
(defn relative-ago
[then-ms now-ms]
(cond
(not (number? then-ms)) "-"
(not (number? now-ms)) "-"
(<= then-ms 0) "-"
(>= then-ms now-ms) "0s ago"
:else
(let [result (relative-datetime then-ms now-ms)]
(if (string/starts-with? result "in ")
"0s ago"
result))))

View File

@@ -174,4 +174,32 @@
(is (string/ends-with? checked-path "/dist/db-worker-node.js"))))
(p/catch (fn [e]
(is false (str "unexpected error: " e))))
(p/finally done))))
(p/finally done))))
(deftest test-check-running-servers-formats-large-count
(async done
(let [servers (mapv (fn [idx]
{:repo (str "logseq_db_graph-" idx)
:status :starting})
(range 1234))]
(-> (p/with-redefs [cli-server/list-servers (fn [_] (p/resolved servers))]
(p/let [result (#'doctor-command/check-running-servers {})
message (get-in result [:check :message])]
(is (= true (:warning? result)))
(is (string/includes? message "1,234 servers still starting:"))))
(p/catch (fn [e]
(is false (str "unexpected error: " e))))
(p/finally done)))))
(deftest test-check-server-revision-mismatch-formats-large-count
(let [mismatch-servers (mapv (fn [idx]
{:repo (str "logseq_db_graph-" idx)
:revision "worker-rev"})
(range 1234))
result (with-redefs [cli-server/compute-revision-mismatches
(fn [_ _]
{:servers mismatch-servers})]
(#'doctor-command/check-server-revision-mismatch "cli-rev" []))]
(is (= true (:warning? result)))
(is (string/includes? (get-in result [:check :message])
"1,234 servers use a different revision than this CLI"))))

View File

@@ -80,3 +80,13 @@
result (example-command/build-action mock-base-table ["example" "upsert"])]
(is (false? (:ok? result)))
(is (= :missing-examples (get-in result [:error :code]))))))
(deftest test-build-action-formats-large-example-count
(let [mock-base-table [{:cmds ["upsert" "page"]
:examples (mapv (fn [idx]
(str "logseq upsert page --graph demo --page Page-" idx))
(range 1234))}]
result (example-command/build-action mock-base-table ["example" "upsert" "page"])]
(is (true? (:ok? result)))
(is (= "Found 1,234 examples for selector upsert page"
(get-in result [:action :message])))))

View File

@@ -22,6 +22,17 @@
"Found 1 entity with errors:"))
(is (= :ok (:status valid-result)))))
(deftest test-graph-validate-result-formats-large-error-count
(let [graph-validate-result #'graph-command/graph-validate-result
errors (mapv (fn [idx]
{:entity {:db/id idx}
:errors {:foo ["bad"]}})
(range 1234))
invalid-result (graph-validate-result {:errors errors})]
(is (= :error (:status invalid-result)))
(is (string/includes? (get-in invalid-result [:error :message])
"Found 1,234 entities with errors:"))))
(deftest test-execute-graph-info-queries-kv-rows-with-thread-api-q
(async done
(let [invoke-calls* (atom [])

View File

@@ -236,6 +236,16 @@
{(string/lower-case u1) {:label "Broken ref"}
(string/lower-case u2) {:id 88}}))))))
(deftest test-render-referenced-entities-footer-formats-large-count
(let [render-footer (fn [ordered-uuids uuid->entity]
(call-private 'render-referenced-entities-footer ordered-uuids uuid->entity))
ordered-uuids (mapv (fn [idx]
(str "uuid-" idx))
(range 1234))
uuid->entity {}
output (render-footer ordered-uuids uuid->entity)]
(is (string/includes? output "Referenced Entities (1,234)"))))
(deftest test-build-action-ref-id-footer
(testing "ref-id-footer defaults to true"
(let [result (show-command/build-action {:id "42"}

View File

@@ -177,7 +177,30 @@
:conflict? false}]}}
{:output-format nil
:data-dir "/tmp/graphs"})]
(is (string/includes? result "mv '/tmp/graphs/weird'\"'\"'++name' '/tmp/graphs/weird~27~2Fname'")))))
(is (string/includes? result "mv '/tmp/graphs/weird'\"'\"'++name' '/tmp/graphs/weird~27~2Fname'"))))
)
(deftest test-human-output-graph-list-count-grouping
(let [graphs (mapv #(str "graph-" %) (range 1234))
result (format/format-result {:status :ok
:command :graph-list
:data {:graphs graphs}}
{:output-format nil})]
(is (string/includes? result "Count: 1,234"))))
(deftest test-human-output-graph-list-legacy-warning-singular
(let [result (format/format-result {:status :ok
:command :graph-list
:data {:graphs ["legacy/name"]
:graph-items [{:kind :legacy
:legacy-dir "legacy++name"
:legacy-graph-name "legacy/name"
:target-graph-dir "legacy~2Fname"
:conflict? false}]}}
{:output-format nil
:data-dir "/tmp/graphs"})]
(is (string/includes? result "Warning: 1 legacy graph directory detected."))))
(deftest test-human-output-list-page
(testing "list page renders a table with count"
@@ -265,7 +288,26 @@
(is (string/includes? result "SCHEDULED"))
(is (string/includes? result "DEADLINE"))
(is (string/includes? result "Alpha task"))
(is (string/includes? result "Count: 1"))))
(is (string/includes? result "Count: 1")))
(testing "list task renders epoch-ms scheduled/deadline with humanized relative datetime"
(let [now-ms 1000000000
scheduled-ms (+ now-ms (* 3 60 60 1000))
deadline-ms (- now-ms (* 2 24 60 60 1000))
result (format/format-result {:status :ok
:command :list-task
:data {:items [{:db/id 226
:block/title "q3"
:logseq.property/status :logseq.property/status.todo
:logseq.property/priority :logseq.property/priority.high
:logseq.property/scheduled scheduled-ms
:logseq.property/deadline deadline-ms}]}}
{:output-format nil
:now-ms now-ms})]
(is (string/includes? result "in "))
(is (string/includes? result "ago"))
(is (not (string/includes? result (str scheduled-ms))))
(is (not (string/includes? result (str deadline-ms)))))))
(deftest test-human-output-list-node
(let [result (format/format-result {:status :ok
@@ -306,11 +348,18 @@
:block/updated-at 90000}]}}
{:output-format nil
:now-ms 100000})
lines (string/split-lines result)]
(is (= "ID TITLE ASSET-TYPE SIZE UPDATED-AT CREATED-AT" (first lines)))
lines (string/split-lines result)
header (first lines)]
(is (string/includes? header "ID"))
(is (string/includes? header "TITLE"))
(is (string/includes? header "ASSET-TYPE"))
(is (string/includes? header "SIZE"))
(is (string/includes? header "UPDATED-AT"))
(is (string/includes? header "CREATED-AT"))
(is (string/includes? result "Asset Node"))
(is (string/includes? result "md"))
(is (string/includes? result "2552"))
(is (not (string/includes? result "2552")))
(is (some? (re-find #"\b2(\.\d+)?\s*[KM]i?B\b" result)))
(is (not (string/includes? (first lines) " TYPE ")))
(is (not (string/includes? result "PAGE-ID")))
(is (not (string/includes? result "PAGE-TITLE")))
@@ -530,6 +579,13 @@
:ids [1 2 3]}
:data {:result {:ok true}}}
"Removed blocks: 3 (repo: demo-repo)"]
["remove block with large id list uses grouped count"
{:status :ok
:command :remove-block
:context {:repo "demo-repo"
:ids (vec (range 1234))}
:data {:result {:ok true}}}
"Removed blocks: 1,234 (repo: demo-repo)"]
["remove tag renders a succinct success line"
{:status :ok
:command :remove-tag
@@ -559,6 +615,14 @@
:remove-tags ["TagB"]
:remove-properties [:logseq.property/deadline]}
"Upserted block: source-uuid -> target-uuid (repo: demo-repo, tags:+1, properties:+1, remove-tags:+1, remove-properties:+1)"]
["upsert block update uses grouped counts for large changes"
{:repo "demo-repo"
:source "source-uuid"
:update-tags (vec (range 1234))
:update-properties (zipmap (map #(keyword (str "prop-" %)) (range 1234)) (repeat true))
:remove-tags (vec (range 1234))
:remove-properties (mapv #(keyword (str "remove-" %)) (range 1234))}
"Upserted block: source-uuid (repo: demo-repo, tags:+1,234, properties:+1,234, remove-tags:+1,234, remove-properties:+1,234)"]
["upsert block update without move target renders a succinct success line"
{:repo "demo-repo"
:source "source-uuid"
@@ -657,18 +721,20 @@
:data {:repo "demo-graph"
:graph-id "graph-uuid"
:ws-state :open
:pending-local 2
:pending-asset 1
:pending-server 3
:local-tx 10
:remote-tx 13}}
:pending-local 2345
:pending-asset 1234
:pending-server 9876
:local-tx 12345
:remote-tx 67890}}
{:output-format nil})]
(is (string/includes? result "Sync status"))
(is (string/includes? result "demo-graph"))
(is (string/includes? result "graph-uuid"))
(is (string/includes? result "pending-local"))
(is (string/includes? result "pending-asset"))
(is (string/includes? result "pending-server"))))
(is (string/includes? result "pending-local: 2,345"))
(is (string/includes? result "pending-asset: 1,234"))
(is (string/includes? result "pending-server: 9,876"))
(is (string/includes? result "local-tx: 12,345"))
(is (string/includes? result "remote-tx: 67,890"))))
(testing "sync status renders last error diagnostic when present"
(let [result (format/format-result {:status :ok
@@ -900,18 +966,20 @@
(let [result (format/format-result {:status :ok
:command :server-cleanup
:data {:cli-revision "cli-rev"
:checked 4
:mismatched 3
:eligible 2
:skipped-owner 1
:checked 4321
:mismatched 3210
:eligible 2100
:skipped-owner 1111
: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"}]
:killed (mapv (fn [idx]
{:repo (str "logseq_db_graph-killed-" idx)
:pid (+ 1000 idx)
:owner-source :cli
:revision "worker-rev-a"})
(range 1234))
:failed [{:repo "logseq_db_graph-c"
:pid 33
:owner-source :cli
@@ -921,11 +989,11 @@
{:output-format nil})]
(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 "Checked: 4,321"))
(is (string/includes? result "Mismatched: 3,210"))
(is (string/includes? result "Eligible (:cli owner): 2,100"))
(is (string/includes? result "Skipped owner mismatch: 1,111"))
(is (string/includes? result "Killed: 1,234"))
(is (string/includes? result "Failed: 1"))
(is (string/includes? result "Skipped owner targets:"))
(is (string/includes? result "graph-b"))