From b2676eb405a3a2d29e2e9f90f85e043754cd9806 Mon Sep 17 00:00:00 2001 From: rcmerci Date: Sun, 26 Apr 2026 23:05:15 +0800 Subject: [PATCH] feat(skill): add logseq-repl skill --- .agents/skills/db-worker-node-repl/SKILL.md | 158 -------- .agents/skills/logseq-debug-workflow/SKILL.md | 2 +- .agents/skills/logseq-repl/SKILL.md | 293 ++++++++++++++ .../scripts/cleanup-db-worker-node-repl.sh | 42 +- .../scripts/cleanup-desktop-app-repl.sh | 170 ++++++++ .../scripts/start-db-worker-node-repl.sh | 82 +++- .../scripts/start-desktop-app-repl.sh | 329 ++++++++++++++++ .../tests/test-db-worker-node-repl.sh | 343 +++++++++++++++++ .../tests/test-desktop-app-repl.sh | 363 ++++++++++++++++++ .agents/skills/logseq-repl/tests/test-lib.sh | 77 ++++ src/main/logseq/cli/AGENTS.md | 2 +- 11 files changed, 1678 insertions(+), 183 deletions(-) delete mode 100644 .agents/skills/db-worker-node-repl/SKILL.md create mode 100644 .agents/skills/logseq-repl/SKILL.md rename .agents/skills/{db-worker-node-repl => logseq-repl}/scripts/cleanup-db-worker-node-repl.sh (56%) create mode 100755 .agents/skills/logseq-repl/scripts/cleanup-desktop-app-repl.sh rename .agents/skills/{db-worker-node-repl => logseq-repl}/scripts/start-db-worker-node-repl.sh (71%) create mode 100755 .agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh create mode 100644 .agents/skills/logseq-repl/tests/test-db-worker-node-repl.sh create mode 100644 .agents/skills/logseq-repl/tests/test-desktop-app-repl.sh create mode 100644 .agents/skills/logseq-repl/tests/test-lib.sh diff --git a/.agents/skills/db-worker-node-repl/SKILL.md b/.agents/skills/db-worker-node-repl/SKILL.md deleted file mode 100644 index cfe0774eb7..0000000000 --- a/.agents/skills/db-worker-node-repl/SKILL.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -name: db-worker-node-repl -description: Start `db-worker-node` in development and attach a `shadow-cljs` CLJS REPL (or editor nREPL session) to the `:db-worker-node` build for interactive debugging. ---- - -# db-worker-node REPL - -## When to use - -Use this skill when the user asks how to: - -- start `db-worker-node` locally, -- start `shadow-cljs` REPL/nREPL, -- connect a CLJS REPL to the `:db-worker-node` runtime. - -## Repo context - -Commands below assume repo root: - -- `/Users/rcmerci/gh-repos/logseq` - -## Automation scripts (recommended) - -This skill now includes two scripts under `scripts/`: - -- `scripts/start-db-worker-node-repl.sh` -- `scripts/cleanup-db-worker-node-repl.sh` - -Location: - -- `/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/` - -### Start script - -`start-db-worker-node-repl.sh` automates the workflow: - -1. starts/reuses `npx shadow-cljs watch db-worker-node`, -2. starts/reuses `node ./static/db-worker-node.js --repo `, -3. attaches `npx shadow-cljs cljs-repl db-worker-node` (unless `--no-repl`). - -Basic usage: - -```bash -/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh -``` - -Common options: - -```bash -# Start with a specific repo and do not attach REPL -/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh --repo demo --no-repl - -# Pass extra args to db-worker-node -/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh --repo demo -- --create-empty-db - -# Override repo root -/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh --repo-root /path/to/logseq -``` - -Environment overrides: - -- `REPO_ROOT` (default: `/Users/rcmerci/gh-repos/logseq`) -- `DB_REPO` (default: `demo`) - -State/log files created under: - -- `/tmp/db-worker-node-repl/shadow-db-worker-node.log` -- `/tmp/db-worker-node-repl/db-worker-node.log` -- `/tmp/db-worker-node-repl/shadow-db-worker-node.pid` -- `/tmp/db-worker-node-repl/db-worker-node.pid` - -### Cleanup script - -`cleanup-db-worker-node-repl.sh` stops the processes tracked by PID files. - -Basic usage: - -```bash -/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/cleanup-db-worker-node-repl.sh -``` - -Options: - -```bash -# Force kill if graceful stop times out -/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/cleanup-db-worker-node-repl.sh --force - -# Override repo root -/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/cleanup-db-worker-node-repl.sh --repo-root /path/to/logseq -``` - -## Editor nREPL attach flow (Calva/CIDER/Cursive) - -If your editor connects to nREPL directly: - -- Host: `localhost` -- Port: `8701` -- Build: `:db-worker-node` - -For a CLJ-first session (for example CIDER), after connecting to nREPL run: - -```clojure -(shadow.cljs.devtools.api/repl :db-worker-node) -``` - -## If `yarn watch` is already running - -Still prefer the start script as the single entry point: - -```bash -/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh -``` - -If state becomes inconsistent, run cleanup first, then start again. - -## Troubleshooting - -### `shadow-cljs already running in project` - -Cause: stale or conflicting shadow/db-worker-node processes. - -Fix: - -```bash -/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/cleanup-db-worker-node-repl.sh -/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh -``` - -### `repo is required` - -Cause: runtime was started without `--repo`. - -Fix: - -```bash -/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh --repo -``` - -### `No available JS runtime` - -Cause: shadow REPL is up but no live `:db-worker-node` runtime is attached. - -Fix: - -```bash -/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/cleanup-db-worker-node-repl.sh -/Users/rcmerci/gh-repos/logseq/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh --repo -``` - -### Runtime/module startup errors in node process - -If startup fails with missing module/bundled runtime errors, rebuild runtime artifacts first: - -```bash -yarn db-worker-node:release:bundle -``` - -Then run the start script again. diff --git a/.agents/skills/logseq-debug-workflow/SKILL.md b/.agents/skills/logseq-debug-workflow/SKILL.md index 3647e05a7c..434ac5db37 100644 --- a/.agents/skills/logseq-debug-workflow/SKILL.md +++ b/.agents/skills/logseq-debug-workflow/SKILL.md @@ -32,7 +32,7 @@ Useful tools: ### `db-worker-node` -Before controlling or attaching to `db-worker-node`, load repo-local `db-worker-node-repl`. +Before controlling or attaching to `db-worker-node`, load repo-local `logseq-repl`. Useful tools: diff --git a/.agents/skills/logseq-repl/SKILL.md b/.agents/skills/logseq-repl/SKILL.md new file mode 100644 index 0000000000..758a7251c1 --- /dev/null +++ b/.agents/skills/logseq-repl/SKILL.md @@ -0,0 +1,293 @@ +--- +name: logseq-repl +description: Start and coordinate Logseq development REPL workflows for the Desktop renderer `:app` runtime and the `:db-worker-node` runtime while sharing a single `yarn watch` process. +--- + +# Logseq REPL workflows + +Use this skill when the user needs a Logseq development REPL for: + +- Desktop renderer `:app` +- `:db-worker-node` +- or both at once + +This skill keeps both workflows coordinated through `/tmp/logseq-repl/` so they do not start multiple different REPL types at the same time with competing `shadow-cljs` servers. + +## Cheat sheet + +### Pick the right runtime + +- Need DOM, UI, renderer state, page rendering? Use **Desktop app `:app` REPL**. +- Need Node worker behavior or db-worker-node code paths? Use **db-worker-node REPL**. +- Need Electron main process APIs? You probably need `:electron`, not this workflow. + +Runtime reminders: + +- `:app` = Electron renderer +- `:electron` = Electron main process +- `:db-worker` = browser worker +- `:db-worker-node` = Node worker + +### Fast paths + +Desktop `:app`: + +```bash +.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh --no-repl +npx shadow-cljs cljs-repl app +``` + +`db-worker-node`: + +```bash +.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh --repo demo --no-repl +npx shadow-cljs cljs-repl db-worker-node +``` + +Both runtimes: + +```bash +.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh --no-repl +.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh --repo demo --no-repl +npx shadow-cljs cljs-repl app +npx shadow-cljs cljs-repl db-worker-node +``` + +### Preflight for Desktop `:app` + +- close browser dev app instances before starting Desktop `:app` +- keep the Desktop window as the only renderer runtime you want to attach to + +Why: the start script expects exactly one live `:app` runtime. + +### Shared watch and logs + +Both workflows share one `yarn watch` process. + +Look here first: + +- real shared watch log: `/tmp/logseq-repl/shared-shadow-watch.log` + +Workflow-local files may exist but may stay mostly empty when shared watch is reused. + +### Editor attach helpers + +```clojure +(shadow.user/cljs-repl) +(shadow.user/worker-node-repl) +``` + +### Non-interactive verification examples + +Desktop `:app`: + +```bash +printf '(prn {:runtime :app :document? (some? js/document) :title (.-title js/document)})\n:cljs/quit\n' | npx shadow-cljs cljs-repl app +``` + +`db-worker-node`: + +```bash +printf '(prn {:runtime :db-worker-node :process? (some? js/process) :platform (.-platform js/process)})\n:cljs/quit\n' | npx shadow-cljs cljs-repl db-worker-node +``` + +### Cleanup both workflows + +```bash +.agents/skills/logseq-repl/scripts/cleanup-desktop-app-repl.sh +.agents/skills/logseq-repl/scripts/cleanup-db-worker-node-repl.sh +``` + +Expected behavior: + +- cleaning one workflow keeps shared watch alive if the other is still active +- cleaning the last active workflow stops shared watch + +--- + +## Details and troubleshooting + +### Desktop app `:app` REPL + +Scripts: + +- `scripts/start-desktop-app-repl.sh` +- `scripts/cleanup-desktop-app-repl.sh` + +Standard start: + +```bash +.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh +``` + +Start without attaching: + +```bash +.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh --no-repl +``` + +What it does: + +1. starts or reuses `yarn watch` +2. waits for `:app` and `:electron` builds plus nREPL +3. starts or reuses `yarn dev-electron-app` +4. verifies one live `:app` runtime +5. runs a DOM smoke test +6. attaches `npx shadow-cljs cljs-repl app` unless `--no-repl` is used + +### db-worker-node REPL + +Scripts: + +- `scripts/start-db-worker-node-repl.sh` +- `scripts/cleanup-db-worker-node-repl.sh` + +Standard start: + +```bash +.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh --repo demo +``` + +Start without attaching: + +```bash +.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh --repo demo --no-repl +``` + +Pass extra runtime args: + +```bash +.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh --repo demo -- --create-empty-db +``` + +What it does: + +1. starts or reuses shared `yarn watch` +2. waits for the `:db-worker-node` build +3. starts or reuses `node ./static/db-worker-node.js --repo ` +4. runs a REPL connectivity check +5. attaches `npx shadow-cljs cljs-repl db-worker-node` unless `--no-repl` is used + +### Run both `:app` and `:db-worker-node` together + +Use one terminal or editor session per runtime. + +Shared `yarn watch` is expected here. Do not rely on one interactive REPL session to cover both runtimes for you. + +### Editor attach + +If the start scripts already launched `yarn watch`, nREPL should be on `localhost:8701`. + +#### Desktop `:app` + +Direct shadow-cljs/editor attach: + +- Host: `localhost` +- Port: `8701` +- Build: `:app` + +CLJ-first attach: + +```clojure +(shadow.cljs.devtools.api/repl :app) +``` + +Helper: + +```clojure +(shadow.user/cljs-repl) +``` + +#### `db-worker-node` + +Direct shadow-cljs/editor attach: + +- Host: `localhost` +- Port: `8701` +- Build: `:db-worker-node` + +CLJ-first attach: + +```clojure +(shadow.cljs.devtools.api/repl :db-worker-node) +``` + +Helper: + +```clojure +(shadow.user/worker-node-repl) +``` + +`shadow.user/worker-node-repl` picks the first available `:db-worker-node` runtime. + +### Troubleshooting + +#### Desktop `:app`: `No available JS runtime` + +Cause: the Desktop renderer is not connected yet, or the Desktop window is not open. + +Fix: + +1. run `scripts/start-desktop-app-repl.sh --no-repl` +2. confirm the Desktop window is open +3. re-run the script without `--no-repl`, or attach from your editor + +#### Desktop `:app`: wrong runtime attached + +Cause: browser and Desktop renderer runtimes are both alive. + +Fix: close the browser dev app and retry. + +#### Desktop `:app`: connected to `:electron` by mistake + +Symptom: DOM/browser globals are missing. + +Fix: reconnect to `:app`, not `:electron`. + +#### `db-worker-node`: `shadow-cljs already running in project` + +Cause: stale or conflicting shadow/db-worker-node processes. + +Fix: + +```bash +.agents/skills/logseq-repl/scripts/cleanup-db-worker-node-repl.sh +.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh +``` + +#### `db-worker-node`: `repo is required` + +Fix: + +```bash +.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh --repo +``` + +#### `db-worker-node`: `No available JS runtime` + +Fix: + +```bash +.agents/skills/logseq-repl/scripts/cleanup-db-worker-node-repl.sh +.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh --repo +``` + +#### `db-worker-node`: runtime/module startup errors + +Rebuild runtime artifacts first: + +```bash +yarn db-worker-node:release:bundle +``` + +## Recommended response pattern + +When helping a user connect to one or both REPLs: + +1. identify whether they need `:app`, `:db-worker-node`, or both +2. if they need Desktop `:app`, close browser dev app instances first +3. run the matching start script under `logseq-repl/scripts/` +4. if they need both runtimes, start both with `--no-repl` +5. attach from the matching build or helper +6. point troubleshooting at `tmp/logseq-repl/shared-shadow-watch.log` +7. run the matching cleanup script when finished diff --git a/.agents/skills/db-worker-node-repl/scripts/cleanup-db-worker-node-repl.sh b/.agents/skills/logseq-repl/scripts/cleanup-db-worker-node-repl.sh similarity index 56% rename from .agents/skills/db-worker-node-repl/scripts/cleanup-db-worker-node-repl.sh rename to .agents/skills/logseq-repl/scripts/cleanup-db-worker-node-repl.sh index 00f64a87ab..d3e7cab063 100755 --- a/.agents/skills/db-worker-node-repl/scripts/cleanup-db-worker-node-repl.sh +++ b/.agents/skills/logseq-repl/scripts/cleanup-db-worker-node-repl.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash set -euo pipefail -REPO_ROOT="${REPO_ROOT:-/Users/rcmerci/gh-repos/logseq}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEFAULT_REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +REPO_ROOT="${REPO_ROOT:-$DEFAULT_REPO_ROOT}" FORCE_KILL=0 usage() { @@ -12,7 +14,7 @@ Usage: cleanup-db-worker-node-repl.sh [options] Options: - --repo-root Logseq repository root (default: /Users/rcmerci/gh-repos/logseq) + --repo-root Logseq repository root (default: auto-detect from script location) --force Use SIGKILL if process does not stop gracefully -h, --help Show this help EOF @@ -41,8 +43,13 @@ while [[ $# -gt 0 ]]; do done LOG_DIR="$REPO_ROOT/tmp/db-worker-node-repl" +DESKTOP_LOG_DIR="$REPO_ROOT/tmp/desktop-app-repl" +SHARED_LOG_DIR="$REPO_ROOT/tmp/logseq-repl" SHADOW_PID_FILE="$LOG_DIR/shadow-db-worker-node.pid" DB_PID_FILE="$LOG_DIR/db-worker-node.pid" +DB_REPO_FILE="$LOG_DIR/db-worker-node.repo" +DESKTOP_SHADOW_PID_FILE="$DESKTOP_LOG_DIR/shadow-watch.pid" +SHARED_SHADOW_PID_FILE="$SHARED_LOG_DIR/shared-shadow-watch.pid" is_running_pid() { local pid="$1" @@ -102,13 +109,40 @@ stop_by_pid_file() { rm -f "$pid_file" } -if [[ ! -d "$LOG_DIR" ]]; then +stop_shadow_watch() { + local own_pid shared_pid other_pid + own_pid="$(read_pid "$SHADOW_PID_FILE" || true)" + shared_pid="$(read_pid "$SHARED_SHADOW_PID_FILE" || true)" + other_pid="$(read_pid "$DESKTOP_SHADOW_PID_FILE" || true)" + + if [[ -n "${own_pid:-}" && -n "${other_pid:-}" && "$own_pid" == "$other_pid" ]] && is_running_pid "$other_pid"; then + echo "shadow-cljs watch: shared with other workflows, leaving it running" + rm -f "$SHADOW_PID_FILE" + return 0 + fi + + if [[ -n "${shared_pid:-}" && -n "${other_pid:-}" && "$shared_pid" == "$other_pid" ]] && is_running_pid "$other_pid"; then + echo "shadow-cljs watch: shared with other workflows, leaving it running" + rm -f "$SHADOW_PID_FILE" + return 0 + fi + + if [[ -f "$SHARED_SHADOW_PID_FILE" ]]; then + stop_by_pid_file "$SHARED_SHADOW_PID_FILE" "shadow-cljs watch" + rm -f "$SHADOW_PID_FILE" + else + stop_by_pid_file "$SHADOW_PID_FILE" "shadow-cljs watch" + fi +} + +if [[ ! -d "$LOG_DIR" && ! -d "$SHARED_LOG_DIR" ]]; then echo "State directory not found: $LOG_DIR" echo "Nothing to clean up." exit 0 fi stop_by_pid_file "$DB_PID_FILE" "db-worker-node" -stop_by_pid_file "$SHADOW_PID_FILE" "shadow-cljs watch" +rm -f "$DB_REPO_FILE" +stop_shadow_watch echo "Cleanup done." diff --git a/.agents/skills/logseq-repl/scripts/cleanup-desktop-app-repl.sh b/.agents/skills/logseq-repl/scripts/cleanup-desktop-app-repl.sh new file mode 100755 index 0000000000..4f56e22029 --- /dev/null +++ b/.agents/skills/logseq-repl/scripts/cleanup-desktop-app-repl.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEFAULT_REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +REPO_ROOT="${REPO_ROOT:-$DEFAULT_REPO_ROOT}" +FORCE_KILL=0 + +usage() { + cat <<'EOF' +Stop processes started by start-desktop-app-repl.sh. + +Usage: + cleanup-desktop-app-repl.sh [options] + +Options: + --repo-root Logseq repository root (default: auto-detect from script location) + --force Use SIGKILL if process does not stop gracefully + -h, --help Show this help +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --repo-root) + shift + REPO_ROOT="${1:?missing value for --repo-root}" + ;; + --force) + FORCE_KILL=1 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + esac + shift +done + +LOG_DIR="$REPO_ROOT/tmp/desktop-app-repl" +DB_WORKER_LOG_DIR="$REPO_ROOT/tmp/db-worker-node-repl" +SHARED_LOG_DIR="$REPO_ROOT/tmp/logseq-repl" +SHADOW_PID_FILE="$LOG_DIR/shadow-watch.pid" +DESKTOP_PID_FILE="$LOG_DIR/desktop-electron.pid" +DB_WORKER_SHADOW_PID_FILE="$DB_WORKER_LOG_DIR/shadow-db-worker-node.pid" +SHARED_SHADOW_PID_FILE="$SHARED_LOG_DIR/shared-shadow-watch.pid" + +is_running_pid() { + local pid="$1" + [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null +} + +read_pid() { + local file="$1" + if [[ -f "$file" ]]; then + tr -d '[:space:]' < "$file" + fi +} + +process_group_id() { + local pid="$1" + ps -o pgid= -p "$pid" 2>/dev/null | tr -d '[:space:]' +} + +current_process_group_id() { + process_group_id "$$" +} + +signal_process_group() { + local signal="$1" + local pid="$2" + + local current_pgid pgid + pgid="$(process_group_id "$pid" || true)" + current_pgid="$(current_process_group_id || true)" + + if [[ -n "${pgid:-}" && "$pgid" =~ ^[0-9]+$ && "$pgid" != "${current_pgid:-}" ]]; then + kill "-$signal" "-$pgid" 2>/dev/null || true + fi + + kill "-$signal" "$pid" 2>/dev/null || true +} + +stop_by_pid_file() { + local pid_file="$1" + local label="$2" + + local pid + pid="$(read_pid "$pid_file" || true)" + + if [[ -z "${pid:-}" ]]; then + echo "$label: no pid file, nothing to stop" + return 0 + fi + + if ! is_running_pid "$pid"; then + echo "$label: process already stopped (pid=$pid)" + rm -f "$pid_file" + return 0 + fi + + echo "$label: stopping pid=$pid" + signal_process_group TERM "$pid" + + local i + for ((i=0; i<10; i++)); do + if ! is_running_pid "$pid"; then + echo "$label: stopped" + rm -f "$pid_file" + return 0 + fi + sleep 1 + done + + if [[ "$FORCE_KILL" -eq 1 ]]; then + echo "$label: force killing pid=$pid" + signal_process_group KILL "$pid" + sleep 1 + fi + + if is_running_pid "$pid"; then + echo "$label: failed to stop pid=$pid" >&2 + return 1 + fi + + echo "$label: stopped" + rm -f "$pid_file" +} + +stop_shadow_watch() { + local own_pid shared_pid other_pid + own_pid="$(read_pid "$SHADOW_PID_FILE" || true)" + shared_pid="$(read_pid "$SHARED_SHADOW_PID_FILE" || true)" + other_pid="$(read_pid "$DB_WORKER_SHADOW_PID_FILE" || true)" + + if [[ -n "${own_pid:-}" && -n "${other_pid:-}" && "$own_pid" == "$other_pid" ]] && is_running_pid "$other_pid"; then + echo "shadow-cljs watch: shared with other workflows, leaving it running" + rm -f "$SHADOW_PID_FILE" + return 0 + fi + + if [[ -n "${shared_pid:-}" && -n "${other_pid:-}" && "$shared_pid" == "$other_pid" ]] && is_running_pid "$other_pid"; then + echo "shadow-cljs watch: shared with other workflows, leaving it running" + rm -f "$SHADOW_PID_FILE" + return 0 + fi + + if [[ -f "$SHARED_SHADOW_PID_FILE" ]]; then + stop_by_pid_file "$SHARED_SHADOW_PID_FILE" "shadow-cljs watch" + rm -f "$SHADOW_PID_FILE" + else + stop_by_pid_file "$SHADOW_PID_FILE" "shadow-cljs watch" + fi +} + +if [[ ! -d "$LOG_DIR" && ! -d "$SHARED_LOG_DIR" ]]; then + echo "State directory not found: $LOG_DIR" + echo "Nothing to clean up." + exit 0 +fi + +stop_by_pid_file "$DESKTOP_PID_FILE" "desktop-electron" +stop_shadow_watch + +echo "Cleanup done." diff --git a/.agents/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh b/.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh similarity index 71% rename from .agents/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh rename to .agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh index 77eb05909c..6ca2aa4065 100755 --- a/.agents/skills/db-worker-node-repl/scripts/start-db-worker-node-repl.sh +++ b/.agents/skills/logseq-repl/scripts/start-db-worker-node-repl.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash set -euo pipefail -REPO_ROOT="${REPO_ROOT:-/Users/rcmerci/gh-repos/logseq}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEFAULT_REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +REPO_ROOT="${REPO_ROOT:-$DEFAULT_REPO_ROOT}" DB_REPO="${DB_REPO:-demo}" ATTACH_REPL=1 EXTRA_NODE_ARGS=() @@ -15,7 +17,7 @@ Usage: Options: --repo Graph repo name passed to db-worker-node (default: demo) - --repo-root Logseq repository root (default: /Users/rcmerci/gh-repos/logseq) + --repo-root Logseq repository root (default: auto-detect from script location) --no-repl Do not attach `shadow-cljs cljs-repl` after startup --repl Force attach REPL (default behavior) -h, --help Show this help @@ -69,18 +71,28 @@ if ! command -v npx >/dev/null 2>&1; then exit 1 fi +if ! command -v yarn >/dev/null 2>&1; then + echo "Error: yarn not found in PATH" >&2 + exit 1 +fi + if ! command -v node >/dev/null 2>&1; then echo "Error: node not found in PATH" >&2 exit 1 fi LOG_DIR="$REPO_ROOT/tmp/db-worker-node-repl" +SHARED_LOG_DIR="$REPO_ROOT/tmp/logseq-repl" mkdir -p "$LOG_DIR" +mkdir -p "$SHARED_LOG_DIR" SHADOW_PID_FILE="$LOG_DIR/shadow-db-worker-node.pid" DB_PID_FILE="$LOG_DIR/db-worker-node.pid" +DB_REPO_FILE="$LOG_DIR/db-worker-node.repo" SHADOW_LOG="$LOG_DIR/shadow-db-worker-node.log" DB_LOG="$LOG_DIR/db-worker-node.log" +SHARED_SHADOW_PID_FILE="$SHARED_LOG_DIR/shared-shadow-watch.pid" +SHARED_SHADOW_LOG="$SHARED_LOG_DIR/shared-shadow-watch.log" is_running_pid() { local pid="$1" @@ -131,51 +143,80 @@ wait_for_shadow_build_ready() { return 1 } +start_background_command() { + local log_file="$1" + shift + + pushd "$REPO_ROOT" >/dev/null + if command -v setsid >/dev/null 2>&1; then + nohup setsid "$@" > "$log_file" 2>&1 & + else + nohup "$@" > "$log_file" 2>&1 & + fi + local pid=$! + popd >/dev/null + + echo "$pid" +} + ensure_shadow_watch() { - local existing_pid + local existing_pid shared_pid existing_pid="$(read_pid "$SHADOW_PID_FILE" || true)" if [[ -n "${existing_pid:-}" ]] && is_running_pid "$existing_pid"; then - echo "Reusing shadow-cljs watch (pid=$existing_pid)" + echo "Reusing shared shadow-cljs watch (pid=$existing_pid)" return 0 fi - echo "Starting shadow-cljs watch db-worker-node ..." - pushd "$REPO_ROOT" >/dev/null - nohup npx shadow-cljs watch db-worker-node > "$SHADOW_LOG" 2>&1 & - local shadow_pid=$! - popd >/dev/null + shared_pid="$(read_pid "$SHARED_SHADOW_PID_FILE" || true)" + if [[ -n "${shared_pid:-}" ]] && is_running_pid "$shared_pid"; then + echo "$shared_pid" > "$SHADOW_PID_FILE" + : > "$SHADOW_LOG" + echo "Reusing shared shadow-cljs watch (pid=$shared_pid)" + if wait_for_shadow_build_ready "$SHARED_SHADOW_LOG" 120; then + echo "shadow-cljs db-worker-node build is ready" + else + echo "Error: shadow-cljs build is not ready. Check $SHARED_SHADOW_LOG" >&2 + exit 1 + fi + return 0 + fi + echo "Starting shared shadow-cljs watch via yarn watch ..." + local shadow_pid + shadow_pid="$(start_background_command "$SHARED_SHADOW_LOG" yarn watch)" + + echo "$shadow_pid" > "$SHARED_SHADOW_PID_FILE" echo "$shadow_pid" > "$SHADOW_PID_FILE" + : > "$SHADOW_LOG" sleep 1 if ! is_running_pid "$shadow_pid"; then - echo "Error: shadow-cljs exited early. Check $SHADOW_LOG" >&2 + echo "Error: yarn watch exited early. Check $SHARED_SHADOW_LOG" >&2 exit 1 fi - if wait_for_log_pattern "$SHADOW_LOG" "watching build :db-worker-node" 45; then + if wait_for_log_pattern "$SHARED_SHADOW_LOG" "watching build :db-worker-node\\|\\[:db-worker-node\\] Build completed\\." 45; then echo "shadow-cljs watch is ready (pid=$shadow_pid)" else echo "Warning: did not observe watch-ready log within timeout. Continuing anyway." >&2 fi - if wait_for_shadow_build_ready "$SHADOW_LOG" 120; then + if wait_for_shadow_build_ready "$SHARED_SHADOW_LOG" 120; then echo "shadow-cljs db-worker-node build is ready" else - echo "Error: shadow-cljs build is not ready. Check $SHADOW_LOG" >&2 + echo "Error: shadow-cljs build is not ready. Check $SHARED_SHADOW_LOG" >&2 exit 1 fi } ensure_db_worker_node() { - local existing_pid + local existing_pid existing_repo existing_pid="$(read_pid "$DB_PID_FILE" || true)" + existing_repo="$(read_pid "$DB_REPO_FILE" || true)" if [[ -n "${existing_pid:-}" ]] && is_running_pid "$existing_pid"; then - local existing_cmd - existing_cmd="$(ps -p "$existing_pid" -o command= || true)" - if [[ "$existing_cmd" == *"--repo $DB_REPO"* ]]; then + if [[ "$existing_repo" == "$DB_REPO" ]]; then echo "Reusing db-worker-node runtime (pid=$existing_pid, repo=$DB_REPO)" return 0 fi @@ -196,6 +237,7 @@ ensure_db_worker_node() { popd >/dev/null echo "$db_pid" > "$DB_PID_FILE" + echo "$DB_REPO" > "$DB_REPO_FILE" sleep 1 if ! is_running_pid "$db_pid"; then @@ -247,9 +289,11 @@ ensure_db_worker_node verify_repl_connectivity echo + echo "Logs:" -echo " shadow-cljs: $SHADOW_LOG" -echo " db-worker: $DB_LOG" +echo " shared shadow-cljs: $SHARED_SHADOW_LOG" +echo " db-worker-node: $DB_LOG" +echo " workflow state file: $SHADOW_LOG" echo "PID files:" echo " $SHADOW_PID_FILE" echo " $DB_PID_FILE" diff --git a/.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh b/.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh new file mode 100755 index 0000000000..9d39c68abd --- /dev/null +++ b/.agents/skills/logseq-repl/scripts/start-desktop-app-repl.sh @@ -0,0 +1,329 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEFAULT_REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +REPO_ROOT="${REPO_ROOT:-$DEFAULT_REPO_ROOT}" +ATTACH_REPL=1 + +usage() { + cat <<'EOF' +Start or reuse the Desktop app `:app` REPL workflow. + +Usage: + start-desktop-app-repl.sh [options] + +Options: + --repo-root Logseq repository root (default: auto-detect from script location) + --no-repl Do not attach `shadow-cljs cljs-repl app` after startup + --repl Force attach REPL (default behavior) + -h, --help Show this help +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --repo-root) + shift + REPO_ROOT="${1:?missing value for --repo-root}" + ;; + --no-repl) + ATTACH_REPL=0 + ;; + --repl) + ATTACH_REPL=1 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + esac + shift +done + +if [[ ! -d "$REPO_ROOT" ]]; then + echo "Error: repo root not found: $REPO_ROOT" >&2 + exit 1 +fi + +if ! command -v yarn >/dev/null 2>&1; then + echo "Error: yarn not found in PATH" >&2 + exit 1 +fi + +if ! command -v npx >/dev/null 2>&1; then + echo "Error: npx not found in PATH" >&2 + exit 1 +fi + +LOG_DIR="$REPO_ROOT/tmp/desktop-app-repl" +SHARED_LOG_DIR="$REPO_ROOT/tmp/logseq-repl" +mkdir -p "$LOG_DIR" +mkdir -p "$SHARED_LOG_DIR" + +SHADOW_PID_FILE="$LOG_DIR/shadow-watch.pid" +DESKTOP_PID_FILE="$LOG_DIR/desktop-electron.pid" +SHADOW_LOG="$LOG_DIR/shadow-watch.log" +DESKTOP_LOG="$LOG_DIR/desktop-electron.log" +SHARED_SHADOW_PID_FILE="$SHARED_LOG_DIR/shared-shadow-watch.pid" +SHARED_SHADOW_LOG="$SHARED_LOG_DIR/shared-shadow-watch.log" + +is_running_pid() { + local pid="$1" + [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null +} + +read_pid() { + local file="$1" + if [[ -f "$file" ]]; then + tr -d '[:space:]' < "$file" + fi +} + +wait_for_all_patterns() { + local file="$1" + local timeout_seconds="$2" + shift 2 + + local second pattern all_found + for ((second=0; second/dev/null + if command -v setsid >/dev/null 2>&1; then + nohup setsid "$@" > "$log_file" 2>&1 & + else + nohup "$@" > "$log_file" 2>&1 & + fi + local pid=$! + popd >/dev/null + + echo "$pid" +} + +ensure_shadow_watch() { + local existing_pid shared_pid + existing_pid="$(read_pid "$SHADOW_PID_FILE" || true)" + + if [[ -n "${existing_pid:-}" ]] && is_running_pid "$existing_pid"; then + echo "Reusing shared shadow-cljs watch (pid=$existing_pid)" + return 0 + fi + + shared_pid="$(read_pid "$SHARED_SHADOW_PID_FILE" || true)" + if [[ -n "${shared_pid:-}" ]] && is_running_pid "$shared_pid"; then + echo "$shared_pid" > "$SHADOW_PID_FILE" + : > "$SHADOW_LOG" + echo "Reusing shared shadow-cljs watch (pid=$shared_pid)" + return 0 + fi + + echo "Starting shadow-cljs watch via yarn watch ..." + local shadow_pid + shadow_pid="$(start_background_command "$SHARED_SHADOW_LOG" yarn watch)" + echo "$shadow_pid" > "$SHARED_SHADOW_PID_FILE" + echo "$shadow_pid" > "$SHADOW_PID_FILE" + : > "$SHADOW_LOG" + sleep 1 + + if ! is_running_pid "$shadow_pid"; then + echo "Error: yarn watch exited early. Check $SHARED_SHADOW_LOG" >&2 + exit 1 + fi + + if wait_for_all_patterns "$SHARED_SHADOW_LOG" 180 \ + "shadow-cljs - nREPL server started on port 8701" \ + "[:electron] Build completed." \ + "[:app] Build completed."; then + echo "shadow-cljs watch is ready (pid=$shadow_pid)" + else + echo "Error: yarn watch did not become ready in time. Check $SHARED_SHADOW_LOG" >&2 + exit 1 + fi +} + +ensure_desktop_app() { + local existing_pid + existing_pid="$(read_pid "$DESKTOP_PID_FILE" || true)" + + if [[ -n "${existing_pid:-}" ]] && is_running_pid "$existing_pid"; then + echo "Reusing Desktop dev app (pid=$existing_pid)" + return 0 + fi + + echo "Starting Desktop dev app via yarn dev-electron-app ..." + local desktop_pid + desktop_pid="$(start_background_command "$DESKTOP_LOG" yarn dev-electron-app)" + echo "$desktop_pid" > "$DESKTOP_PID_FILE" + sleep 1 + + if ! is_running_pid "$desktop_pid"; then + echo "Error: yarn dev-electron-app exited early. Check $DESKTOP_LOG" >&2 + exit 1 + fi + + if wait_for_any_pattern "$DESKTOP_LOG" 120 "shadow-cljs - #" "Logseq App("; then + echo "Desktop dev app is running (pid=$desktop_pid)" + else + echo "Error: Desktop dev app did not report startup in time. Check $DESKTOP_LOG" >&2 + exit 1 + fi +} + +get_app_runtime_count() { + local output + pushd "$REPO_ROOT" >/dev/null + if ! output="$(npx shadow-cljs clj-eval "(do (require '[shadow.cljs.devtools.api :as api]) (println (count (api/repl-runtimes :app))))" 2>&1)"; then + popd >/dev/null + echo "Error: failed to inspect :app runtimes." >&2 + echo "--- clj-eval output ---" >&2 + echo "$output" >&2 + echo "-----------------------" >&2 + exit 1 + fi + popd >/dev/null + + local runtime_count + runtime_count="$(printf '%s\n' "$output" | awk '/^[0-9]+$/{n=$0} END{if (n != "") print n; else exit 1}')" || { + echo "Error: could not parse :app runtime count." >&2 + echo "--- clj-eval output ---" >&2 + echo "$output" >&2 + echo "-----------------------" >&2 + exit 1 + } + + printf '%s\n' "$runtime_count" +} + +ensure_single_app_runtime() { + local runtime_count second + + for ((second=0; second<60; second++)); do + runtime_count="$(get_app_runtime_count)" + + if [[ "$runtime_count" == "1" ]]; then + echo "Detected exactly one live :app runtime" + return 0 + fi + + if [[ "$runtime_count" != "0" ]]; then + break + fi + + sleep 1 + done + + if [[ "$runtime_count" == "0" ]]; then + echo "Error: Expected exactly one live :app runtime, found 0 after waiting for the Desktop renderer to connect." >&2 + echo "Make sure the Desktop dev app window is fully open, then retry." >&2 + exit 1 + fi + + echo "Error: Expected exactly one live :app runtime, found $runtime_count." >&2 + echo "Close the browser dev app so only the Desktop renderer remains, then retry." >&2 + exit 1 +} + +verify_renderer_smoke_test() { + echo "Verifying Desktop renderer smoke test ..." + + local smoke_output + pushd "$REPO_ROOT" >/dev/null + if ! smoke_output="$(printf '(some? js/document)\n(.-title js/document)\n:cljs/quit\n' | npx shadow-cljs cljs-repl app 2>&1)"; then + popd >/dev/null + echo "Error: smoke test REPL command failed." >&2 + echo "--- smoke output ---" >&2 + echo "$smoke_output" >&2 + echo "--------------------" >&2 + exit 1 + fi + popd >/dev/null + + if [[ "$smoke_output" != *"shadow-cljs - connected to server"* ]]; then + echo "Error: smoke test did not connect to shadow-cljs." >&2 + echo "--- smoke output ---" >&2 + echo "$smoke_output" >&2 + echo "--------------------" >&2 + exit 1 + fi + + if [[ "$smoke_output" != *$'cljs.user=> true'* ]]; then + echo "Error: smoke test did not confirm js/document." >&2 + echo "--- smoke output ---" >&2 + echo "$smoke_output" >&2 + echo "--------------------" >&2 + exit 1 + fi + + echo "Desktop renderer smoke test passed" +} + +ensure_shadow_watch +ensure_desktop_app +ensure_single_app_runtime +verify_renderer_smoke_test + +echo + +echo "Logs:" +echo " shared shadow-cljs: $SHARED_SHADOW_LOG" +echo " desktop-app: $DESKTOP_LOG" +echo " workflow state file: $SHADOW_LOG" +echo "PID files:" +echo " $SHADOW_PID_FILE" +echo " $DESKTOP_PID_FILE" +echo + +if [[ "$ATTACH_REPL" -eq 1 ]]; then + echo "Attaching CLJS REPL: npx shadow-cljs cljs-repl app" + pushd "$REPO_ROOT" >/dev/null + npx shadow-cljs cljs-repl app + popd >/dev/null +else + echo "Startup complete. REPL attach skipped (--no-repl)." +fi diff --git a/.agents/skills/logseq-repl/tests/test-db-worker-node-repl.sh b/.agents/skills/logseq-repl/tests/test-db-worker-node-repl.sh new file mode 100644 index 0000000000..d5f013d530 --- /dev/null +++ b/.agents/skills/logseq-repl/tests/test-db-worker-node-repl.sh @@ -0,0 +1,343 @@ +#!/usr/bin/env bash +set -euo pipefail + +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKILL_DIR="$(cd "$TEST_DIR/.." && pwd)" +SKILLS_ROOT="$(cd "$SKILL_DIR/.." && pwd)" +START_SCRIPT="$SKILL_DIR/scripts/start-db-worker-node-repl.sh" +CLEANUP_SCRIPT="$SKILL_DIR/scripts/cleanup-db-worker-node-repl.sh" +DESKTOP_START_SCRIPT="$SKILL_DIR/scripts/start-desktop-app-repl.sh" +DESKTOP_CLEANUP_SCRIPT="$SKILL_DIR/scripts/cleanup-desktop-app-repl.sh" +SKILL_FILE="$SKILL_DIR/SKILL.md" +CURRENT_SKILL_NAME="$(basename "$SKILL_DIR")" +ORIGINAL_PATH="$PATH" + +# shellcheck source=./test-lib.sh +source "$TEST_DIR/test-lib.sh" + +PASS_COUNT=0 +FAIL_COUNT=0 + +create_fake_env() { + TEST_ROOT="$(mktemp -d)" + REPO_ROOT="$TEST_ROOT/repo" + BIN_DIR="$TEST_ROOT/bin" + CMD_LOG="$TEST_ROOT/commands.log" + + mkdir -p "$REPO_ROOT/static" "$BIN_DIR" + : > "$CMD_LOG" + + cat > "$BIN_DIR/yarn" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +echo "yarn $*" >> "$FAKE_CMD_LOG" + +case "${1:-}" in + watch) + echo "shadow-cljs - nREPL server started on port 8701" + echo "[:electron] Build completed." + echo "[:app] Build completed." + echo "[:db-worker-node] Build completed." + while true; do sleep 1; done + ;; + dev-electron-app) + echo "17:12:00.841 › Logseq App(2.0.1) Starting..." + echo "shadow-cljs - #6 ready!" + while true; do sleep 1; done + ;; + *) + echo "Unexpected yarn command: $*" >&2 + exit 1 + ;; + esac +EOF + + cat > "$BIN_DIR/npx" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +input="" +if [[ "${1:-}" == "shadow-cljs" && "${2:-}" == "cljs-repl" ]] && [[ -p /dev/stdin ]]; then + input="$(cat)" +fi + +if [[ "${1:-}" == "shadow-cljs" && "${2:-}" == "clj-eval" ]]; then + echo "npx-clj-eval $*" >> "$FAKE_CMD_LOG" + echo "shadow-cljs - connected to server" + echo "${FAKE_APP_RUNTIME_COUNT:-1}" + exit 0 +fi + +if [[ "${1:-}" == "shadow-cljs" && "${2:-}" == "cljs-repl" && "${3:-}" == "app" ]]; then + if [[ "$input" == *"(.-title js/document)"* ]]; then + echo "npx-app-smoke $*" >> "$FAKE_CMD_LOG" + echo "shadow-cljs - connected to server" + echo "cljs.user=> true" + echo 'cljs.user=> "Logseq"' + echo "cljs.user=>" + exit 0 + fi + + echo "npx-app-attach $*" >> "$FAKE_CMD_LOG" + echo "shadow-cljs - connected to server" + echo "cljs.user=>" + exit 0 +fi + +if [[ "${1:-}" == "shadow-cljs" && "${2:-}" == "cljs-repl" && "${3:-}" == "db-worker-node" ]]; then + if [[ "$input" == *"(+ 1 2)"* ]]; then + echo "npx-db-smoke $*" >> "$FAKE_CMD_LOG" + echo "shadow-cljs - connected to server" + echo "cljs.user=> 3" + echo "cljs.user=>" + exit 0 + fi + + echo "npx-db-attach $*" >> "$FAKE_CMD_LOG" + echo "shadow-cljs - connected to server" + echo "cljs.user=>" + exit 0 +fi + +echo "Unexpected npx command: $*" >&2 +exit 1 +EOF + + cat > "$BIN_DIR/node" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +echo "node $*" >> "$FAKE_CMD_LOG" +while true; do sleep 1; done +EOF + + cat > "$BIN_DIR/setsid" <<'EOF' +#!/usr/bin/env bash +exec "$@" +EOF + + chmod +x "$BIN_DIR/yarn" "$BIN_DIR/npx" "$BIN_DIR/node" "$BIN_DIR/setsid" + + export PATH="$BIN_DIR:$ORIGINAL_PATH" + export FAKE_CMD_LOG="$CMD_LOG" + export FAKE_APP_RUNTIME_COUNT="${FAKE_APP_RUNTIME_COUNT:-1}" +} + +cleanup_fake_env() { + if [[ -n "${REPO_ROOT:-}" ]]; then + local pid_file + for pid_file in \ + "$REPO_ROOT"/tmp/db-worker-node-repl/*.pid \ + "$REPO_ROOT"/tmp/desktop-app-repl/*.pid \ + "$REPO_ROOT"/tmp/logseq-repl/*.pid; do + [[ -e "$pid_file" ]] || continue + local pid + pid="$(tr -d '[:space:]' < "$pid_file")" + if [[ "$pid" =~ ^[0-9]+$ ]]; then + kill -9 "$pid" 2>/dev/null || true + fi + done + fi + + PATH="$ORIGINAL_PATH" + unset FAKE_CMD_LOG FAKE_APP_RUNTIME_COUNT || true + + if [[ -n "${TEST_ROOT:-}" && -d "$TEST_ROOT" ]]; then + rm -rf "$TEST_ROOT" + fi +} + +scripts_exist_and_skill_is_renamed_test() { + assert_equals "logseq-repl" "$CURRENT_SKILL_NAME" + assert_file_exists "$START_SCRIPT" + assert_file_exists "$CLEANUP_SCRIPT" + assert_file_exists "$DESKTOP_START_SCRIPT" + assert_file_exists "$DESKTOP_CLEANUP_SCRIPT" +} + +start_no_repl_launches_shared_shadow_and_runtime_test() { + create_fake_env + trap cleanup_fake_env RETURN + + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo --no-repl > "$TEST_ROOT/output.log" 2>&1 + + assert_contains "Startup complete. REPL attach skipped (--no-repl)." "$TEST_ROOT/output.log" + assert_contains "shared shadow-cljs: $REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.log" "$TEST_ROOT/output.log" + assert_contains "db-worker-node: $REPO_ROOT/tmp/db-worker-node-repl/db-worker-node.log" "$TEST_ROOT/output.log" + assert_contains "yarn watch" "$CMD_LOG" + assert_contains "node ./static/db-worker-node.js --repo demo" "$CMD_LOG" + assert_contains "npx-db-smoke shadow-cljs cljs-repl db-worker-node" "$CMD_LOG" + + assert_file_exists "$REPO_ROOT/tmp/db-worker-node-repl/shadow-db-worker-node.pid" + assert_file_exists "$REPO_ROOT/tmp/db-worker-node-repl/db-worker-node.pid" + assert_file_exists "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid" + assert_file_exists "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.log" +} + +start_reuses_existing_db_worker_runtime_test() { + create_fake_env + trap cleanup_fake_env RETURN + + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo --no-repl > "$TEST_ROOT/first.log" 2>&1 + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo --no-repl > "$TEST_ROOT/second.log" 2>&1 + + local watch_count node_count + watch_count="$(grep -c '^yarn watch$' "$CMD_LOG")" + node_count="$(grep -c '^node ./static/db-worker-node.js --repo demo$' "$CMD_LOG")" + + assert_equals "1" "$watch_count" + assert_equals "1" "$node_count" + assert_contains "Reusing shared shadow-cljs watch" "$TEST_ROOT/second.log" + assert_contains "Reusing db-worker-node runtime" "$TEST_ROOT/second.log" +} + +desktop_and_db_worker_share_single_shadow_watch_test() { + create_fake_env + trap cleanup_fake_env RETURN + + bash "$DESKTOP_START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/desktop.log" 2>&1 + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo --no-repl > "$TEST_ROOT/db.log" 2>&1 + + local watch_count desktop_shadow_pid db_shadow_pid + watch_count="$(grep -c '^yarn watch$' "$CMD_LOG")" + desktop_shadow_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/desktop-app-repl/shadow-watch.pid")" + db_shadow_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/db-worker-node-repl/shadow-db-worker-node.pid")" + + assert_equals "1" "$watch_count" + assert_equals "$desktop_shadow_pid" "$db_shadow_pid" + assert_contains "Reusing shared shadow-cljs watch" "$TEST_ROOT/db.log" +} + +cleanup_db_worker_keeps_shared_watch_for_desktop_test() { + create_fake_env + trap cleanup_fake_env RETURN + + bash "$DESKTOP_START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/desktop.log" 2>&1 + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo --no-repl > "$TEST_ROOT/db.log" 2>&1 + + local watch_pid runtime_pid + watch_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid")" + runtime_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/db-worker-node-repl/db-worker-node.pid")" + + bash "$CLEANUP_SCRIPT" --repo-root "$REPO_ROOT" > "$TEST_ROOT/cleanup-db.log" 2>&1 + + if ! kill -0 "$watch_pid" 2>/dev/null; then + fail "expected shared watch to keep running while desktop workflow is active" + fi + + if kill -0 "$runtime_pid" 2>/dev/null; then + fail "expected db-worker-node runtime to stop during cleanup" + fi + + assert_contains "shadow-cljs watch: shared with other workflows, leaving it running" "$TEST_ROOT/cleanup-db.log" + assert_file_exists "$REPO_ROOT/tmp/desktop-app-repl/shadow-watch.pid" + assert_not_exists "$REPO_ROOT/tmp/db-worker-node-repl/shadow-db-worker-node.pid" +} + +cleanup_desktop_keeps_shared_watch_for_db_worker_test() { + create_fake_env + trap cleanup_fake_env RETURN + + bash "$DESKTOP_START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/desktop.log" 2>&1 + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo --no-repl > "$TEST_ROOT/db.log" 2>&1 + + local watch_pid electron_pid + watch_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid")" + electron_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/desktop-app-repl/desktop-electron.pid")" + + bash "$DESKTOP_CLEANUP_SCRIPT" --repo-root "$REPO_ROOT" > "$TEST_ROOT/cleanup-desktop.log" 2>&1 + + if ! kill -0 "$watch_pid" 2>/dev/null; then + fail "expected shared watch to keep running while db-worker workflow is active" + fi + + if kill -0 "$electron_pid" 2>/dev/null; then + fail "expected desktop electron process to stop during cleanup" + fi + + assert_contains "shadow-cljs watch: shared with other workflows, leaving it running" "$TEST_ROOT/cleanup-desktop.log" + assert_file_exists "$REPO_ROOT/tmp/db-worker-node-repl/shadow-db-worker-node.pid" + assert_not_exists "$REPO_ROOT/tmp/desktop-app-repl/shadow-watch.pid" +} + +shared_watch_stops_after_last_cleanup_test() { + create_fake_env + trap cleanup_fake_env RETURN + + bash "$DESKTOP_START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/desktop.log" 2>&1 + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo --no-repl > "$TEST_ROOT/db.log" 2>&1 + + local watch_pid + watch_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid")" + + bash "$CLEANUP_SCRIPT" --repo-root "$REPO_ROOT" > "$TEST_ROOT/cleanup-db.log" 2>&1 + bash "$DESKTOP_CLEANUP_SCRIPT" --repo-root "$REPO_ROOT" > "$TEST_ROOT/cleanup-desktop.log" 2>&1 + + if kill -0 "$watch_pid" 2>/dev/null; then + fail "expected shared watch to stop after the last workflow cleaned up" + fi + + assert_not_exists "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid" +} + +start_attaches_repl_by_default_test() { + create_fake_env + trap cleanup_fake_env RETURN + + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --repo demo < /dev/null > "$TEST_ROOT/output.log" 2>&1 + + assert_contains "Attaching CLJS REPL: npx shadow-cljs cljs-repl db-worker-node" "$TEST_ROOT/output.log" + assert_contains "npx-db-attach shadow-cljs cljs-repl db-worker-node" "$CMD_LOG" +} + +help_and_docs_are_portable_test() { + local temp_dir start_help_file cleanup_help_file path_pattern + temp_dir="$(mktemp -d)" + start_help_file="$temp_dir/start-help.txt" + cleanup_help_file="$temp_dir/cleanup-help.txt" + path_pattern="$(portable_path_pattern)" + + bash "$START_SCRIPT" --help > "$start_help_file" + bash "$CLEANUP_SCRIPT" --help > "$cleanup_help_file" + + assert_not_matches "$path_pattern" "$start_help_file" + assert_not_matches "$path_pattern" "$cleanup_help_file" + assert_not_matches "$path_pattern" "$SKILL_FILE" + assert_contains "name: logseq-repl" "$SKILL_FILE" + assert_contains 'Desktop app `:app` REPL' "$SKILL_FILE" + assert_contains "db-worker-node REPL" "$SKILL_FILE" + assert_contains "multiple different REPL types at the same time" "$SKILL_FILE" + assert_contains "tmp/logseq-repl" "$SKILL_FILE" + assert_contains 'Run both `:app` and `:db-worker-node` together' "$SKILL_FILE" + assert_contains "shared-shadow-watch.log" "$SKILL_FILE" + assert_contains "shadow.user/worker-node-repl" "$SKILL_FILE" + assert_contains "Non-interactive verification examples" "$SKILL_FILE" + assert_contains "Cleanup both workflows" "$SKILL_FILE" + + rm -rf "$temp_dir" +} + +obsolete_skill_directories_removed_test() { + assert_not_exists "$SKILLS_ROOT/desktop-app-repl" + assert_not_exists "$SKILLS_ROOT/db-worker-node-repl" +} + +run_test "scripts exist and skill is renamed" scripts_exist_and_skill_is_renamed_test +run_test "start --no-repl launches shared shadow and runtime" start_no_repl_launches_shared_shadow_and_runtime_test +run_test "start reuses existing db-worker runtime" start_reuses_existing_db_worker_runtime_test +run_test "desktop and db-worker share a single shadow watch" desktop_and_db_worker_share_single_shadow_watch_test +run_test "cleanup db-worker keeps shared watch for desktop" cleanup_db_worker_keeps_shared_watch_for_desktop_test +run_test "cleanup desktop keeps shared watch for db-worker" cleanup_desktop_keeps_shared_watch_for_db_worker_test +run_test "shared watch stops after last cleanup" shared_watch_stops_after_last_cleanup_test +run_test "start attaches repl by default" start_attaches_repl_by_default_test +run_test "help and docs are portable" help_and_docs_are_portable_test +run_test "obsolete skill directories removed" obsolete_skill_directories_removed_test + +echo +if [[ "$FAIL_COUNT" -gt 0 ]]; then + echo "$FAIL_COUNT test(s) failed; $PASS_COUNT passed." >&2 + exit 1 +fi + +echo "All $PASS_COUNT test(s) passed." diff --git a/.agents/skills/logseq-repl/tests/test-desktop-app-repl.sh b/.agents/skills/logseq-repl/tests/test-desktop-app-repl.sh new file mode 100644 index 0000000000..fbf8257f4a --- /dev/null +++ b/.agents/skills/logseq-repl/tests/test-desktop-app-repl.sh @@ -0,0 +1,363 @@ +#!/usr/bin/env bash +set -euo pipefail + +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKILL_DIR="$(cd "$TEST_DIR/.." && pwd)" +SKILLS_ROOT="$(cd "$SKILL_DIR/.." && pwd)" +START_SCRIPT="$SKILL_DIR/scripts/start-desktop-app-repl.sh" +CLEANUP_SCRIPT="$SKILL_DIR/scripts/cleanup-desktop-app-repl.sh" +SKILL_FILE="$SKILL_DIR/SKILL.md" +CURRENT_SKILL_NAME="$(basename "$SKILL_DIR")" +ORIGINAL_PATH="$PATH" + +# shellcheck source=./test-lib.sh +source "$TEST_DIR/test-lib.sh" + +PASS_COUNT=0 +FAIL_COUNT=0 + +create_fake_env() { + TEST_ROOT="$(mktemp -d)" + REPO_ROOT="$TEST_ROOT/repo" + BIN_DIR="$TEST_ROOT/bin" + CMD_LOG="$TEST_ROOT/commands.log" + + mkdir -p "$REPO_ROOT/static" "$BIN_DIR" + : > "$CMD_LOG" + + cat > "$BIN_DIR/yarn" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +echo "yarn $*" >> "$FAKE_CMD_LOG" + +case "${1:-}" in + watch) + echo "shadow-cljs - nREPL server started on port 8701" + echo "[:electron] Build completed." + echo "[:app] Build completed." + while true; do sleep 1; done + ;; + dev-electron-app) + echo "17:12:00.841 › Logseq App(2.0.1) Starting..." + echo "shadow-cljs - #6 ready!" + while true; do sleep 1; done + ;; + *) + echo "Unexpected yarn command: $*" >&2 + exit 1 + ;; + esac +EOF + + cat > "$BIN_DIR/npx" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +input="" +if [[ "${1:-}" == "shadow-cljs" && "${2:-}" == "cljs-repl" && "${3:-}" == "app" ]] && [[ -p /dev/stdin ]]; then + input="$(cat)" +fi + +if [[ "${1:-}" == "shadow-cljs" && "${2:-}" == "clj-eval" ]]; then + echo "npx-clj-eval $*" >> "$FAKE_CMD_LOG" + echo "shadow-cljs - connected to server" + echo "${FAKE_APP_RUNTIME_COUNT:-1}" + exit 0 +fi + +if [[ "${1:-}" == "shadow-cljs" && "${2:-}" == "cljs-repl" && "${3:-}" == "app" ]]; then + if [[ "$input" == *"(.-title js/document)"* ]]; then + echo "npx-smoke $*" >> "$FAKE_CMD_LOG" + echo "shadow-cljs - connected to server" + if [[ "${FAKE_SMOKE_TEST_FAIL:-0}" == "1" ]]; then + echo "cljs.user=> false" + echo "cljs.user=> \"Wrong\"" + echo "cljs.user=>" + exit 0 + fi + echo "cljs.user=> true" + echo "cljs.user=> \"${FAKE_SMOKE_TEST_TITLE:-Logseq}\"" + echo "cljs.user=>" + exit 0 + fi + + echo "npx-attach $*" >> "$FAKE_CMD_LOG" + echo "shadow-cljs - connected to server" + echo "cljs.user=>" + exit 0 +fi + +echo "Unexpected npx command: $*" >&2 +exit 1 +EOF + + cat > "$BIN_DIR/setsid" <<'EOF' +#!/usr/bin/env bash +exec "$@" +EOF + + chmod +x "$BIN_DIR/yarn" "$BIN_DIR/npx" "$BIN_DIR/setsid" + + export PATH="$BIN_DIR:$ORIGINAL_PATH" + export FAKE_CMD_LOG="$CMD_LOG" + export FAKE_APP_RUNTIME_COUNT="${FAKE_APP_RUNTIME_COUNT:-1}" + export FAKE_SMOKE_TEST_FAIL="${FAKE_SMOKE_TEST_FAIL:-0}" + export FAKE_SMOKE_TEST_TITLE="${FAKE_SMOKE_TEST_TITLE:-Logseq}" +} + +link_system_tool() { + local name="$1" + local target + target="$(command -v "$name")" + ln -s "$target" "$SYSTEM_BIN_DIR/$name" +} + +create_fake_env_without_setsid() { + create_fake_env + + SYSTEM_BIN_DIR="$TEST_ROOT/system-bin" + mkdir -p "$SYSTEM_BIN_DIR" + rm -f "$BIN_DIR/setsid" + + local tool + for tool in bash awk cat dirname grep mkdir nohup ps rm sleep tr; do + link_system_tool "$tool" + done + + export PATH="$BIN_DIR:$SYSTEM_BIN_DIR" +} + +cleanup_fake_env() { + if [[ -n "${REPO_ROOT:-}" && -d "$REPO_ROOT/tmp/desktop-app-repl" ]]; then + local pid_file + for pid_file in "$REPO_ROOT"/tmp/desktop-app-repl/*.pid; do + [[ -e "$pid_file" ]] || continue + local pid + pid="$(tr -d '[:space:]' < "$pid_file")" + if [[ "$pid" =~ ^[0-9]+$ ]]; then + kill -9 "$pid" 2>/dev/null || true + fi + done + fi + + PATH="$ORIGINAL_PATH" + unset FAKE_CMD_LOG FAKE_APP_RUNTIME_COUNT FAKE_SMOKE_TEST_FAIL FAKE_SMOKE_TEST_TITLE || true + + if [[ -n "${TEST_ROOT:-}" && -d "$TEST_ROOT" ]]; then + rm -rf "$TEST_ROOT" + fi +} + +scripts_exist_and_skill_is_renamed_test() { + assert_equals "logseq-repl" "$CURRENT_SKILL_NAME" + assert_file_exists "$START_SCRIPT" + assert_file_exists "$CLEANUP_SCRIPT" +} + +start_no_repl_launches_watch_and_electron_test() { + create_fake_env + trap cleanup_fake_env RETURN + + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/output.log" 2>&1 + + assert_contains "Startup complete. REPL attach skipped (--no-repl)." "$TEST_ROOT/output.log" + assert_contains "shared shadow-cljs: $REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.log" "$TEST_ROOT/output.log" + assert_contains "desktop-app: $REPO_ROOT/tmp/desktop-app-repl/desktop-electron.log" "$TEST_ROOT/output.log" + assert_contains "yarn watch" "$CMD_LOG" + assert_contains "yarn dev-electron-app" "$CMD_LOG" + assert_contains "npx-clj-eval shadow-cljs clj-eval" "$CMD_LOG" + assert_contains "npx-smoke shadow-cljs cljs-repl app" "$CMD_LOG" + + assert_file_exists "$REPO_ROOT/tmp/desktop-app-repl/shadow-watch.pid" + assert_file_exists "$REPO_ROOT/tmp/desktop-app-repl/desktop-electron.pid" + assert_file_exists "$REPO_ROOT/tmp/desktop-app-repl/shadow-watch.log" + assert_file_exists "$REPO_ROOT/tmp/desktop-app-repl/desktop-electron.log" + assert_file_exists "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid" + assert_file_exists "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.log" +} + +start_reuses_running_processes_test() { + create_fake_env + trap cleanup_fake_env RETURN + + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/first.log" 2>&1 + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/second.log" 2>&1 + + local watch_count + watch_count="$(grep -c '^yarn watch$' "$CMD_LOG")" + local electron_count + electron_count="$(grep -c '^yarn dev-electron-app$' "$CMD_LOG")" + + assert_equals "1" "$watch_count" + assert_equals "1" "$electron_count" + assert_contains "Reusing shared shadow-cljs watch" "$TEST_ROOT/second.log" + assert_contains "Reusing Desktop dev app" "$TEST_ROOT/second.log" +} + +start_fails_when_multiple_app_runtimes_exist_test() { + create_fake_env + trap cleanup_fake_env RETURN + export FAKE_APP_RUNTIME_COUNT=2 + + if bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/output.log" 2>&1; then + fail "expected start script to fail when multiple :app runtimes exist" + fi + + assert_contains "Expected exactly one live :app runtime" "$TEST_ROOT/output.log" + assert_contains "Close the browser dev app" "$TEST_ROOT/output.log" +} + +start_attaches_repl_by_default_test() { + create_fake_env + trap cleanup_fake_env RETURN + + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" < /dev/null > "$TEST_ROOT/output.log" 2>&1 + + assert_contains "Attaching CLJS REPL: npx shadow-cljs cljs-repl app" "$TEST_ROOT/output.log" + assert_contains "npx-attach shadow-cljs cljs-repl app" "$CMD_LOG" +} + +start_works_without_setsid_test() { + create_fake_env_without_setsid + trap cleanup_fake_env RETURN + + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/output.log" 2>&1 + + assert_contains "Startup complete. REPL attach skipped (--no-repl)." "$TEST_ROOT/output.log" + assert_contains "yarn watch" "$CMD_LOG" + assert_contains "yarn dev-electron-app" "$CMD_LOG" +} + +start_accepts_non_logseq_window_title_test() { + create_fake_env + trap cleanup_fake_env RETURN + export FAKE_SMOKE_TEST_TITLE="My Graph" + + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/output.log" 2>&1 + + assert_contains "Desktop renderer smoke test passed" "$TEST_ROOT/output.log" + assert_contains "npx-smoke shadow-cljs cljs-repl app" "$CMD_LOG" +} + +cleanup_stops_tracked_processes_test() { + create_fake_env + trap cleanup_fake_env RETURN + + bash "$START_SCRIPT" --repo-root "$REPO_ROOT" --no-repl > "$TEST_ROOT/start.log" 2>&1 + + local shadow_pid + shadow_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/desktop-app-repl/shadow-watch.pid")" + local electron_pid + electron_pid="$(tr -d '[:space:]' < "$REPO_ROOT/tmp/desktop-app-repl/desktop-electron.pid")" + + bash "$CLEANUP_SCRIPT" --repo-root "$REPO_ROOT" > "$TEST_ROOT/cleanup.log" 2>&1 + + if kill -0 "$shadow_pid" 2>/dev/null; then + fail "expected shadow watch pid to be stopped" + fi + + if kill -0 "$electron_pid" 2>/dev/null; then + fail "expected desktop electron pid to be stopped" + fi + + assert_not_exists "$REPO_ROOT/tmp/desktop-app-repl/shadow-watch.pid" + assert_not_exists "$REPO_ROOT/tmp/desktop-app-repl/desktop-electron.pid" + assert_not_exists "$REPO_ROOT/tmp/logseq-repl/shared-shadow-watch.pid" + assert_contains "Cleanup done." "$TEST_ROOT/cleanup.log" +} + +cleanup_does_not_signal_own_process_group_test() { + create_fake_env + trap cleanup_fake_env RETURN + + local bash_env_file fake_pid fake_pgid + fake_pid=424242 + fake_pgid=777777 + mkdir -p "$REPO_ROOT/tmp/desktop-app-repl" + echo "$fake_pid" > "$REPO_ROOT/tmp/desktop-app-repl/desktop-electron.pid" + + cat > "$BIN_DIR/ps" < "$bash_env_file" <> "$CMD_LOG" +return 0 +} +EOF + + chmod +x "$BIN_DIR/ps" + + BASH_ENV="$bash_env_file" bash "$CLEANUP_SCRIPT" --repo-root "$REPO_ROOT" > "$TEST_ROOT/cleanup-own-pgid.log" 2>&1 || true + + if grep -Eq '^kill -TERM -[0-9]+' "$CMD_LOG"; then + fail "expected cleanup not to signal its own process group" + fi +} + +help_and_docs_are_portable_test() { + local temp_dir start_help_file cleanup_help_file path_pattern + temp_dir="$(mktemp -d)" + start_help_file="$temp_dir/start-help.txt" + cleanup_help_file="$temp_dir/cleanup-help.txt" + path_pattern="$(portable_path_pattern)" + + bash "$START_SCRIPT" --help > "$start_help_file" + bash "$CLEANUP_SCRIPT" --help > "$cleanup_help_file" + + assert_not_matches "$path_pattern" "$start_help_file" + assert_not_matches "$path_pattern" "$cleanup_help_file" + assert_not_matches "$path_pattern" "$SKILL_FILE" + assert_contains "name: logseq-repl" "$SKILL_FILE" + assert_contains 'Desktop app `:app` REPL' "$SKILL_FILE" + assert_contains "start-desktop-app-repl.sh" "$SKILL_FILE" + assert_contains "start-db-worker-node-repl.sh" "$SKILL_FILE" + assert_contains "tmp/logseq-repl" "$SKILL_FILE" + assert_contains 'Run both `:app` and `:db-worker-node` together' "$SKILL_FILE" + assert_contains "close browser dev app instances" "$SKILL_FILE" + assert_contains "shared-shadow-watch.log" "$SKILL_FILE" + assert_contains "shadow.user/worker-node-repl" "$SKILL_FILE" + assert_contains "Non-interactive verification examples" "$SKILL_FILE" + assert_contains "printf '(prn {:runtime :app" "$SKILL_FILE" + assert_contains "printf '(prn {:runtime :db-worker-node" "$SKILL_FILE" + assert_contains "Cleanup both workflows" "$SKILL_FILE" + assert_not_contains_text "name: repl-workflows" "$SKILL_FILE" + + rm -rf "$temp_dir" +} + +obsolete_skill_directories_removed_test() { + assert_not_exists "$SKILLS_ROOT/desktop-app-repl" + assert_not_exists "$SKILLS_ROOT/db-worker-node-repl" +} + +run_test "scripts exist and skill is renamed" scripts_exist_and_skill_is_renamed_test +run_test "start --no-repl launches watch and electron" start_no_repl_launches_watch_and_electron_test +run_test "start reuses running processes" start_reuses_running_processes_test +run_test "start fails when multiple app runtimes exist" start_fails_when_multiple_app_runtimes_exist_test +run_test "start attaches repl by default" start_attaches_repl_by_default_test +run_test "start works without setsid" start_works_without_setsid_test +run_test "start accepts non-Logseq window title" start_accepts_non_logseq_window_title_test +run_test "cleanup stops tracked processes" cleanup_stops_tracked_processes_test +run_test "cleanup does not signal its own process group" cleanup_does_not_signal_own_process_group_test +run_test "help and docs are portable" help_and_docs_are_portable_test +run_test "obsolete skill directories removed" obsolete_skill_directories_removed_test + +echo +if [[ "$FAIL_COUNT" -gt 0 ]]; then + echo "$FAIL_COUNT test(s) failed; $PASS_COUNT passed." >&2 + exit 1 +fi + +echo "All $PASS_COUNT test(s) passed." diff --git a/.agents/skills/logseq-repl/tests/test-lib.sh b/.agents/skills/logseq-repl/tests/test-lib.sh new file mode 100644 index 0000000000..0334f54688 --- /dev/null +++ b/.agents/skills/logseq-repl/tests/test-lib.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +fail() { + echo "FAIL: $*" >&2 + return 1 +} + +assert_file_exists() { + local path="$1" + [[ -e "$path" ]] || fail "expected file to exist: $path" +} + +assert_not_exists() { + local path="$1" + [[ ! -e "$path" ]] || fail "expected path to be absent: $path" +} + +assert_contains() { + local needle="$1" + local haystack_file="$2" + if ! grep -Fq "$needle" "$haystack_file"; then + echo "Expected to find: $needle" >&2 + echo "--- file: $haystack_file ---" >&2 + cat "$haystack_file" >&2 + echo "----------------------------" >&2 + return 1 + fi +} + +assert_not_contains_text() { + local needle="$1" + local file="$2" + if grep -Fq "$needle" "$file"; then + echo "Did not expect to find: $needle" >&2 + echo "--- file: $file ---" >&2 + cat "$file" >&2 + echo "--------------------" >&2 + return 1 + fi +} + +assert_not_matches() { + local pattern="$1" + local file="$2" + if grep -Eq "$pattern" "$file"; then + echo "Did not expect regex match: $pattern" >&2 + echo "--- file: $file ---" >&2 + cat "$file" >&2 + echo "--------------------" >&2 + return 1 + fi +} + +assert_equals() { + local expected="$1" + local actual="$2" + [[ "$expected" == "$actual" ]] || fail "expected '$expected' but got '$actual'" +} + +portable_path_pattern() { + local slash='/' + local windows_drive='[A-Za-z]:\\[^[:space:]]+' + printf '%s' "${slash}Users${slash}[^[:space:]]+|${slash}home${slash}[^[:space:]]+|${windows_drive}" +} + +run_test() { + local name="$1" + local fn="$2" + + if (set -e; "$fn"); then + echo "PASS: $name" + PASS_COUNT=$((PASS_COUNT + 1)) + else + echo "FAIL: $name" >&2 + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi +} diff --git a/src/main/logseq/cli/AGENTS.md b/src/main/logseq/cli/AGENTS.md index 4f29970b7e..6e6dddb7e5 100644 --- a/src/main/logseq/cli/AGENTS.md +++ b/src/main/logseq/cli/AGENTS.md @@ -12,5 +12,5 @@ - `--profile` - performance check - Logseq-cli skill - explore cli self in agent - db-worker-node.log - logs for db-worker-node process -- db-worker-node-repl skill - Directly validate some db worker node code in the REPL +- logseq-repl skill - Directly validate some db worker node code in the REPL