From 6df4196a2aabf63faa8d6ead2de60f3c0752595b Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Fri, 15 May 2026 14:01:09 -0400 Subject: [PATCH 1/5] docs(testing): rewrite for current API and CLI-first workflow Reflect the post-#475 ("Sprout speaks Nostr. Nothing else.") API: - Drop `POST /api/events` and `/api/channels/.../threads/...` (both 404 today); list the actual bridge endpoints: `/events`, `/query`, `/count`. - Replace the `nak | curl` + `screen` dance with a CLI-first smoke test using `sprout-cli` (NIP-98 by default), `sprout-admin generate-key`, and the channel UUID extraction needed because `sprout channels create` does not echo the UUID it generates. - Move ACP to an optional section and note that sprout-mcp is being deprecated. - Update env defaults to match `sprout-relay` / `sprout-acp` / `sprout-cli` source (`SPROUT_HEALTH_PORT=8080`, `SPROUT_METRICS_PORT=9102`, `SPROUT_REQUIRE_AUTH_TOKEN=false` in dev, `SPROUT_ACP_IDLE_TIMEOUT` as the canonical timeout, etc.). - Trim troubleshooting to current symptoms. Smoke test verified end-to-end against a fresh `just relay`. Signed-off-by: Tyler Longwell --- TESTING.md | 277 ++++++++++++++++++++++++++++------------------------- 1 file changed, 146 insertions(+), 131 deletions(-) diff --git a/TESTING.md b/TESTING.md index 5e2e97fc59..15e84e2a8c 100644 --- a/TESTING.md +++ b/TESTING.md @@ -3,191 +3,206 @@ ## Automated Tests ```bash -just test-unit # unit tests — no infrastructure needed -just test # unit + integration (starts Docker if needed) +just test-unit # unit tests — no infrastructure needed +just test # unit + integration (starts Docker if needed) ``` -`just test` runs unit tests plus integration tests against Postgres, Redis, and -Typesense. It does **not** run the E2E suites in `sprout-test-client` — those -require a running relay and are marked `#[ignore]`: +`just test` runs unit tests plus integration tests against Postgres and Redis +(started via `docker compose`). Neither task runs the E2E suites in +`sprout-test-client` — those are marked `#[ignore]` and require a running relay: ```bash -# E2E tests — start the relay first, then: +# Start a relay first (see below), then: cargo test -p sprout-test-client -- --ignored ``` -Each E2E test file documents its own `RELAY_URL` / `RELAY_HTTP_URL` defaults. -See `crates/sprout-test-client/tests/` for source and per-file instructions. - --- -## Live Testing with ACP Agents - -Run AI agents against a local relay to exercise the full stack end-to-end. +## Live Local Relay -``` -User ──nak event──→ POST /api/events ──→ Relay ──WS──→ sprout-acp ──stdio──→ goose - │ - sprout-mcp-server - (send_message, etc.) -``` +The fastest way to exercise the relay end-to-end is `just relay` plus the +`sprout` CLI. The CLI signs every request with NIP-98, so you don't need +`nak` or hand-rolled `curl`. -### Prerequisites +### 1. Setup -- Docker running -- `screen` installed (macOS: built-in; Linux: `apt install screen`) -- [nak](https://github.com/fiatjaf/nak) on PATH (`brew install nak` or `go install github.com/fiatjaf/nak@latest`) -- `goose` on PATH and configured with a provider/model - -All commands below assume you're in the **repo root** (`sprout/`). +```bash +. ./bin/activate-hermit # activate pinned toolchain +cp .env.example .env # one-time +just setup # start Docker services, run migrations +``` -### 1. Build +`just reset` wipes all local data and starts over. -**Rebuild after every code change** — screen sessions run the release binary. +### 2. Build the binaries ```bash -. bin/activate-hermit -just setup # Docker services + schema + deps -cargo build --release --workspace +cargo build --release -p sprout-relay -p sprout-cli -p sprout-admin export PATH="$PWD/target/release:$PATH" ``` -To wipe everything and start fresh: `just reset` (destroys all data). +Rebuild after any code change — the steps below use the release binaries. -> **Already built?** You still need the PATH export in every new shell: -> `export PATH="$PWD/target/release:$PATH"` +### 3. Start the relay -### 2. Start the Relay +In a separate terminal (it runs in the foreground): ```bash -screen -dmS relay bash -c "cd $PWD && . .env 2>/dev/null; sprout-relay 2>&1 | tee /tmp/sprout-relay.log" +just relay # serves ws://localhost:3000 +# or, equivalently: +# cargo run --release -p sprout-relay +``` + +Verify it's up (back in your working terminal): -sleep 3 && curl -s http://localhost:3000/health # → "ok" +```bash +curl -s http://localhost:3000/health # → ok +curl -s http://localhost:8080/_readiness # → {"status":"ready"} ``` -> The relay has built-in dev defaults matching docker-compose. Sourcing `.env` -> is only needed if you've customized ports or want the `RUST_LOG` level it sets. +> Health/readiness/liveness live on a **separate port** (default `8080`, +> `SPROUT_HEALTH_PORT`) so K8s probes bypass auth middleware. The main app +> port also exposes `/health` for convenience. + +The relay starts in dev mode (`SPROUT_REQUIRE_AUTH_TOKEN=false`). The startup +log emits a WARN about this — that's expected for local testing. See the env +vars table at the bottom if you need to lock it down. -### 3. Generate Keys +### 4. Smoke test the CLI against the relay -Each agent needs a Nostr keypair. Authentication uses NIP-42 (WebSocket) and -NIP-98 Schnorr signatures (REST). +End-to-end: generate an identity, create a channel, post a message, read it +back. This is the minimum sequence an agent needs to verify a local relay. ```bash -# Agent identity -AGENT_SK=$(nak key generate) -AGENT_NSEC=$(nak encode nsec "$AGENT_SK") -AGENT_PK=$(nak key public "$AGENT_SK") - -# Human user identity (for sending tasks) -USER_SK=$(nak key generate) -USER_NSEC=$(nak encode nsec "$USER_SK") -USER_PK=$(nak key public "$USER_SK") - -echo "AGENT_PK=$AGENT_PK" -echo "USER_PK=$USER_PK" +# Generate a keypair +GEN=$(sprout-admin generate-key) +export SPROUT_PRIVATE_KEY=$(echo "$GEN" | awk '/Secret key:/ {print $3}') +PUBKEY=$(echo "$GEN" | awk '/Public key:/ {print $3}') +echo "pubkey: $PUBKEY" + +# Create a channel (the CLI generates the UUID client-side and embeds it in +# the kind:9007 event; it does NOT return the UUID in the response yet) +sprout channels create --name "smoke-$$" --type stream --visibility open + +# Find your new channel's UUID. kind:39002 (channel metadata) lists you as +# owner; the channel UUID is in the `d` tag. +CHANNEL=$(sprout channels list --member \ + | jq -r --arg pk "$PUBKEY" ' + .[] + | select(any(.tags[]; .[0]=="p" and .[1]==$pk and .[3]=="owner")) + | (.tags[] | select(.[0]=="d") | .[1])' \ + | head -1) +echo "channel: $CHANNEL" + +# Send a message and read it back +sprout messages send --channel "$CHANNEL" --content "hello from smoke test" +sprout messages get --channel "$CHANNEL" --limit 5 | jq . ``` -### 4. Create a Channel and Add the Agent +A successful run prints `{"event_id":"…","accepted":true,"message":""}` for +the send, and the message body in the `get` output. -Channels are created via signed Nostr events submitted to `POST /api/events`. +### 5. Going deeper -```bash -CHANNEL=$(python3 -c "import uuid; print(uuid.uuid4())") -echo "CHANNEL=$CHANNEL" - -# Create channel (kind:9007) -nak event --sec "$USER_NSEC" -k 9007 \ - -t h="$CHANNEL" -t name="testing" -t channel_type="stream" -t visibility="open" -c "" \ -| curl -s -X POST -H "Content-Type: application/json" -H "X-Pubkey: $USER_PK" \ - http://localhost:3000/api/events -d @- - -# Add the agent to the channel (kind:9000) -nak event --sec "$USER_NSEC" -k 9000 \ - -t h="$CHANNEL" -t p="$AGENT_PK" -c "" \ -| curl -s -X POST -H "Content-Type: application/json" -H "X-Pubkey: $USER_PK" \ - http://localhost:3000/api/events -d @- -``` +For full coverage of every CLI command (54 subcommands across 12 groups), +follow [`crates/sprout-cli/TESTING.md`](crates/sprout-cli/TESTING.md). -### 5. Launch an ACP Agent +The relay's HTTP bridge accepts three endpoints — useful if you're testing +a client other than `sprout-cli`: -```bash -screen -dmS agent bash -c " - export PATH=\"$PWD/target/release:\$PATH\" - export SPROUT_PRIVATE_KEY=\"$AGENT_NSEC\" - export SPROUT_RELAY_URL=ws://localhost:3000 - export SPROUT_ACP_RESPOND_TO=anyone - export GOOSE_MODE=auto - sprout-acp 2>&1 | tee /tmp/sprout-agent.log -" -``` +| Endpoint | Purpose | +|-----------------|------------------------------------| +| `POST /events` | Submit a signed Nostr event | +| `POST /query` | NIP-01 filter query (returns events) | +| `POST /count` | NIP-45 count query | -Wait ~10 seconds, then verify: +All three accept NIP-98 auth (recommended) or, in dev mode, an `X-Pubkey` +header fallback. There is no REST API for fetching message threads — use +`POST /query` with an `#e` filter, or `sprout messages thread`. -```bash -tail -5 /tmp/sprout-agent.log # should show "discovered N channel(s)" -``` +--- -| Variable | Required | Why | -|----------|----------|-----| -| `SPROUT_PRIVATE_KEY` | yes | Agent's `nsec1...` identity | -| `SPROUT_RELAY_URL` | no | Defaults to `ws://localhost:3000` | -| `SPROUT_ACP_RESPOND_TO` | no | Set to `anyone` for testing (default `owner-only` drops all events) | -| `GOOSE_MODE` | yes | Must be `auto` or goose hangs on permission prompts | +## ACP Harness (optional, end-to-end with a real agent) -The harness auto-discovers `sprout-mcp-server` on PATH — make sure -`target/release` is in PATH inside the screen session. +`sprout-acp` connects an ACP-speaking agent (goose, codex, claude code, +sprout-agent) to the relay. The harness listens for events, drives the +agent over stdio, and the agent replies through MCP tools. -### 6. Send a Task and Check Results +> The `sprout-mcp` server is being deprecated in favour of direct CLI/relay +> integration. Keep it in mind if you're poking at the ACP code, but new +> tests should not depend on it. + +Minimum recipe — assumes the relay from step 3 is running: ```bash -# @mention the agent (kind:9 with p-tag) and capture the event ID -EVENT_ID=$(nak event --sec "$USER_NSEC" -k 9 \ - -t h="$CHANNEL" -t p="$AGENT_PK" -c "Hey, say hello!" \ -| curl -s -X POST -H "Content-Type: application/json" -H "X-Pubkey: $USER_PK" \ - http://localhost:3000/api/events -d @- \ -| python3 -c "import json,sys; print(json.load(sys.stdin)['event_id'])") - -echo "Sent event: $EVENT_ID" -``` +cargo build --release -p sprout-acp -p sprout-mcp +export PATH="$PWD/target/release:$PATH" -Agent turns typically take 10–90 seconds depending on the task and model. The -ACP log goes quiet during turns — this is normal (agent I/O goes through the -stdio pipe). Check the relay for the agent's reply: +AGENT_GEN=$(sprout-admin generate-key) +export SPROUT_PRIVATE_KEY=$(echo "$AGENT_GEN" | awk '/Secret key:/ {print $3}') +export SPROUT_RELAY_URL=ws://localhost:3000 +export SPROUT_ACP_RESPOND_TO=anyone # default is owner-only; opens the gate for testing +export GOOSE_MODE=auto # must be 'auto' or goose hangs on prompts -```bash -# Agent replies are threaded — use the thread endpoint -curl -s -H "X-Pubkey: $USER_PK" \ - "http://localhost:3000/api/channels/$CHANNEL/threads/$EVENT_ID" \ -| python3 -c " -import json, sys -data = json.load(sys.stdin) -for r in data.get('replies', []): - print(f'{r[\"pubkey\"][:12]}... {r[\"content\"][:200]}') -" +sprout-acp # foreground; logs to stdout ``` -### 7. Teardown +The justfile also ships `just goose key="$AGENT_NSEC"` (foreground) and +`just goose-bg key="$AGENT_NSEC"` (background screen session) which set the +same env. See `crates/sprout-acp/README.md` for parallel agents, heartbeats, +respond-to gates, and forum subscriptions. + +To send the agent a task, @mention its pubkey from another identity: ```bash -screen -S agent -X quit -screen -S relay -X quit -docker compose down # stop services, keep data -# or: just reset # stop services, destroy all data +sprout messages send --channel "$CHANNEL" \ + --content "Hey agent, say hello." \ + --mention "$AGENT_PUBKEY" ``` +Agent turns typically take 10–90s. Replies appear as kind:9 threaded events; +`sprout messages thread --channel --event ` fetches them. + +--- + +## Configuration reference + +The relay reads all configuration from environment variables. Defaults work +out of the box with `docker compose up`. Common overrides: + +| Variable | Default | Notes | +|-----------------------------------|-----------------------------|-------| +| `SPROUT_BIND_ADDR` | `0.0.0.0:3000` | Main app port | +| `SPROUT_HEALTH_PORT` | `8080` | `/_liveness`, `/_readiness` | +| `SPROUT_METRICS_PORT` | `9102` | Prometheus `/metrics` | +| `RELAY_URL` | `ws://localhost:3000` | Advertised in NIP-11 / NIP-42 challenges. **Note: no `SPROUT_` prefix.** | +| `DATABASE_URL` | `postgres://sprout:sprout_dev@localhost:5432/sprout` | | +| `REDIS_URL` | `redis://localhost:6379` | | +| `TYPESENSE_URL` | `http://localhost:8108` | | +| `SPROUT_REQUIRE_AUTH_TOKEN` | `false` | When true, REST requires NIP-98 (no `X-Pubkey` fallback) | +| `SPROUT_REQUIRE_RELAY_MEMBERSHIP` | `false` | When true, only pubkeys in `relay_members` can connect | +| `RELAY_OWNER_PUBKEY` | unset | Bootstrapped as `owner` in `relay_members` at first start | +| `SPROUT_ALLOW_NIP_OA_AUTH` | `false` | Enable NIP-OA owner attestation for membership | + +CLI-side, only two matter for testing: + +| Variable | Default | Notes | +|-------------------------|--------------------------|-------| +| `SPROUT_RELAY_URL` | `http://localhost:3000` | CLI relay base; accepts `ws(s)://` and normalises | +| `SPROUT_PRIVATE_KEY` | — (**required**) | `nsec1…` or 64-char hex | +| `SPROUT_AUTH_TAG` | unset | Optional NIP-OA owner attestation JSON | + --- ## Troubleshooting | Symptom | Cause | Fix | |---------|-------|-----| -| Testing stale code | Forgot to rebuild | `cargo build --release --workspace` after every change | -| `all events will be dropped` | Default `respond-to=owner-only` | Set `SPROUT_ACP_RESPOND_TO=anyone` | -| Agent hangs forever | `GOOSE_MODE` not set | Must be `auto` | -| Env vars not reaching agent | Unexported shell variables | All exports go inside `bash -c '...'` | -| `discovered 0 channel(s)` | Agent not a member | Create channel + add agent **before** launching | -| Agent reacts but no reply | Normal — goose is working | Wait 30–90s; check thread endpoint for replies | -| ACP log stops after startup | Normal — agent I/O is stdio | Check relay messages for evidence | -| Relay won't start | Port 3000 in use or DB stale | Kill old processes; `just reset` for clean slate | -| Need more ACP debug output | Default log level is info | Add `export RUST_LOG=sprout_acp=debug` to the screen command | +| `relay error 500` or `400: restricted: not a channel member` after a code change | Stale binary | Rebuild and re-export `PATH`; or `cargo run` directly | +| `Address already in use (os error 48)` on relay start | Another relay (or stale process) holding `:3000` / `:8080` / `:9102` | `lsof -iTCP:3000 -sTCP:LISTEN`; kill the offender, or override `SPROUT_BIND_ADDR` / `SPROUT_HEALTH_PORT` / `SPROUT_METRICS_PORT` | +| `auth_error: SPROUT_PRIVATE_KEY is required` | Env not exported into the CLI's shell | `export SPROUT_PRIVATE_KEY=...` (or pass `--private-key`) | +| `auth-required: verification failed` on a closed relay | NIP-OA attestation needed | Set `SPROUT_AUTH_TAG` to the owner-issued JSON, or relax `SPROUT_REQUIRE_RELAY_MEMBERSHIP` | +| `channels list` empty after `channels create` | The CLI doesn't echo the channel UUID; use the filter shown in step 4 | Or `POST /query` with `{"kinds":[39002]}` | +| ACP agent ignores all events | `SPROUT_ACP_RESPOND_TO=owner-only` (default) with no owner configured | Set `SPROUT_ACP_RESPOND_TO=anyone` for testing | +| `GOOSE_MODE` warning, agent hangs | Not set | `export GOOSE_MODE=auto` | +| Tests pass locally but CI fails | Forgot to run `just ci` | `just ci` runs the gate (fmt, clippy, unit tests, desktop/web builds) | From 37282bf4b46ae7efb61b6e676016aad3a08ba005 Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Fri, 15 May 2026 14:01:09 -0400 Subject: [PATCH 2/5] docs(testing): patch sharp edges found in cold-reader walkthrough MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Step 3: add port-collision callout (Sprout Desktop on :3000/:8080) and flag that overriding relay ports requires $SPROUT_RELAY_URL on the CLI. - Step 3: warn cold readers to `unset SPROUT_AUTH_TAG` if their shell has it set from another relay's config. - ACP recipe: add `channels add-member $AGENT_PUBKEY` BEFORE starting sprout-acp. Without this the agent boots to 'discovered 0 channel(s)' / 'no channel subscriptions resolved — agent will sit idle' and never picks up mentions. Verified end-to-end with goose against a local relay. - ACP recipe: capture sender SK first so the reader can switch back to it to send the @mention. Note that the agent log is quiet during turns; use `sprout messages get` to confirm the reply. - Troubleshooting: new row for the 'discovered 0 channels / sit idle' case. Signed-off-by: Tyler Longwell --- TESTING.md | 61 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/TESTING.md b/TESTING.md index 15e84e2a8c..dd668beaa6 100644 --- a/TESTING.md +++ b/TESTING.md @@ -68,6 +68,19 @@ The relay starts in dev mode (`SPROUT_REQUIRE_AUTH_TOKEN=false`). The startup log emits a WARN about this — that's expected for local testing. See the env vars table at the bottom if you need to lock it down. +> **Already running Sprout Desktop (or another relay) on `:3000` / `:8080`?** +> Override the relay's three ports and point the CLI at the new main port: +> ```bash +> export SPROUT_BIND_ADDR=0.0.0.0:3030 SPROUT_HEALTH_PORT=8088 SPROUT_METRICS_PORT=9202 +> export RELAY_URL=ws://localhost:3030 # for the relay (NIP-42 advertisement) +> export SPROUT_RELAY_URL=http://localhost:3030 # for the CLI in steps 4+ +> ``` + +> **Heads up:** if your shell already has `SPROUT_AUTH_TAG` set (e.g. from a +> staging relay config), `unset SPROUT_AUTH_TAG` before testing. The local +> dev relay tolerates it, but a stale tag will trip you up the moment you +> point the CLI at a membership-gated relay. + ### 4. Smoke test the CLI against the relay End-to-end: generate an identity, create a channel, post a message, read it @@ -132,36 +145,63 @@ agent over stdio, and the agent replies through MCP tools. > integration. Keep it in mind if you're poking at the ACP code, but new > tests should not depend on it. -Minimum recipe — assumes the relay from step 3 is running: +Minimum recipe — assumes the relay from step 3 is running and the channel +`$CHANNEL` from step 4 still exists. The agent identity must be **different** +from the sender identity (`SPROUT_ACP_RESPOND_TO=anyone` still skips events +the agent signed itself). ```bash cargo build --release -p sprout-acp -p sprout-mcp export PATH="$PWD/target/release:$PATH" +# 1. Save your sender identity from step 4 — you'll need it to @mention the agent +SENDER_SK="$SPROUT_PRIVATE_KEY" + +# 2. Mint a fresh agent identity and capture its pubkey AGENT_GEN=$(sprout-admin generate-key) -export SPROUT_PRIVATE_KEY=$(echo "$AGENT_GEN" | awk '/Secret key:/ {print $3}') -export SPROUT_RELAY_URL=ws://localhost:3000 -export SPROUT_ACP_RESPOND_TO=anyone # default is owner-only; opens the gate for testing -export GOOSE_MODE=auto # must be 'auto' or goose hangs on prompts +AGENT_SK=$(echo "$AGENT_GEN" | awk '/Secret key:/ {print $3}') +AGENT_PUBKEY=$(echo "$AGENT_GEN" | awk '/Public key:/ {print $3}') + +# 3. Add the agent as a member of $CHANNEL — still using the sender identity. +# Skip this and the agent boots to "discovered 0 channel(s) → agent will +# sit idle" and silently ignores every mention. +sprout channels add-member --channel "$CHANNEL" --pubkey "$AGENT_PUBKEY" --role member -sprout-acp # foreground; logs to stdout +# 4. Switch to the agent identity and start it +export SPROUT_PRIVATE_KEY="$AGENT_SK" +export SPROUT_RELAY_URL=ws://localhost:3000 # or :3030 if you overrode ports in step 3 +export SPROUT_ACP_RESPOND_TO=anyone # default is owner-only; opens the gate for testing +export GOOSE_MODE=auto # must be 'auto' or goose hangs on prompts + +sprout-acp # foreground; logs to stdout (run in a separate terminal) ``` +If you started the agent before adding it to the channel, just run the +`add-member` afterwards — it picks up the membership notification live and +subscribes without restart (`membership notification: subscribing to new channel …`). + The justfile also ships `just goose key="$AGENT_NSEC"` (foreground) and `just goose-bg key="$AGENT_NSEC"` (background screen session) which set the same env. See `crates/sprout-acp/README.md` for parallel agents, heartbeats, respond-to gates, and forum subscriptions. -To send the agent a task, @mention its pubkey from another identity: +Send the agent a task — switch your shell back to the **sender** identity +from step 4 and @mention the agent: ```bash +export SPROUT_PRIVATE_KEY=$SENDER_SK # the key from step 4 sprout messages send --channel "$CHANNEL" \ - --content "Hey agent, say hello." \ + --content "Hey agent, reply PONG only." \ --mention "$AGENT_PUBKEY" + +# Wait 10–90s, then read the channel — the agent's reply is a kind:9 from +# AGENT_PUBKEY. The current ACP build is quiet on stdout during a turn, so +# `sprout messages get` is how you confirm it ran. +sprout messages get --channel "$CHANNEL" --limit 5 | jq '.[] | {pubkey, content}' ``` -Agent turns typically take 10–90s. Replies appear as kind:9 threaded events; -`sprout messages thread --channel --event ` fetches them. +Replies are kind:9 in the same channel; `sprout messages thread --channel +--event ` fetches the reply chain for a specific mention. --- @@ -204,5 +244,6 @@ CLI-side, only two matter for testing: | `auth-required: verification failed` on a closed relay | NIP-OA attestation needed | Set `SPROUT_AUTH_TAG` to the owner-issued JSON, or relax `SPROUT_REQUIRE_RELAY_MEMBERSHIP` | | `channels list` empty after `channels create` | The CLI doesn't echo the channel UUID; use the filter shown in step 4 | Or `POST /query` with `{"kinds":[39002]}` | | ACP agent ignores all events | `SPROUT_ACP_RESPOND_TO=owner-only` (default) with no owner configured | Set `SPROUT_ACP_RESPOND_TO=anyone` for testing | +| ACP logs `discovered 0 channel(s)` / `no channel subscriptions resolved` | Agent identity isn't a member of any channel | `sprout channels add-member --channel "$CHANNEL" --pubkey "$AGENT_PUBKEY" --role member` from another identity | | `GOOSE_MODE` warning, agent hangs | Not set | `export GOOSE_MODE=auto` | | Tests pass locally but CI fails | Forgot to run `just ci` | `just ci` runs the gate (fmt, clippy, unit tests, desktop/web builds) | From 9342d188d215f1c39fbb5dc9c2044fd77fcb2ab4 Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Fri, 15 May 2026 14:01:09 -0400 Subject: [PATCH 3/5] docs(testing): address Max's cold-reader review Findings from Max's second-pair-of-eyes pass on commit 0a0b3a6: 1. `just relay` is a DEBUG build (verified: `cargo run -p sprout-relay` with no --release). Doc said 'build release binaries' then 'just relay or cargo run --release', which contradicts itself. Switch the primary recommendation to running the `sprout-relay` release binary directly (already on PATH from step 2). Mention `just relay-release` as the matching recipe for anyone who wants a just target. 2. Port-override callout used to tell readers to switch ports, but the verify snippet right after still hardcoded :3000/:8080. A cold reader pasting the verify commands would hit Sprout Desktop's relay instead. Move the verify curls into the callout block on the overridden ports, and explicitly say later snippets assume defaults. 3. ACP recipe used to re-export `SPROUT_RELAY_URL=ws://localhost:3000 # or :3030 if you overrode...` which was paste-and-pray. Reworded to call out that sprout-acp wants `ws://` (not `http://`), and that the reader should match whatever step 3 used. 4. Added a 'using a different ACP agent?' note pointing out the recipe assumes `goose` is installed and configured (`goose --version`) and showing the env vars to swap for codex / claude code / sprout-agent. 5. `os error 48` is macOS-specific; Linux is `os error 98`. Make the troubleshooting row OS-agnostic. Signed-off-by: Tyler Longwell --- TESTING.md | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/TESTING.md b/TESTING.md index dd668beaa6..8a11672eb6 100644 --- a/TESTING.md +++ b/TESTING.md @@ -20,9 +20,10 @@ cargo test -p sprout-test-client -- --ignored ## Live Local Relay -The fastest way to exercise the relay end-to-end is `just relay` plus the -`sprout` CLI. The CLI signs every request with NIP-98, so you don't need -`nak` or hand-rolled `curl`. +The fastest way to exercise the relay end-to-end is to build the release +binaries once, run `sprout-relay`, and drive it with the `sprout` CLI. The +CLI signs every request with NIP-98, so you don't need `nak` or hand-rolled +`curl`. ### 1. Setup @@ -48,9 +49,12 @@ Rebuild after any code change — the steps below use the release binaries. In a separate terminal (it runs in the foreground): ```bash -just relay # serves ws://localhost:3000 -# or, equivalently: -# cargo run --release -p sprout-relay +sprout-relay # release binary from step 2, serves ws://localhost:3000 +# alternatives: +# cargo run --release -p sprout-relay # rebuild + run in release +# just relay # DEBUG build — fast to launch on a hot cache, +# # but mismatched if step 2 left you on release. +# # Use `just relay-release` if you want the recipe. ``` Verify it's up (back in your working terminal): @@ -69,12 +73,19 @@ log emits a WARN about this — that's expected for local testing. See the env vars table at the bottom if you need to lock it down. > **Already running Sprout Desktop (or another relay) on `:3000` / `:8080`?** -> Override the relay's three ports and point the CLI at the new main port: +> Export these *before* launching the relay, and use the matching verify +> commands instead of the ones above: > ```bash > export SPROUT_BIND_ADDR=0.0.0.0:3030 SPROUT_HEALTH_PORT=8088 SPROUT_METRICS_PORT=9202 -> export RELAY_URL=ws://localhost:3030 # for the relay (NIP-42 advertisement) -> export SPROUT_RELAY_URL=http://localhost:3030 # for the CLI in steps 4+ +> export RELAY_URL=ws://localhost:3030 # advertised in NIP-42 challenges +> export SPROUT_RELAY_URL=http://localhost:3030 # CLI target for steps 4+ +> # verify on the overridden ports: +> curl -s http://localhost:3030/health # → ok +> curl -s http://localhost:8088/_readiness # → {"status":"ready"} > ``` +> Every subsequent snippet in this doc assumes the defaults — when you see +> `localhost:3000` / `:8080`, mentally substitute your overrides, or the CLI +> will end up talking to Sprout Desktop's relay. > **Heads up:** if your shell already has `SPROUT_AUTH_TAG` set (e.g. from a > staging relay config), `unset SPROUT_AUTH_TAG` before testing. The local @@ -167,15 +178,23 @@ AGENT_PUBKEY=$(echo "$AGENT_GEN" | awk '/Public key:/ {print $3}') # sit idle" and silently ignores every mention. sprout channels add-member --channel "$CHANNEL" --pubkey "$AGENT_PUBKEY" --role member -# 4. Switch to the agent identity and start it +# 4. Switch to the agent identity and start it. +# sprout-acp wants ws:// (not http://). If you set SPROUT_RELAY_URL to an +# http:// URL in step 3, set the ws:// equivalent here — same host/port. export SPROUT_PRIVATE_KEY="$AGENT_SK" -export SPROUT_RELAY_URL=ws://localhost:3000 # or :3030 if you overrode ports in step 3 +export SPROUT_RELAY_URL=ws://localhost:3000 # match step 3 (e.g. ws://localhost:3030 if overridden) export SPROUT_ACP_RESPOND_TO=anyone # default is owner-only; opens the gate for testing export GOOSE_MODE=auto # must be 'auto' or goose hangs on prompts sprout-acp # foreground; logs to stdout (run in a separate terminal) ``` +> **Using a different ACP agent?** The default recipe assumes `goose` is on +> `$PATH` and configured (`goose --version` should print). For codex / claude +> code / sprout-agent, set `SPROUT_ACP_AGENT_COMMAND` and `SPROUT_ACP_AGENT_ARGS` +> accordingly — see `crates/sprout-acp/README.md`. Without these, sprout-acp +> will fail to spawn the agent subprocess on startup. + If you started the agent before adding it to the channel, just run the `add-member` afterwards — it picks up the membership notification live and subscribes without restart (`membership notification: subscribing to new channel …`). @@ -239,7 +258,7 @@ CLI-side, only two matter for testing: | Symptom | Cause | Fix | |---------|-------|-----| | `relay error 500` or `400: restricted: not a channel member` after a code change | Stale binary | Rebuild and re-export `PATH`; or `cargo run` directly | -| `Address already in use (os error 48)` on relay start | Another relay (or stale process) holding `:3000` / `:8080` / `:9102` | `lsof -iTCP:3000 -sTCP:LISTEN`; kill the offender, or override `SPROUT_BIND_ADDR` / `SPROUT_HEALTH_PORT` / `SPROUT_METRICS_PORT` | +| `Address already in use` on relay start (os error 48 on macOS, 98 on Linux) | Another relay (or stale process) holding `:3000` / `:8080` / `:9102` | `lsof -iTCP:3000 -sTCP:LISTEN`; kill the offender, or use the port-override block in step 3 | | `auth_error: SPROUT_PRIVATE_KEY is required` | Env not exported into the CLI's shell | `export SPROUT_PRIVATE_KEY=...` (or pass `--private-key`) | | `auth-required: verification failed` on a closed relay | NIP-OA attestation needed | Set `SPROUT_AUTH_TAG` to the owner-issued JSON, or relax `SPROUT_REQUIRE_RELAY_MEMBERSHIP` | | `channels list` empty after `channels create` | The CLI doesn't echo the channel UUID; use the filter shown in step 4 | Or `POST /query` with `{"kinds":[39002]}` | From 56c5eea0a15452da04c67f758947eeed3c837a0a Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Fri, 15 May 2026 14:01:10 -0400 Subject: [PATCH 4/5] docs(testing): address Mari's cold-reader review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Findings from Mari's pass on commit 16454af (third pair of eyes after my own walkthrough and Max's review): 1. Port-override callout used to lump relay-terminal vars (SPROUT_BIND_ADDR, SPROUT_HEALTH_PORT, SPROUT_METRICS_PORT, RELAY_URL) together with the working-terminal var (SPROUT_RELAY_URL). Step 3 explicitly tells the reader 'separate terminal', but the callout didn't reflect that split, forcing readers to consciously re-export SPROUT_RELAY_URL in the CLI shell or accidentally talk to the wrong relay. Restructured as 'in the relay terminal' / 'in your working terminal' blocks so the division is obvious. Also called out :9102 since metrics is the third collision surface (Mari's collision was actually on metrics first). 2. ACP manual recipe now sets SPROUT_ACP_MCP_COMMAND explicitly to the built release binary. The default value 'sprout-mcp-server' relies on step 2's PATH export, which doesn't survive a new shell. The `just goose` recipe already does this — manual recipe should match. Signed-off-by: Tyler Longwell --- TESTING.md | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/TESTING.md b/TESTING.md index 8a11672eb6..4e6e6687e4 100644 --- a/TESTING.md +++ b/TESTING.md @@ -72,20 +72,31 @@ The relay starts in dev mode (`SPROUT_REQUIRE_AUTH_TOKEN=false`). The startup log emits a WARN about this — that's expected for local testing. See the env vars table at the bottom if you need to lock it down. -> **Already running Sprout Desktop (or another relay) on `:3000` / `:8080`?** -> Export these *before* launching the relay, and use the matching verify -> commands instead of the ones above: +> **Already running Sprout Desktop (or another relay) on `:3000` / `:8080` / +> `:9102`?** Sprout binds three ports — main, health, metrics — and any of +> them can collide. Use a separate terminal per role and export the right +> vars in each: +> +> **In the relay terminal** (before launching `sprout-relay`): > ```bash -> export SPROUT_BIND_ADDR=0.0.0.0:3030 SPROUT_HEALTH_PORT=8088 SPROUT_METRICS_PORT=9202 -> export RELAY_URL=ws://localhost:3030 # advertised in NIP-42 challenges -> export SPROUT_RELAY_URL=http://localhost:3030 # CLI target for steps 4+ -> # verify on the overridden ports: +> export SPROUT_BIND_ADDR=0.0.0.0:3030 +> export SPROUT_HEALTH_PORT=8088 +> export SPROUT_METRICS_PORT=9202 +> export RELAY_URL=ws://localhost:3030 # advertised in NIP-42 challenges +> sprout-relay +> ``` +> +> **In your working / CLI terminal** (for steps 4+ and the ACP harness): +> ```bash +> export SPROUT_RELAY_URL=http://localhost:3030 # CLI target +> # verify the relay on the overridden ports: > curl -s http://localhost:3030/health # → ok > curl -s http://localhost:8088/_readiness # → {"status":"ready"} > ``` -> Every subsequent snippet in this doc assumes the defaults — when you see -> `localhost:3000` / `:8080`, mentally substitute your overrides, or the CLI -> will end up talking to Sprout Desktop's relay. +> +> Every snippet later in this doc shows the defaults. When you see +> `localhost:3000` / `:8080` in a code block, mentally substitute your +> overrides — or the CLI will end up talking to Sprout Desktop's relay. > **Heads up:** if your shell already has `SPROUT_AUTH_TAG` set (e.g. from a > staging relay config), `unset SPROUT_AUTH_TAG` before testing. The local @@ -184,6 +195,7 @@ sprout channels add-member --channel "$CHANNEL" --pubkey "$AGENT_PUBKEY" --role export SPROUT_PRIVATE_KEY="$AGENT_SK" export SPROUT_RELAY_URL=ws://localhost:3000 # match step 3 (e.g. ws://localhost:3030 if overridden) export SPROUT_ACP_RESPOND_TO=anyone # default is owner-only; opens the gate for testing +export SPROUT_ACP_MCP_COMMAND="$PWD/target/release/sprout-mcp-server" # explicit path beats $PATH export GOOSE_MODE=auto # must be 'auto' or goose hangs on prompts sprout-acp # foreground; logs to stdout (run in a separate terminal) From cd97c5bd0a571575e0a994fa032eeefe9c80b50c Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Fri, 15 May 2026 14:01:10 -0400 Subject: [PATCH 5/5] docs(testing): address Quinn's cold-reader review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - §1: callout for Sprout Desktop's shared Docker stack (sprout-postgres / -redis / -typesense reuse Desktop's containers and DB; just reset wipes Desktop's data along with yours). - §1: stronger stale-env scrub block — unset SPROUT_AUTH_TAG / SPROUT_RELAY_URL / SPROUT_PRIVATE_KEY. A stale SPROUT_AUTH_TAG fails the local dev relay with auth_error (it is not tolerated, contrary to the old phrasing). - §3: ignore just setup's 'Next steps' banner (it still prints 'just relay' / debug; doc prefers 'sprout-relay' / release). - §3: explicit teardown note so the next reviewer doesn't collide on the override ports. - §4: smoke snippet now also exercises 'sprout messages thread' so all five core message commands are covered in one block. - §5: optional RUST_LOG=sprout_acp=debug,sprout_mcp=debug hint for cold readers who want visible turn boundaries. - Troubleshooting: widened the 'Address already in use' row (read the panic, lsof all three ports, handle the alt-port collision case) and added a new row for the stale SPROUT_AUTH_TAG failure. Signed-off-by: Tyler Longwell --- TESTING.md | 51 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/TESTING.md b/TESTING.md index 4e6e6687e4..95ba49d40d 100644 --- a/TESTING.md +++ b/TESTING.md @@ -33,7 +33,28 @@ cp .env.example .env # one-time just setup # start Docker services, run migrations ``` -`just reset` wipes all local data and starts over. +> **Already running Sprout Desktop?** Desktop uses the same Docker container +> names (`sprout-postgres`, `sprout-redis`, `sprout-typesense`) and the same +> default ports (`:5432`, `:6379`, `:8108`). `just setup` will reuse those +> services, so **your test relay writes into Desktop's database**. That's +> fine for read/write smoke tests, but: `just reset` wipes Desktop's data +> along with yours. If you need isolation, stop Desktop first or run the +> dev stack on a different Compose project +> (`COMPOSE_PROJECT_NAME=sprout-dev docker compose …`). + +`just reset` wipes all local data and starts over — **including Sprout +Desktop's data** if its services are sharing your dev stack (see callout +above). + +> **Heads up — scrub stale env first.** If your shell inherits any of +> `SPROUT_AUTH_TAG`, `SPROUT_RELAY_URL`, or `SPROUT_PRIVATE_KEY` from a +> prior session (or a staging config), `unset` them before continuing. +> A stale `SPROUT_AUTH_TAG` fails the **local dev relay** with +> `auth_error: signature verification failed` on the first CLI write — +> it is *not* tolerated. +> ```bash +> unset SPROUT_AUTH_TAG SPROUT_RELAY_URL SPROUT_PRIVATE_KEY +> ``` ### 2. Build the binaries @@ -98,10 +119,14 @@ vars table at the bottom if you need to lock it down. > `localhost:3000` / `:8080` in a code block, mentally substitute your > overrides — or the CLI will end up talking to Sprout Desktop's relay. -> **Heads up:** if your shell already has `SPROUT_AUTH_TAG` set (e.g. from a -> staging relay config), `unset SPROUT_AUTH_TAG` before testing. The local -> dev relay tolerates it, but a stale tag will trip you up the moment you -> point the CLI at a membership-gated relay. +> **Ignore `just setup`'s "Next steps" banner.** It still prints +> `just relay` (a debug build). Use `sprout-relay` from step 2 here — +> step 2 already built the release binary. + +When you're done, stop the relay (Ctrl-C in its terminal). If it's +backgrounded or you lost the terminal: `pkill -f sprout-relay`. Leaving +it running will collide with the next reviewer who follows this doc on +the same machine. ### 4. Smoke test the CLI against the relay @@ -130,12 +155,17 @@ CHANNEL=$(sprout channels list --member \ echo "channel: $CHANNEL" # Send a message and read it back -sprout messages send --channel "$CHANNEL" --content "hello from smoke test" +SEND=$(sprout messages send --channel "$CHANNEL" --content "hello from smoke test") +EVENT_ID=$(echo "$SEND" | jq -r '.event_id') sprout messages get --channel "$CHANNEL" --limit 5 | jq . + +# Fetch the reply chain for a specific message (empty array on a leaf — that's fine) +sprout messages thread --channel "$CHANNEL" --event "$EVENT_ID" | jq . ``` A successful run prints `{"event_id":"…","accepted":true,"message":""}` for -the send, and the message body in the `get` output. +the send, and the message body in the `get` output. `thread` returns `[]` +for a leaf message — populated only after a reply comes in (see §5). ### 5. Going deeper @@ -199,6 +229,10 @@ export SPROUT_ACP_MCP_COMMAND="$PWD/target/release/sprout-mcp-server" # explici export GOOSE_MODE=auto # must be 'auto' or goose hangs on prompts sprout-acp # foreground; logs to stdout (run in a separate terminal) + +# Optional: turn on per-turn tracing if the default log is too quiet. +# Both crates honour RUST_LOG via tracing_subscriber's EnvFilter. +# RUST_LOG=sprout_acp=debug,sprout_mcp=debug sprout-acp ``` > **Using a different ACP agent?** The default recipe assumes `goose` is on @@ -270,8 +304,9 @@ CLI-side, only two matter for testing: | Symptom | Cause | Fix | |---------|-------|-----| | `relay error 500` or `400: restricted: not a channel member` after a code change | Stale binary | Rebuild and re-export `PATH`; or `cargo run` directly | -| `Address already in use` on relay start (os error 48 on macOS, 98 on Linux) | Another relay (or stale process) holding `:3000` / `:8080` / `:9102` | `lsof -iTCP:3000 -sTCP:LISTEN`; kill the offender, or use the port-override block in step 3 | +| `Address already in use` on relay start (os error 48 on macOS, 98 on Linux) | Another relay (or stale process) holding `:3000` / `:8080` / `:9102` (or your override ports) | The panic line names the failing port — read it first. Then `lsof -iTCP:3000,8080,9102 -sTCP:LISTEN` (or your override equivalents). Kill the offender (`pkill -f sprout-relay`) or use the port-override block in step 3. If you already overrode and *still* collide, a prior reviewer left a relay running on the same alt ports — kill it or pick fresh ports | | `auth_error: SPROUT_PRIVATE_KEY is required` | Env not exported into the CLI's shell | `export SPROUT_PRIVATE_KEY=...` (or pass `--private-key`) | +| `auth_error: SPROUT_AUTH_TAG verification failed … signature verification failed` | A stale `SPROUT_AUTH_TAG` inherited from a parent shell. The local dev relay rejects it. | `unset SPROUT_AUTH_TAG` (see the scrub block in step 1) | | `auth-required: verification failed` on a closed relay | NIP-OA attestation needed | Set `SPROUT_AUTH_TAG` to the owner-issued JSON, or relax `SPROUT_REQUIRE_RELAY_MEMBERSHIP` | | `channels list` empty after `channels create` | The CLI doesn't echo the channel UUID; use the filter shown in step 4 | Or `POST /query` with `{"kinds":[39002]}` | | ACP agent ignores all events | `SPROUT_ACP_RESPOND_TO=owner-only` (default) with no owner configured | Set `SPROUT_ACP_RESPOND_TO=anyone` for testing |