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

15 KiB

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:

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

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:

cd REPOS/sprout-nostr
set -a && source .env && set +a
cargo run -p sprout-relay

Verify:

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).

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:

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

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

# 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

# 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

# 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

# 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

# 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

# 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

# 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)

# 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

# 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

sprout feed get | jq .
sprout feed get --limit 5 | jq .

6.11 Forum & Voting

# 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.

# 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.

# 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

# 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