Files
buzz/crates/sprout-cli/TESTING.md

535 lines
15 KiB
Markdown

# sprout-cli Live Testing Guide
Manual testing runbook for verifying every CLI command against a local relay.
An agent or developer follows this step by step, running each command and
checking the output.
---
## 1. Prerequisites
Docker services running and healthy:
```bash
docker compose ps
# sprout-postgres healthy
# sprout-redis healthy
# sprout-typesense healthy
```
If not running: `./scripts/dev-setup.sh` from the repo root.
Tools: `jq`, `curl`, Rust toolchain.
---
## 2. Build the CLI
```bash
cargo build -p sprout-cli
```
Use `cargo run -p sprout-cli --` or the built binary at `target/debug/sprout`.
---
## 3. Start the Relay
In a separate terminal:
```bash
cd REPOS/sprout-nostr
set -a && source .env && set +a
cargo run -p sprout-relay
```
Verify:
```bash
curl -s http://localhost:3000/_liveness
# "ok" or 200 status
```
The `.env` should have `SPROUT_REQUIRE_AUTH_TOKEN=false` for local dev.
---
## 4. Mint Test Credentials
### Option A: sprout-admin (full scopes including admin)
This mints a token with all CLI-relevant scopes (including `admin:channels`)
via direct DB access. Use this for testing admin operations (archive,
delete-channel, add/remove-channel-member).
```bash
DATABASE_URL=postgres://sprout:sprout_dev@localhost:5432/sprout \
cargo run -p sprout-admin -- mint-token \
--name "cli-test" \
--scopes "messages:read,messages:write,channels:read,channels:write,users:read,users:write,files:read,files:write,admin:channels"
```
This generates a keypair and prints:
- **Private key (nsec)** — save for `SPROUT_PRIVATE_KEY` testing
Export:
```bash
export SPROUT_RELAY_URL="http://localhost:3000"
export SPROUT_PRIVATE_KEY="nsec1..." # from the mint output
```
### Scope reference
| Scope | Self-mintable | Needed for |
|-------|:---:|------------|
| `messages:read` | ✅ | `messages get`, `messages thread`, `messages search`, `feed get` |
| `messages:write` | ✅ | `messages send`, `messages edit`, `messages delete`, `reactions`, `messages vote` |
| `channels:read` | ✅ | `channels list`, `channels get`, `channels members` |
| `channels:write` | ✅ | `channels create`, `channels update`, `channels join`, `channels leave`, `channels topic`, `channels purpose` |
| `users:read` | ✅ | `users get`, `users presence` |
| `users:write` | ✅ | `users set-profile`, `users set-presence` |
| `files:read` | ✅ | — |
| `files:write` | ✅ | — |
| `admin:channels` | ❌ | `channels archive`, `channels unarchive`, `channels delete`, `channels add-member`, `channels remove-member` |
---
## 5. Unit Tests
```bash
cargo test -p sprout-cli
# Expected: see cargo test -p sprout-cli for current count
cargo clippy -p sprout-cli -- -D warnings
# Expected: zero warnings
```
---
## 6. Live Testing — Command by Command
Run each command, verify exit code 0 and check output. Most commands
return JSON (pipe through `jq .` to validate). Commands are ordered so
earlier ones create resources that later ones need.
### 6.1 Channels
```bash
# channels create (stream)
sprout channels create --name "test-stream" --type stream --visibility open \
--description "CLI test channel" | jq .
# Save the channel ID:
CHANNEL_ID=$(sprout channels create --name "test-cli" --type stream --visibility open | jq -r '.id')
# channels create (forum) — needed for messages vote later
FORUM_ID=$(sprout channels create --name "test-forum" --type forum --visibility open | jq -r '.id')
# channels list
sprout channels list | jq .
sprout channels list --visibility open | jq .
sprout channels list --member | jq .
# channels get
sprout channels get --channel "$CHANNEL_ID" | jq .
# channels update
sprout channels update --channel "$CHANNEL_ID" --name "test-cli-updated" \
--description "Updated" | jq .
# channels topic
sprout channels topic --channel "$CHANNEL_ID" --topic "Test topic" | jq .
# channels purpose
sprout channels purpose --channel "$CHANNEL_ID" --purpose "Testing" | jq .
# channels join (may already be a member from create)
sprout channels join --channel "$CHANNEL_ID" | jq .
# channels leave
sprout channels leave --channel "$CHANNEL_ID" | jq .
# Re-join so we can send messages
sprout channels join --channel "$CHANNEL_ID" | jq .
# channels archive (requires admin:channels scope)
sprout channels archive --channel "$CHANNEL_ID" | jq .
# channels unarchive
sprout channels unarchive --channel "$CHANNEL_ID" | jq .
```
### 6.2 Canvas
```bash
# canvas set
sprout canvas set --channel "$CHANNEL_ID" --content "# Test Canvas" | jq .
# canvas set from stdin
echo "# Canvas from stdin" | sprout canvas set --channel "$CHANNEL_ID" --content - | jq .
# canvas get
sprout canvas get --channel "$CHANNEL_ID" | jq .
```
### 6.3 Messages
```bash
# messages send
MSG=$(sprout messages send --channel "$CHANNEL_ID" --content "Hello from CLI test" | jq .)
echo "$MSG"
EVENT_ID=$(echo "$MSG" | jq -r '.id // .event_id')
# messages send with reply + broadcast
REPLY=$(sprout messages send --channel "$CHANNEL_ID" --content "Reply" \
--reply-to "$EVENT_ID" --broadcast | jq .)
echo "$REPLY"
REPLY_ID=$(echo "$REPLY" | jq -r '.id // .event_id')
# messages send with mentions
sprout messages send --channel "$CHANNEL_ID" --content "Hey @someone" \
--mention "0000000000000000000000000000000000000000000000000000000000000001" | jq .
# messages send from stdin — safe path for content with shell metacharacters
# (backticks, $vars, code blocks) that would otherwise be expanded by the shell.
echo 'Body with `backticks` and $vars stays literal.' \
| sprout messages send --channel "$CHANNEL_ID" --content - | jq .
# messages get
sprout messages get --channel "$CHANNEL_ID" | jq .
sprout messages get --channel "$CHANNEL_ID" --limit 5 | jq .
# messages thread
sprout messages thread --channel "$CHANNEL_ID" --event "$EVENT_ID" | jq .
# messages search
sprout messages search --query "Hello" | jq .
sprout messages search --query "CLI test" --limit 5 | jq .
# messages edit
sprout messages edit --event "$EVENT_ID" --content "Edited by CLI test" | jq .
# messages delete
sprout messages delete --event "$REPLY_ID" | jq .
```
### 6.4 Diff Messages
```bash
# messages send-diff from stdin
echo '--- a/foo.rs
+++ b/foo.rs
@@ -1,3 +1,3 @@
-fn old() {}
+fn new() {}' | sprout messages send-diff \
--channel "$CHANNEL_ID" \
--diff - \
--repo "https://github.com/example/repo" \
--commit "abcdef1234567890abcdef1234567890abcdef12" | jq .
# messages send-diff with metadata
echo "diff content" | sprout messages send-diff \
--channel "$CHANNEL_ID" \
--diff - \
--repo "https://github.com/example/repo" \
--commit "abcdef1234567890abcdef1234567890abcdef12" \
--file "src/main.rs" \
--lang "rust" \
--description "Refactored main" | jq .
# messages send-diff with branch + PR metadata
echo "diff content" | sprout messages send-diff \
--channel "$CHANNEL_ID" \
--diff - \
--repo "https://github.com/example/repo" \
--commit "abcdef1234567890abcdef1234567890abcdef12" \
--parent-commit "1234567890abcdef1234567890abcdef12345678" \
--source-branch "feature/cli" \
--target-branch "main" \
--pr 42 | jq .
```
### 6.5 Reactions
```bash
# Send a message to react to
REACT_MSG=$(sprout messages send --channel "$CHANNEL_ID" --content "React to this")
REACT_ID=$(echo "$REACT_MSG" | jq -r '.id // .event_id')
# reactions add
sprout reactions add --event "$REACT_ID" --emoji "👍" | jq .
# reactions get
sprout reactions get --event "$REACT_ID" | jq .
# reactions remove
sprout reactions remove --event "$REACT_ID" --emoji "👍" | jq .
```
### 6.6 DMs
```bash
# dms list
sprout dms list | jq .
# dms open (needs a real pubkey — use your own or a test one)
# Get your own pubkey first:
MY_PUBKEY=$(sprout users get | jq -r '.pubkey // .[0].pubkey // empty')
echo "My pubkey: $MY_PUBKEY"
# dms open with a synthetic pubkey (relay will create the user)
DM_RESULT=$(sprout dms open --pubkey "0000000000000000000000000000000000000000000000000000000000000001")
echo "$DM_RESULT" | jq .
DM_ID=$(echo "$DM_RESULT" | jq -r '.channel_id // .id // empty')
# dms add-member (requires messages:write scope — NOT admin:channels)
sprout dms add-member --channel "$DM_ID" \
--pubkey "0000000000000000000000000000000000000000000000000000000000000002" | jq .
```
### 6.7 Users & Presence
```bash
# users get — own profile (0 pubkeys)
sprout users get | jq .
# users get — single pubkey
sprout users get --pubkey "$MY_PUBKEY" | jq .
# users get — batch (2+ pubkeys)
sprout users get --pubkey "$MY_PUBKEY" --pubkey "$MY_PUBKEY" | jq .
# users set-profile
sprout users set-profile --name "CLI Test Agent" --about "Testing sprout-cli" | jq .
# users presence
sprout users presence --pubkeys "$MY_PUBKEY" | jq .
# users set-presence
sprout users set-presence --status online | jq .
sprout users set-presence --status away | jq .
sprout users set-presence --status offline | jq .
```
### 6.8 Channel Members (add/remove require admin:channels)
```bash
# channels add-member
sprout channels add-member --channel "$CHANNEL_ID" \
--pubkey "0000000000000000000000000000000000000000000000000000000000000001" \
--role member | jq .
# channels members
sprout channels members --channel "$CHANNEL_ID" | jq .
# channels remove-member
sprout channels remove-member --channel "$CHANNEL_ID" \
--pubkey "0000000000000000000000000000000000000000000000000000000000000001" | jq .
```
### 6.9 Workflows
```bash
# workflows create
# NOTE: trigger uses `on:` tag (serde internally tagged enum).
# Valid triggers: message_posted, reaction_added, diff_posted, schedule, webhook
# Steps use `action:` tag: send_message, send_dm, set_channel_topic, add_reaction, etc.
WF=$(sprout workflows create --channel "$CHANNEL_ID" \
--yaml 'name: test-wf
trigger:
on: webhook
steps:
- id: step1
action: send_message
text: "Hello from workflow"' | jq .)
echo "$WF"
WF_ID=$(echo "$WF" | jq -r '.id')
# workflows list
sprout workflows list --channel "$CHANNEL_ID" | jq .
# workflows get
sprout workflows get --workflow "$WF_ID" | jq .
# workflows update
sprout workflows update --workflow "$WF_ID" \
--yaml 'name: test-wf-updated
trigger:
on: webhook
steps:
- id: step1
action: send_message
text: "Updated"' | jq .
# workflows trigger
sprout workflows trigger --workflow "$WF_ID" | jq .
# workflows runs
sprout workflows runs --workflow "$WF_ID" | jq .
# workflows approve — requires a workflow run waiting for approval
# This is hard to test ad-hoc without a workflow that has an approval gate.
# Test the validation instead:
sprout workflows approve --token "00000000-0000-0000-0000-000000000000" 2>&1 || true
# Should fail with relay error (token not found), not a validation error
# To test the deny path: sprout workflows approve --token <UUID> --approved false
# workflows delete
sprout workflows delete --workflow "$WF_ID" | jq .
```
### 6.10 Feed
```bash
sprout feed get | jq .
sprout feed get --limit 5 | jq .
```
### 6.11 Forum & Voting
```bash
# Send a forum post (kind 45001) to the forum channel
FORUM_POST=$(sprout messages send --channel "$FORUM_ID" \
--content "Forum post for vote testing" --kind 45001 | jq .)
echo "$FORUM_POST"
FORUM_EVENT_ID=$(echo "$FORUM_POST" | jq -r '.id // .event_id')
# messages vote (up)
sprout messages vote --event "$FORUM_EVENT_ID" --direction up | jq .
# messages vote (down)
sprout messages vote --event "$FORUM_EVENT_ID" --direction down | jq .
```
---
## 7. Error Path Testing
Verify the CLI produces correct JSON on stderr and correct exit codes.
```bash
# Exit 1: Invalid UUID
sprout channels get --channel "not-a-uuid" 2>&1; echo "exit: $?"
# stderr: {"error":"user_error","message":"invalid UUID: not-a-uuid"}
# exit: 1
# Exit 1: Invalid hex64
sprout messages delete --event "not-hex" 2>&1; echo "exit: $?"
# stderr: {"error":"user_error","message":"must be a 64-character hex string: not-hex"}
# exit: 1
# Exit 1: Invalid --type value
sprout channels create --name x --type invalid --visibility open 2>&1; echo "exit: $?"
# stderr: {"error":"user_error","message":"--type must be 'stream' or 'forum' (got: invalid)"}
# exit: 1
# Exit 1: Invalid --direction value
sprout messages vote --event "$(printf '0%.0s' {1..64})" \
--direction sideways 2>&1; echo "exit: $?"
# exit: 1
# Exit 1: Empty body guard
sprout users set-profile 2>&1; echo "exit: $?"
# exit: 1 (at least one field required)
# Exit 3: No auth configured
env -u SPROUT_PRIVATE_KEY \
cargo run -p sprout-cli -- channels list 2>&1; echo "exit: $?"
# stderr: {"error":"auth_error","message":"SPROUT_PRIVATE_KEY is required (use --private-key or set env var)"}
# exit: 3
# Exit 2: Non-existent channel (valid UUID)
sprout channels get --channel "00000000-0000-0000-0000-000000000000" 2>&1; echo "exit: $?"
# stderr: {"error":"relay_error","message":"..."}
# exit: 2
```
---
## 8. Auth Testing
Test authentication.
```bash
# Private key (SPROUT_PRIVATE_KEY)
SPROUT_PRIVATE_KEY="nsec1..." sprout channels list | jq .
# Should succeed
# No auth → exit 3
env -u SPROUT_PRIVATE_KEY \
cargo run -p sprout-cli -- channels list 2>&1; echo "exit: $?"
# stderr: {"error":"auth_error","message":"SPROUT_PRIVATE_KEY is required (use --private-key or set env var)"}
# exit: 3
```
---
## 9. Cleanup
```bash
# Delete test channels
sprout channels delete --channel "$CHANNEL_ID" | jq .
sprout channels delete --channel "$FORUM_ID" | jq .
```
---
## 10. Checklist
| # | Command | Tested | Notes |
|---|---------|:------:|-------|
| 1 | `messages send` | ☐ | Basic, reply, broadcast, mentions, stdin |
| 2 | `messages send-diff` | ☐ | Stdin, metadata, branch/PR |
| 3 | `messages edit` | ☐ | |
| 4 | `messages delete` | ☐ | |
| 5 | `messages get` | ☐ | With limit |
| 6 | `messages thread` | ☐ | |
| 7 | `messages search` | ☐ | With limit |
| 8 | `messages vote` | ☐ | Up and down |
| 9 | `channels list` | ☐ | With visibility, member |
| 10 | `channels get` | ☐ | |
| 11 | `channels create` | ☐ | Stream and forum |
| 12 | `channels update` | ☐ | |
| 13 | `channels topic` | ☐ | |
| 14 | `channels purpose` | ☐ | |
| 15 | `channels join` | ☐ | |
| 16 | `channels leave` | ☐ | |
| 17 | `channels archive` | ☐ | Needs admin:channels |
| 18 | `channels unarchive` | ☐ | Needs admin:channels |
| 19 | `channels delete` | ☐ | Needs admin:channels |
| 20 | `channels members` | ☐ | |
| 21 | `channels add-member` | ☐ | Needs admin:channels |
| 22 | `channels remove-member` | ☐ | Needs admin:channels |
| 23 | `canvas get` | ☐ | |
| 24 | `canvas set` | ☐ | Direct and stdin |
| 25 | `reactions add` | ☐ | |
| 26 | `reactions remove` | ☐ | |
| 27 | `reactions get` | ☐ | |
| 28 | `dms list` | ☐ | |
| 29 | `dms open` | ☐ | |
| 30 | `dms add-member` | ☐ | Needs messages:write |
| 31 | `users get` | ☐ | Self, single, batch |
| 32 | `users set-profile` | ☐ | |
| 33 | `users presence` | ☐ | |
| 34 | `users set-presence` | ☐ | online, away, offline |
| 35 | `workflows list` | ☐ | |
| 36 | `workflows create` | ☐ | |
| 37 | `workflows update` | ☐ | |
| 38 | `workflows delete` | ☐ | |
| 39 | `workflows trigger` | ☐ | |
| 40 | `workflows runs` | ☐ | |
| 41 | `workflows get` | ☐ | |
| 42 | `workflows approve` | ☐ | Validation only (needs approval gate); bare = approve, `--approved false` = deny |
| 43 | `feed get` | ☐ | |
| 44 | `social publish` | ☐ | |
| 45 | `social set-contacts` | ☐ | |
| 46 | `social event` | ☐ | |
| 47 | `social notes` | ☐ | |
| 48 | `social contacts` | ☐ | |
| 49 | `repos create` | ☐ | |
| 50 | `repos get` | ☐ | |
| 51 | `repos list` | ☐ | |
| 52 | `upload file` | ☐ | |
| 53 | `pack validate` | ☐ | Local, no relay |
| 54 | `pack inspect` | ☐ | Local, no relay |