Files
buzz/TESTING.md

1317 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Sprout Testing Guide
This guide enables an AI agent (the **operator**) to run the full Sprout test suite: automated `cargo test` suites and a three-agent multi-agent E2E run that exercises all 40 MCP tools against a live relay.
## Two Test Modes
| Mode | What It Does | When to Use |
|------|-------------|-------------|
| **Automated** (`cargo test`) | Unit tests + REST/WebSocket/MCP integration tests | Fast CI check; verify no unit regressions |
| **Multi-Agent E2E** | Three agents (Alice, Bob, Charlie) run via `sprout-acp` harness, exercising all 40 MCP tools via real Nostr identities | Before merging relay/MCP/auth changes; full regression run; exploring new features |
Run both modes for a complete regression check. Run automated-only for a fast sanity check.
---
## Table of Contents
1. [Prerequisites](#1-prerequisites)
2. [Quick Start: Automated Tests Only](#2-quick-start-automated-tests-only)
3. [Multi-Agent E2E Testing](#3-multi-agent-e2e-testing)
- [3.1 Architecture](#31-architecture)
- [3.2 Infrastructure Setup](#32-infrastructure-setup)
- [3.3 Mint Agent Keys](#33-mint-agent-keys)
- [3.4 Launch Harness Instances](#34-launch-harness-instances)
- [3.5 Test Exercises](#35-test-exercises)
- [3.6 Monitoring & Verification](#36-monitoring--verification)
- [3.7 Expected Results](#37-expected-results)
4. [Advanced: ACP Harness Scenarios](#4-advanced-acp-harness-scenarios)
5. [Workflow YAML Reference](#5-workflow-yaml-reference)
6. [The 40 MCP Tools](#6-the-40-mcp-tools)
7. [Cleanup](#7-cleanup)
8. [Known Issues / Troubleshooting](#8-known-issues--troubleshooting)
---
## 1. Prerequisites
Verify each requirement before proceeding. All commands must succeed.
### Docker
```bash
docker --version
# Required: any recent version
docker compose version
# Required: v2+ (uses "docker compose", not "docker-compose")
```
### Rust 1.88+
```bash
# From the sprout repo root — use Hermit if system Rust is older than 1.88
. bin/activate-hermit
rustc --version
# Required: rustc 1.88.0 or newer
```
### goose CLI
```bash
goose --version
# Must be on $PATH and configured with a valid provider/model
goose run --help | head -5
# Must not error
```
### sqlx-cli
```bash
sqlx --version
# If missing:
cargo install sqlx-cli --no-default-features --features mysql
```
### screen
```bash
screen --version
# Must print a version string (note: on macOS this exits with code 1 — that's fine)
# If missing: brew install screen
```
### All clear
If all commands above print version info, proceed. If any binary is missing, install it first — the tests will not work without all prerequisites.
---
## 2. Quick Start: Automated Tests Only
Run this when you want a fast check without spinning up multi-agent infrastructure.
```bash
# Enter the repo and activate toolchain FIRST — all subsequent commands
# assume you are in the sprout repo root with hermit activated.
cd /path/to/sprout # e.g. ~/Development/goosetown_oss/REPOS/sprout
. bin/activate-hermit
```
### Check for existing infrastructure
If Docker services or a relay are already running from a previous session, you can
leave the Docker services up and just reset the database and relay:
```bash
# Kill any existing relay
screen -S relay -X quit 2>/dev/null
lsof -ti :3000 | xargs kill -9 2>/dev/null
# Check Docker services — if already running, skip `docker compose up`
docker compose ps --format '{{.Name}} {{.Status}}' 2>/dev/null
# If mysql/redis/typesense show "Up", you can skip to "Setup and build" below.
# If not running:
docker compose up -d
```
> **Port conflicts:** If `docker compose up -d` fails with "port already allocated",
> a container from another project may be using the port. Find it with
> `docker ps --format '{{.Names}} {{.Ports}}'` and stop it manually.
> **Keycloak:** You may see `sprout-keycloak` as `unhealthy` or `starting` — this
> is fine. Keycloak is only needed for token-based auth and is not required for
> automated tests (which use dev-mode `X-Pubkey` header auth). You may also see
> extra containers like `sprout-postgres` from other projects — ignore them.
### Setup and build
```bash
# Configure environment
[ -f .env ] || cp .env.example .env
# Load env vars — ALWAYS required, even if .env already existed
export $(cat .env | grep -v "^#" | grep -v "^$" | xargs) 2>/dev/null
# Reset database (fresh state for tests)
docker exec sprout-mysql mysql -u root -psprout_dev -e \
"DROP DATABASE IF EXISTS sprout; CREATE DATABASE sprout;" 2>/dev/null
sqlx migrate run --database-url "$DATABASE_URL"
# Build the full workspace (relay, MCP server, ACP harness, test client, etc.)
cargo build --release --workspace
# Run unit tests
cargo test --workspace
```
### Integration Tests (require running relay)
Start the relay (kill any stale instance first):
```bash
screen -S relay -X quit 2>/dev/null
lsof -ti :3000 | xargs kill -9 2>/dev/null; sleep 1
screen -dmS relay bash -c \
'export $(cat .env | grep -v "^#" | grep -v "^$" | xargs) 2>/dev/null; \
./target/release/sprout-relay 2>&1 | tee /tmp/sprout-relay.log'
sleep 3 && curl -s http://localhost:3000/health
# Must print: ok
```
Then run the integration suites:
```bash
# REST API integration tests (35 tests)
RELAY_URL=ws://localhost:3000 \
cargo test -p sprout-test-client --test e2e_rest_api -- --ignored
# WebSocket relay integration tests (14 tests)
RELAY_URL=ws://localhost:3000 \
cargo test -p sprout-test-client --test e2e_relay -- --ignored
# MCP server integration tests (10 tests)
RELAY_URL=ws://localhost:3000 \
cargo test -p sprout-test-client --test e2e_mcp -- --ignored
```
### Expected Results
```
test result: ok. 35 passed; 0 failed; 0 ignored ← REST API
test result: ok. 14 passed; 0 failed; 0 ignored ← relay
test result: ok. 10 passed; 0 failed; 0 ignored ← MCP
```
All 59 integration tests pass (across the three suites above). An additional 7 workflow integration tests exist in `e2e_workflows.rs` — run them separately if workflow changes are involved. If any fail, check that the relay is running and Docker services are healthy before proceeding to E2E.
---
## 3. Multi-Agent E2E Testing
### 3.1 Architecture
The E2E suite uses the `sprout-acp` harness — a process that bridges Sprout relay events to AI agents over the ACP protocol. The operator sends `@mention` events via the `mention` binary; each harness instance picks up mentions targeting its agent's pubkey and forwards them to a goose session with Sprout MCP tools pre-configured.
```
Operator (you)
│
│ mention <channel> <pubkey> "task instructions"
▼
Sprout Relay ──WS (NIP-01)──► sprout-acp (harness) ──stdio (ACP)──► goose
│
sprout-mcp-server
(40 MCP tools)
│
Sprout Relay
(send_message, etc.)
```
Three harness instances run simultaneously — one each for Alice, Bob, and Charlie. Each has its own Nostr keypair (identity) and responds only to `@mentions` targeting its pubkey.
**Key properties of the harness:**
- Discovers and subscribes to all accessible channels on startup
- Queues events per channel; one prompt in flight globally at a time
- Batches multiple rapid `@mentions` into a single prompt
- Auto-respawns the agent subprocess on crash
- Reconnects to the relay with a `since` filter on disconnect (no missed events)
- `GOOSE_MODE=auto` is **mandatory** — prevents goose from pausing for permission prompts
### 3.2 Infrastructure Setup
Run all commands from the sprout repo root.
```bash
cd /path/to/sprout
. bin/activate-hermit
# 1. Start Docker services (MySQL, Redis, Typesense, Keycloak)
docker compose down -v && docker compose up -d
docker compose ps # All services should show "Up"
# 2. Configure environment
[ -f .env ] || cp .env.example .env
export $(cat .env | grep -v "^#" | grep -v "^$" | xargs) 2>/dev/null
# 3. Run database migrations
sqlx migrate run --database-url "$DATABASE_URL"
# 4. Build all binaries (sprout-acp, sprout-mcp-server, mention, sprout-admin)
cargo build --release --workspace
# 5. Add release binaries to PATH
export PATH="$PWD/target/release:$PATH"
# 6. Verify key binaries are present
ls -la target/release/sprout-acp target/release/sprout-mcp-server \
target/release/mention target/release/sprout-admin
# 7. Start the relay
lsof -ti :3000 | xargs kill -9 2>/dev/null; sleep 1
screen -dmS relay bash -c \
'export $(cat .env | grep -v "^#" | grep -v "^$" | xargs) 2>/dev/null; \
./target/release/sprout-relay 2>&1 | tee /tmp/sprout-relay.log'
sleep 3
# 8. Verify relay is up
curl -s http://localhost:3000/health
# Expected: {"status":"ok"} or similar
```
### 3.3 Mint Agent Keys
Each agent needs its own Nostr keypair. Use `sprout-admin` to mint them — it handles all database interaction internally.
```bash
# Mint keys for all three agents
for agent in alice bob charlie; do
echo "=== $agent ==="
cargo run -p sprout-admin -- mint-token \
--name "$agent" \
--scopes "messages:read,messages:write,channels:read"
echo ""
done
```
Each invocation prints an `nsec1...` private key, the corresponding pubkey hex, and an API token. **Save all three sets immediately — they are shown only once.**
Set environment variables for the session:
```bash
# Replace with actual values from mint-token output
export ALICE_NSEC="nsec1..."
export ALICE_PUBKEY="<alice-pubkey-hex>"
export BOB_NSEC="nsec1..."
export BOB_PUBKEY="<bob-pubkey-hex>"
export CHARLIE_NSEC="nsec1..."
export CHARLIE_PUBKEY="<charlie-pubkey-hex>"
```
> **Tip:** Pipe the mint output to a temp file during setup:
> `cargo run -p sprout-admin -- mint-token --name alice ... | tee /tmp/alice-keys.txt`
### 3.4 Launch Harness Instances
Start one `sprout-acp` instance per agent in a dedicated screen session. `GOOSE_MODE=auto` is required on all three.
```bash
# Alice's harness
SPROUT_PRIVATE_KEY="$ALICE_NSEC" \
SPROUT_RELAY_URL="ws://localhost:3000" \
GOOSE_MODE=auto \
screen -dmS agent-alice bash -c \
'sprout-acp 2>&1 | tee /tmp/agent-alice.log'
# Bob's harness
SPROUT_PRIVATE_KEY="$BOB_NSEC" \
SPROUT_RELAY_URL="ws://localhost:3000" \
GOOSE_MODE=auto \
screen -dmS agent-bob bash -c \
'sprout-acp 2>&1 | tee /tmp/agent-bob.log'
# Charlie's harness
SPROUT_PRIVATE_KEY="$CHARLIE_NSEC" \
SPROUT_RELAY_URL="ws://localhost:3000" \
GOOSE_MODE=auto \
screen -dmS agent-charlie bash -c \
'sprout-acp 2>&1 | tee /tmp/agent-charlie.log'
```
Wait ~5 seconds for all three to connect, then verify:
```bash
sleep 5
for agent in alice bob charlie; do
echo "=== agent-$agent ==="
grep -E "connected|discovered|subscribed|error" /tmp/agent-$agent.log 2>/dev/null \
|| echo "(no log yet)"
echo ""
done
```
Expected startup output for each harness:
```
sprout-acp starting: relay=ws://localhost:3000 harness_pubkey=... agent_pubkey=<hex>
agent initialized: ...
connected to relay at ws://localhost:3000
discovered N channel(s)
subscribed to channel <uuid>
```
If you see `discovered 0 channel(s)`, the agent is not yet a member of any channels. Alice will create channels in the first exercise — after that, all three will discover them on subsequent subscriptions (open channels are accessible to any authenticated pubkey).
> **Bootstrap channel timing:** Harnesses discover channels only at startup.
> If you create the bootstrap channel (in exercise A-1) *after* launching
> harnesses, Alice's harness won't be subscribed to it. Two options:
> 1. Create the bootstrap channel *before* launching harnesses (recommended):
> run the `curl -X POST` command from A-1 first, then start all three harnesses.
> 2. Restart Alice's harness after creating the bootstrap channel — it will
> discover and subscribe to it on reconnect.
---
### 3.5 Test Exercises
All exercises are delivered via `@mention` events using the `mention` binary:
```
mention <channel_uuid> <target_pubkey_hex> "task instructions"
```
The `mention` binary generates ephemeral sender keys — it does not need its own nsec. It requires `SPROUT_RELAY_URL` (defaults to `ws://localhost:3000`).
**Important:** Channel UUIDs are dynamic. Alice creates the channels in Exercise A-1. After that step completes, query the REST API to get the UUIDs before proceeding with other exercises.
```bash
# Helper: get channel UUID by name (run after Alice creates channels)
get_channel_uuid() {
local name="$1"
curl -s -H "X-Pubkey: $ALICE_PUBKEY" \
"http://localhost:3000/api/channels" \
| jq -r ".[] | select(.name == \"$name\") | .id"
}
```
---
#### Alice — Infrastructure Creator
Alice sets up the shared environment that Bob and Charlie will use.
**A-1: Create channels and seed messages**
Alice needs a bootstrap channel to receive her first `@mention`. Use the default test channel from the relay, or create one via the REST API first:
```bash
# Create a bootstrap channel for Alice's first mention
BOOTSTRAP_CHANNEL=$(curl -s -X POST \
-H "Content-Type: application/json" \
-H "X-Pubkey: $ALICE_PUBKEY" \
"http://localhost:3000/api/channels" \
-d '{"name":"bootstrap","channel_type":"stream","visibility":"open"}' \
| jq -r '.id')
echo "Bootstrap channel: $BOOTSTRAP_CHANNEL"
```
Then send Alice her first task:
```bash
mention "$BOOTSTRAP_CHANNEL" "$ALICE_PUBKEY" \
"Create 3 channels: 'general' (stream/open), 'alice-testing' (stream/open), and 'private-ops' (stream/private). Then send 3 messages to the 'general' channel introducing yourself and describing what you're testing."
```
Wait for Alice to complete (~30–60s), then capture channel UUIDs:
```bash
sleep 60
export GENERAL=$(get_channel_uuid "general")
export ALICE_TESTING=$(get_channel_uuid "alice-testing")
export PRIVATE_OPS=$(get_channel_uuid "private-ops")
echo "general=$GENERAL alice-testing=$ALICE_TESTING private-ops=$PRIVATE_OPS"
```
**A-2: Channel metadata and canvas**
```bash
mention "$GENERAL" "$ALICE_PUBKEY" \
"Set the topic on the 'general' channel to 'Sprout E2E Testing'. Set the purpose to 'Multi-agent integration test run'. Then set the canvas on 'general' to a markdown document with a header '# Test Run Notes' and a bullet list of the 3 channels you created."
```
**A-3: Thread and reactions**
```bash
mention "$GENERAL" "$ALICE_PUBKEY" \
"Get the history of the 'general' channel. Reply to your first message there with a thread reply saying 'This is a thread reply from Alice'. Then add a 👍 reaction and a 🚀 reaction to your own first message."
```
**A-4: Workflow creation**
```bash
mention "$GENERAL" "$ALICE_PUBKEY" \
"Create a workflow named 'alice-notify' with a message_posted trigger on the 'general' channel. The workflow should have one step: send a message to the 'general' channel saying 'Workflow fired!'. Save the workflow ID and report it back."
```
**A-5: Profile, NIP-05 identity, and presence**
```bash
mention "$GENERAL" "$ALICE_PUBKEY" \
"Set your display name to 'Alice (Test Agent)'. Set your about/bio to 'I am Alice, the infrastructure creator for the Sprout E2E test suite.' Set your NIP-05 handle to 'alice@localhost' using set_profile. Then use get_presence to check your own presence status (pubkey: $ALICE_PUBKEY)."
```
**A-6: Feed, search, and membership**
```bash
mention "$GENERAL" "$ALICE_PUBKEY" \
"Get your feed and the channel feed for 'general'. Search for messages containing the word 'Alice'. List the members of the 'general' channel. Then invite Bob (pubkey: $BOB_PUBKEY) to the 'private-ops' channel."
```
---
#### Bob — Discoverer and Reactor
Bob explores the environment Alice created and interacts with her content.
**B-1: Discovery and history**
```bash
mention "$GENERAL" "$BOB_PUBKEY" \
"List all channels you have access to. Get the message history from the 'general' channel (last 20 messages). Report what you find — how many channels exist, and what did Alice write in general?"
```
**B-2: Reactions and DM**
```bash
mention "$GENERAL" "$BOB_PUBKEY" \
"React to the first message in the 'general' channel with a ❤️ reaction. Then send a direct message to Alice (pubkey: $ALICE_PUBKEY) saying 'Hi Alice, Bob here — I can see your channels and messages. The setup looks great!'"
```
**B-3: DM history and canvas**
```bash
mention "$GENERAL" "$BOB_PUBKEY" \
"Get your DM conversation history with Alice. Read the canvas on the 'general' channel and report what it says. List all your DM conversations."
```
**B-4: Thread participation**
```bash
mention "$GENERAL" "$BOB_PUBKEY" \
"Find Alice's thread in the 'general' channel (a message that has replies). Add a thread reply saying 'Bob joining the thread — everything looks good from my end.' Then get the thread replies and report how many there are."
```
**B-5: Channel join and profile**
```bash
mention "$GENERAL" "$BOB_PUBKEY" \
"Join the 'alice-testing' channel. Get your own profile and set your display name to 'Bob (Test Agent)'. Search for any messages mentioning 'workflow' or 'canvas'."
```
**B-6: Private channel access test**
```bash
mention "$GENERAL" "$BOB_PUBKEY" \
"Get the message history from the 'private-ops' channel (ID: $PRIVATE_OPS). Alice invited you in exercise A-6 — confirm you have access and report what you find."
```
**B-7: Get presence**
```bash
mention "$GENERAL" "$BOB_PUBKEY" \
"Get the presence status for Alice (pubkey: $ALICE_PUBKEY). Get your own presence status. Report both."
```
**B-8: Profile resolution (public profiles)**
```bash
mention "$GENERAL" "$BOB_PUBKEY" \
"Use get_user_profile to look up Alice's profile (pubkey: $ALICE_PUBKEY). Report her display name and about text. Then use get_users_batch with all three pubkeys (yours: $BOB_PUBKEY, Alice: $ALICE_PUBKEY, Charlie: $CHARLIE_PUBKEY). Report which ones have display names set and which are in the missing list."
```
---
#### Charlie — Edge Case Specialist
Charlie tests error handling, idempotency, and lifecycle operations.
**C-1: Non-existent channel error**
```bash
mention "$GENERAL" "$CHARLIE_PUBKEY" \
"Try to send a message to channel UUID '00000000-0000-0000-0000-000000000000'. Report the exact error you receive."
```
**C-2: Unauthorized archive attempt**
```bash
mention "$GENERAL" "$CHARLIE_PUBKEY" \
"Try to archive the 'general' channel (ID: $GENERAL). You did not create it — report what error you get."
```
**C-3: Canvas overwrite**
```bash
mention "$GENERAL" "$CHARLIE_PUBKEY" \
"Set the canvas on the 'general' channel to a new markdown document: '# Charlie Was Here\n\nCharlie overwrote the canvas on $(date -u +%Y-%m-%dT%H:%M:%SZ)'. Then immediately read the canvas back and confirm it shows your content."
```
**C-4: Reaction idempotency**
```bash
mention "$GENERAL" "$CHARLIE_PUBKEY" \
"Find the first message in the 'general' channel. Add a 🎉 reaction to it. Then try to add the same 🎉 reaction again. Report what happens the second time — does it error or succeed silently?"
```
**C-5: Channel lifecycle (create → archive → send → unarchive → send)**
```bash
mention "$GENERAL" "$CHARLIE_PUBKEY" \
"Create a new channel named 'charlie-lifecycle' (stream/open). Send a message to it saying 'Before archive'. Archive the channel. Try to send another message — report the error. Unarchive the channel. Send a message saying 'After unarchive'. Confirm the final message was accepted."
```
**C-6: Join, send, leave, verify**
```bash
mention "$GENERAL" "$CHARLIE_PUBKEY" \
"Join the 'alice-testing' channel. Send a message there saying 'Charlie was here'. Then leave the channel. Try to send another message to 'alice-testing' — report whether it succeeds or fails after leaving."
```
**C-7: Workflow trigger and cross-agent summary**
```bash
mention "$GENERAL" "$CHARLIE_PUBKEY" \
"Get the list of workflows. Find Alice's 'alice-notify' workflow and trigger it via webhook if it has a webhook trigger, or note that it uses message_posted. Then get the presence for both Alice (pubkey: $ALICE_PUBKEY) and Bob (pubkey: $BOB_PUBKEY). Finally, produce a summary report in the 'general' channel listing: (1) all channels created during this test run, (2) total messages sent, (3) any errors encountered."
```
---
**C-8: NIP-05 identity verification**
```bash
mention "$GENERAL" "$CHARLIE_PUBKEY" \
"Verify the NIP-05 endpoint. Alice set her NIP-05 handle to 'alice@localhost' in exercise A-5. Use curl or an HTTP request to GET http://localhost:3000/.well-known/nostr.json?name=alice — it should return her pubkey in the 'names' map and a relay URL in the 'relays' map. Also try ?name=nonexistent and confirm it returns empty names/relays. Check that the response includes an Access-Control-Allow-Origin: * header. Report your findings."
```
---
**C-9: Profile edge cases**
```bash
mention "$GENERAL" "$CHARLIE_PUBKEY" \
"Test profile edge cases. Use get_user_profile with a pubkey that doesn't exist — report the error. Use get_users_batch with a mix of valid pubkeys, an invalid-length string like 'tooshort', and a string that is 64 chars but not valid hex. Report what ends up in the profiles map vs the missing list."
```
---
### 3.6 Monitoring & Verification
#### Watch harness logs live
```bash
# Tail the log files (agent-safe — no TTY required)
tail -f /tmp/agent-alice.log &
tail -f /tmp/agent-bob.log &
tail -f /tmp/agent-charlie.log &
# Or view recent output from a specific agent
tail -50 /tmp/agent-alice.log
```
> **Note:** Do NOT use `screen -r` to attach to harness sessions if you are an
> AI agent — it requires an interactive TTY and will hang indefinitely. Always
> use `tail` on the log files or `grep` for specific patterns instead.
Key log patterns to watch for:
```
# Good — turn completed
turn complete for channel <uuid>: end_turn
# Good — agent is working
prompting agent for channel <uuid> (session ..., N event(s))
# Investigate — agent had trouble
turn complete for channel <uuid>: max_tokens
turn timeout (300s) for channel <uuid> — cancelling
# Bad — needs attention
agent process exited — respawning
relay connection lost — reconnecting
```
#### Tail log files
```bash
# All three agents at once
tail -f /tmp/agent-alice.log /tmp/agent-bob.log /tmp/agent-charlie.log
# Filter for completions only
grep "turn complete\|turn timeout\|turn cancelled" \
/tmp/agent-alice.log /tmp/agent-bob.log /tmp/agent-charlie.log
```
#### REST API verification
```bash
# List all channels (use any agent's pubkey)
curl -s -H "X-Pubkey: $ALICE_PUBKEY" \
"http://localhost:3000/api/channels" \
| jq '.[] | {id: .id, name: .name, visibility: .visibility}'
# Recent messages in general
curl -s -H "X-Pubkey: $ALICE_PUBKEY" \
"http://localhost:3000/api/channels/$GENERAL/messages?limit=20" \
| jq '.[] | {sender: .pubkey[:16], body: .content[:120]}'
# Messages from a specific agent
curl -s -H "X-Pubkey: $ALICE_PUBKEY" \
"http://localhost:3000/api/channels/$GENERAL/messages?limit=50" \
| jq --arg pk "$CHARLIE_PUBKEY" \
'[.[] | select(.pubkey == $pk)] | {count: length, messages: [.[] | .content[:100]]}'
# Channel members
curl -s -H "X-Pubkey: $ALICE_PUBKEY" \
"http://localhost:3000/api/channels/$GENERAL/members" \
| jq '.[] | {pubkey: .pubkey[:16], role: .role}'
# Check private-ops membership (Bob should be there after A-6)
curl -s -H "X-Pubkey: $ALICE_PUBKEY" \
"http://localhost:3000/api/channels/$PRIVATE_OPS/members" \
| jq '.[] | .pubkey[:16]'
```
#### Screen session management
```bash
# List all active sessions
screen -ls
# Check if a session is still running
screen -ls | grep agent-alice
# Capture current screen contents to file (non-destructive)
screen -S agent-alice -X hardcopy /tmp/alice-snapshot.txt
cat /tmp/alice-snapshot.txt
```
### 3.7 Expected Results
After all exercises complete, the following should be true:
| Check | Expected |
|-------|----------|
| Channels created | At least 5: general, alice-testing, private-ops, charlie-lifecycle, bootstrap |
| Messages in general | 10+ messages from Alice, Bob, and Charlie |
| Thread replies | At least 2 replies on Alice's first message |
| Reactions | 👍 🚀 (Alice), ❤️ (Bob), 🎉 (Charlie) on general messages |
| Canvas | Charlie's content (last writer wins) |
| DM conversation | Alice ↔ Bob DM exists |
| Bob in private-ops | Yes (Alice invited him in A-6) |
| Workflow | alice-notify created with message_posted trigger |
| Display names | Alice and Bob have display names set |
| Profile resolution | Bob can read Alice's profile via `get_user_profile`; `get_users_batch` returns Alice and Bob with display names, Charlie with null display name (all in profiles map) |
| NIP-05 verification | Charlie queries `/.well-known/nostr.json?name=alice` and gets Alice's pubkey (Alice set `alice@localhost` in A-5) |
| Profile edge cases | Charlie gets appropriate errors for invalid/unknown pubkeys |
| Error handling | Charlie's C-1, C-2, C-5 exercises report correct errors |
| charlie-lifecycle | Unarchived and final message sent successfully |
**Verify the full picture:**
```bash
# Channel count
curl -s -H "X-Pubkey: $ALICE_PUBKEY" \
"http://localhost:3000/api/channels" | jq 'length'
# Message count in general
curl -s -H "X-Pubkey: $ALICE_PUBKEY" \
"http://localhost:3000/api/channels/$GENERAL/messages?limit=200" | jq 'length'
# Workflow list
curl -s -H "X-Pubkey: $ALICE_PUBKEY" \
"http://localhost:3000/api/workflows" \
| jq '.[] | {name: .name, trigger: .trigger.on}'
```
---
## 4. Advanced: ACP Harness Scenarios
These scenarios test the `sprout-acp` harness itself — crash recovery, relay reconnection, turn timeout, and concurrent multi-agent operation. Run them independently from the main E2E suite.
### Prerequisites for Advanced Scenarios
```bash
# Use the single-agent test key from sprout-acp TESTING.md
export SPROUT_PRIVATE_KEY=nsec1ddyp0fufd6ejerfqkxcfqlmkktwzx7w45emalvgtcvyafefusj5q8fyllm
export AGENT_PUBKEY=ae670a075ac2446f445808ab5a1a796cec37c72c70b25e10ee39f7f0eab50feb
export TEST_CHANNEL=94a444a4-c0a3-5966-ab05-530c6ddc2301
# Start a single harness
SPROUT_PRIVATE_KEY="$SPROUT_PRIVATE_KEY" \
SPROUT_RELAY_URL="ws://localhost:3000" \
GOOSE_MODE=auto \
screen -dmS harness bash -c 'sprout-acp 2>&1 | tee /tmp/harness.log'
sleep 5
```
### Scenario A: Basic @mention → Agent Replies
```bash
mention "$TEST_CHANNEL" "$AGENT_PUBKEY" "What is 2 + 2? Reply with just the number."
sleep 30
# Verify reply via REST API
curl -s -H "X-Pubkey: $AGENT_PUBKEY" \
"http://localhost:3000/api/channels/$TEST_CHANNEL/messages?limit=5" \
| jq --arg pk "$AGENT_PUBKEY" \
'.[] | select(.pubkey == $pk) | {body: .content[:200]}'
```
Expected: agent replies with "4" via `send_message`.
### Scenario B: Multi-Event Batch
```bash
# Send 3 mentions in rapid succession
for i in 1 2 3; do
mention "$TEST_CHANNEL" "$AGENT_PUBKEY" "Batch message $i" &
done
wait
sleep 30
# Check harness log for batch size
grep "prompting agent" /tmp/harness.log | tail -5
# Look for "(session ..., N event(s))" where N > 1
```
### Scenario C: Agent Crash Recovery
```bash
# Kill the agent subprocess
kill -9 $(pgrep -f "goose acp") 2>/dev/null
# Send a new mention
sleep 2
mention "$TEST_CHANNEL" "$AGENT_PUBKEY" "Are you still alive after the crash?"
sleep 30
# Verify recovery in logs
grep -E "agent process exited|agent respawned|turn complete" /tmp/harness.log | tail -10
```
Expected log sequence: `agent process exited — respawning` → `agent respawned successfully` → `agent initialized` → `turn complete`.
### Scenario D: Relay Disconnect Recovery
```bash
# Stop the relay
screen -S relay -X stuff $'\003' # Ctrl-C
sleep 2
# Watch harness detect disconnect
grep "relay connection lost\|reconnecting" /tmp/harness.log
# Restart relay
screen -dmS relay bash -c \
'export $(cat .env | grep -v "^#" | grep -v "^$" | xargs) 2>/dev/null; \
./target/release/sprout-relay 2>&1 | tee /tmp/sprout-relay.log'
sleep 5
# Verify reconnection
grep "relay reconnected\|subscribed" /tmp/harness.log | tail -5
# Confirm harness is functional again
mention "$TEST_CHANNEL" "$AGENT_PUBKEY" "Post-reconnect test — reply with OK"
sleep 30
grep "turn complete" /tmp/harness.log | tail -3
```
### Scenario E: Turn Timeout
```bash
# Restart harness with 5-second timeout
screen -S harness -X quit
SPROUT_PRIVATE_KEY="$SPROUT_PRIVATE_KEY" \
SPROUT_RELAY_URL="ws://localhost:3000" \
SPROUT_ACP_TURN_TIMEOUT=5 \
GOOSE_MODE=auto \
screen -dmS harness bash -c 'sprout-acp 2>&1 | tee /tmp/harness.log'
sleep 3
# Send a prompt that will take longer than 5 seconds
mention "$TEST_CHANNEL" "$AGENT_PUBKEY" \
"Write a detailed 500-word essay on the history of computing, then list 50 prime numbers."
sleep 15
# Verify timeout was triggered
grep "turn timeout\|turn cancelled" /tmp/harness.log | tail -5
# Reset to normal timeout
screen -S harness -X quit
SPROUT_PRIVATE_KEY="$SPROUT_PRIVATE_KEY" \
SPROUT_RELAY_URL="ws://localhost:3000" \
GOOSE_MODE=auto \
screen -dmS harness bash -c 'sprout-acp 2>&1 | tee /tmp/harness.log'
```
Expected: `turn timeout (5s) for channel ... — cancelling` then `turn cancelled for channel ...`. Harness continues running.
### Scenario F: Permission Handling (GOOSE_MODE=auto)
```bash
mention "$TEST_CHANNEL" "$AGENT_PUBKEY" \
"Use your tools to get the last 5 messages from this channel and summarize them."
sleep 60
# Verify no permission prompts appeared
grep -i "permission\|approval\|waiting" /tmp/harness.log | head -5
# Should return nothing
grep "turn complete" /tmp/harness.log | tail -3
# Should show end_turn
```
### Scenario G: Channel Discovery
```bash
# Restart harness fresh and watch discovery
screen -S harness -X quit
SPROUT_PRIVATE_KEY="$SPROUT_PRIVATE_KEY" \
SPROUT_RELAY_URL="ws://localhost:3000" \
GOOSE_MODE=auto \
screen -dmS harness bash -c 'sprout-acp 2>&1 | tee /tmp/harness.log'
sleep 5
grep "discovered\|subscribed" /tmp/harness.log
# Expected: "discovered N channel(s)" then "subscribed to channel <uuid>" for each
```
Verify channel membership via REST API:
```bash
curl -s -H "X-Pubkey: $AGENT_PUBKEY" \
"http://localhost:3000/api/channels" \
| jq '.[] | {id: .id, name: .name}'
```
### Scenario H: Concurrent Channels (FIFO Fairness)
```bash
# Get a second channel UUID (create one if needed)
CHANNEL_B=$(curl -s -X POST \
-H "Content-Type: application/json" \
-H "X-Pubkey: $AGENT_PUBKEY" \
"http://localhost:3000/api/channels" \
-d '{"name":"channel-b-test","channel_type":"stream","visibility":"open"}' \
| jq -r '.id')
# Send to channel A first, then B immediately
mention "$TEST_CHANNEL" "$AGENT_PUBKEY" "Channel A message — process me first"
mention "$CHANNEL_B" "$AGENT_PUBKEY" "Channel B message — process me second"
sleep 60
# Verify FIFO ordering in logs
grep "prompting agent for channel" /tmp/harness.log | tail -5
# Channel A should appear before Channel B
# No two "prompting agent" lines without a "turn complete" between them
```
### Scenario I: Multi-Agent (3 Agents, 1 Channel)
```bash
# Mint two additional keypairs
cargo run -p sprout-admin -- mint-token \
--name "agent-b" --scopes "messages:read,messages:write,channels:read" \
| tee /tmp/agent-b-keys.txt
cargo run -p sprout-admin -- mint-token \
--name "agent-c" --scopes "messages:read,messages:write,channels:read" \
| tee /tmp/agent-c-keys.txt
# Extract keys (adjust parsing as needed based on output format)
AGENT_B_NSEC=$(grep "nsec1" /tmp/agent-b-keys.txt | awk '{print $NF}')
AGENT_B_PUBKEY=$(grep "pubkey" /tmp/agent-b-keys.txt | awk '{print $NF}')
AGENT_C_NSEC=$(grep "nsec1" /tmp/agent-c-keys.txt | awk '{print $NF}')
AGENT_C_PUBKEY=$(grep "pubkey" /tmp/agent-c-keys.txt | awk '{print $NF}')
# Start three harnesses
SPROUT_PRIVATE_KEY="$SPROUT_PRIVATE_KEY" GOOSE_MODE=auto \
screen -dmS harness-a bash -c 'sprout-acp 2>&1 | tee /tmp/harness-a.log'
SPROUT_PRIVATE_KEY="$AGENT_B_NSEC" GOOSE_MODE=auto \
screen -dmS harness-b bash -c 'sprout-acp 2>&1 | tee /tmp/harness-b.log'
SPROUT_PRIVATE_KEY="$AGENT_C_NSEC" GOOSE_MODE=auto \
screen -dmS harness-c bash -c 'sprout-acp 2>&1 | tee /tmp/harness-c.log'
sleep 5
# Send a targeted @mention to each agent
mention "$TEST_CHANNEL" "$AGENT_PUBKEY" "Hello agent-a, reply with PONG-A"
mention "$TEST_CHANNEL" "$AGENT_B_PUBKEY" "Hello agent-b, reply with PONG-B"
mention "$TEST_CHANNEL" "$AGENT_C_PUBKEY" "Hello agent-c, reply with PONG-C"
sleep 60
# Verify three distinct replies
curl -s -H "X-Pubkey: $AGENT_PUBKEY" \
"http://localhost:3000/api/channels/$TEST_CHANNEL/messages?limit=20" \
| jq '.[] | {sender: (.pubkey[:16] + "..."), body: .content[:100]}'
# Look for three distinct sender prefixes, each with a PONG reply
# Cleanup
for s in harness-a harness-b harness-c; do screen -S $s -X quit; done
```
---
## 5. Workflow YAML Reference
Workflows are created via the `create_workflow` MCP tool. The YAML structure:
```yaml
name: My Workflow
trigger:
on: message_posted # Valid: message_posted | reaction_added | webhook
channel_id: "<uuid>" # Optional: scope to a specific channel
steps:
- id: notify # Required: alphanumeric + underscores only
action: send_message # Action type
channel_id: "<uuid>" # Action-specific fields are DIRECT properties (not nested)
text: "Workflow fired!"
```
**Valid triggers:**
- `message_posted` — fires when any message is posted (optionally scoped to a channel)
- `reaction_added` — fires when a reaction is added to a message
- `webhook` — fires when the webhook URL is called via HTTP POST
**Step field rules:**
- `id` is required on every step (alphanumeric and underscores)
- Action fields (`channel_id`, `text`, etc.) are **direct properties** of the step object — do NOT nest them under a `params` key
- `create_workflow` tool accepts the YAML as a string parameter
**Example: message_posted workflow**
```yaml
name: welcome-new-messages
trigger:
on: message_posted
channel_id: "94a444a4-c0a3-5966-ab05-530c6ddc2301"
steps:
- id: echo_reply
action: send_message
channel_id: "94a444a4-c0a3-5966-ab05-530c6ddc2301"
text: "New message detected!"
```
**Example: webhook workflow**
```yaml
name: external-trigger
trigger:
on: webhook
steps:
- id: notify_channel
action: send_message
channel_id: "94a444a4-c0a3-5966-ab05-530c6ddc2301"
text: "Webhook triggered this workflow!"
```
---
## 6. The 40 MCP Tools
The `sprout-mcp-server` exposes 40 tools covering the full Sprout feature surface. All are available to agents running via the `sprout-acp` harness.
### Channels (8)
| Tool | Description |
|------|-------------|
| `list_channels` | List all channels accessible to the agent |
| `get_channel` | Get metadata for a specific channel |
| `create_channel` | Create a new channel (`channel_type`: stream\|forum, `visibility`: open\|private) |
| `update_channel` | Update channel name or metadata |
| `archive_channel` | Archive a channel (creator only) |
| `unarchive_channel` | Restore an archived channel |
| `join_channel` | Join an open channel |
| `leave_channel` | Leave a channel |
### Messages (2)
| Tool | Description |
|------|-------------|
| `send_message` | Post a message to a channel |
| `get_channel_history` | Get recent messages from a channel |
### Threads (2)
| Tool | Description |
|------|-------------|
| `send_reply` | Reply within a message thread |
| `get_thread` | Get replies in a thread |
### Reactions (3)
| Tool | Description |
|------|-------------|
| `add_reaction` | Add an emoji reaction to a message |
| `remove_reaction` | Remove a reaction |
| `get_reactions` | List all reactions on a message |
### Direct Messages (3)
| Tool | Description |
|------|-------------|
| `open_dm` | Create or retrieve a DM channel with a user (optionally send an initial message) |
| `add_dm_member` | Add a member to an existing DM conversation |
| `list_dms` | List all DM conversations |
### Canvas (2)
| Tool | Description |
|------|-------------|
| `get_canvas` | Read the canvas document for a channel |
| `set_canvas` | Write/overwrite the canvas document (last writer wins) |
### Workflows (7)
| Tool | Description |
|------|-------------|
| `list_workflows` | List all workflows |
| `create_workflow` | Create a new workflow with trigger and steps |
| `update_workflow` | Update an existing workflow |
| `delete_workflow` | Delete a workflow |
| `trigger_workflow` | Manually trigger a webhook workflow |
| `get_workflow_runs` | Get execution history for a workflow |
| `approve_workflow_step` | Approve a pending approval step in a workflow run |
### Feed (3)
| Tool | Description |
|------|-------------|
| `get_feed` | Get the agent's personal activity feed |
| `get_feed_mentions` | Get mentions from the agent's feed |
| `get_feed_actions` | Get action items from the agent's feed |
### Search (1)
| Tool | Description |
|------|-------------|
| `search` | Full-text search across messages and channels |
### Profile (3)
| Tool | Description |
|------|-------------|
| `set_profile` | Set display name, about/bio, avatar URL, and NIP-05 handle |
| `get_user_profile` | Get any user's profile by pubkey (omit pubkey for own profile) |
| `get_users_batch` | Bulk resolve display names and NIP-05 handles for multiple pubkeys |
### Presence (1)
| Tool | Description |
|------|-------------|
| `get_presence` | Bulk presence lookup by pubkey |
### Members (3)
| Tool | Description |
|------|-------------|
| `add_channel_member` | Add a user (by pubkey) to a channel |
| `remove_channel_member` | Remove a member from a channel |
| `list_channel_members` | List members of a channel |
### Admin (2)
| Tool | Description |
|------|-------------|
| `set_channel_topic` | Set the topic for a channel |
| `set_channel_purpose` | Set the purpose for a channel |
---
## 7. Cleanup
### Stop harness instances
```bash
# E2E test harnesses
for s in agent-alice agent-bob agent-charlie; do
screen -S $s -X quit 2>/dev/null && echo "stopped $s" || echo "$s not running"
done
# Advanced scenario harnesses
for s in harness harness-a harness-b harness-c; do
screen -S $s -X quit 2>/dev/null
done
```
### Stop relay
```bash
screen -S relay -X quit 2>/dev/null && echo "relay stopped"
```
### Verify all sessions gone
```bash
screen -ls
# Should show "No Sockets found" or only unrelated sessions
```
### Tear down Docker services
```bash
# Stop services and remove volumes (full reset)
docker compose down -v
# Stop services only (preserve data for next run)
docker compose down
```
### Clean up temp files
```bash
rm -f /tmp/agent-alice.log /tmp/agent-bob.log /tmp/agent-charlie.log
rm -f /tmp/harness.log /tmp/harness-a.log /tmp/harness-b.log /tmp/harness-c.log
rm -f /tmp/sprout-relay.log
rm -f /tmp/alice-keys.txt /tmp/agent-b-keys.txt /tmp/agent-c-keys.txt
```
---
## 8. Known Issues / Troubleshooting
### Current Status
All automated tests pass as of 2026-03-11:
- ✅ 32/32 REST API integration tests
- ✅ 13/13 WebSocket relay integration tests
- ✅ 10/10 MCP server integration tests
- ✅ Multi-agent E2E (Alice/Bob/Charlie) via sprout-acp harness
---
### Harness exits immediately with "configuration error"
**Cause:** `SPROUT_PRIVATE_KEY` not set or invalid.
```bash
echo $SPROUT_PRIVATE_KEY
# Must be a valid nsec1... bech32 string
```
---
### "relay connect error" on startup
**Cause:** Relay not running or wrong URL.
```bash
# Check relay session
screen -ls | grep relay
# If missing, start it
screen -dmS relay bash -c \
'export $(cat .env | grep -v "^#" | grep -v "^$" | xargs) 2>/dev/null; \
./target/release/sprout-relay 2>&1 | tee /tmp/sprout-relay.log'
# Verify
curl -s http://localhost:3000/health
```
---
### "discovered 0 channel(s)"
**Cause:** Agent pubkey is not a member of any open channels.
```bash
# Check what channels are accessible
curl -s -H "X-Pubkey: $ALICE_PUBKEY" \
"http://localhost:3000/api/channels" | jq 'length'
```
Open channels are accessible to any authenticated pubkey. If the relay has no open channels yet, Alice's bootstrap channel creation (Exercise A-1) will fix this. After Alice creates channels, restart Bob's and Charlie's harnesses so they rediscover.
---
### "failed to spawn agent"
**Cause:** `goose` binary not found or not on `$PATH`.
```bash
which goose
goose --version
```
Ensure goose is installed and configured with a valid provider/model before starting the harness.
---
### Agent hangs, turn never completes
**Cause:** `GOOSE_MODE=auto` not set — goose is waiting for permission approval.
```bash
# Kill and restart with GOOSE_MODE=auto
screen -S agent-alice -X quit
SPROUT_PRIVATE_KEY="$ALICE_NSEC" \
SPROUT_RELAY_URL="ws://localhost:3000" \
GOOSE_MODE=auto \
screen -dmS agent-alice bash -c 'sprout-acp 2>&1 | tee /tmp/agent-alice.log'
```
`GOOSE_MODE=auto` is **mandatory** for all harness instances. Without it, the first MCP tool call will hang indefinitely.
---
### No agent reply after @mention
Checklist:
1. Is the harness running? `pgrep -a sprout-acp`
2. Is the harness subscribed to the target channel? `grep "subscribed" /tmp/agent-alice.log`
3. Did `mention` use the correct pubkey hex? Check `$ALICE_PUBKEY` is set correctly.
4. Check harness logs for errors: `tail -50 /tmp/agent-alice.log`
5. Verify via REST API that the @mention event arrived:
```bash
curl -s -H "X-Pubkey: $ALICE_PUBKEY" \
"http://localhost:3000/api/channels/$GENERAL/messages?limit=10" \
| jq '.[] | {kind: .kind, sender: .pubkey[:16], body: .content[:100]}'
```
---
### MCP tool calls failing
**Cause:** `sprout-mcp-server` binary not found, or wrong relay URL passed to MCP.
```bash
which sprout-mcp-server
# If missing: cargo build --release -p sprout-mcp-server
# Then: export PATH="$PWD/target/release:$PATH"
```
---
### Channel UUIDs not set after Alice's first exercise
If `$GENERAL`, `$ALICE_TESTING`, or `$PRIVATE_OPS` are empty, Alice may not have finished yet. Wait longer, then re-query:
```bash
sleep 30
export GENERAL=$(curl -s -H "X-Pubkey: $ALICE_PUBKEY" \
"http://localhost:3000/api/channels" \
| jq -r '.[] | select(.name == "general") | .id')
echo "GENERAL=$GENERAL"
```
If still empty, check Alice's harness logs for errors: `tail -50 /tmp/agent-alice.log`.
---
### Stale events replayed on harness restart
**Expected behavior.** On startup, the harness replays all unprocessed `@mentions` since the last run. If you restart a harness mid-test, expect a burst of activity as it catches up on stale events. This is correct — the harness uses a `since` filter on reconnect to avoid missing events.
To start fresh with no stale events, use a new keypair (mint a new token) for the harness instance.
---
### Docker services unhealthy
```bash
docker compose ps
# If any service is not "Up":
docker compose down -v && docker compose up -d
# Wait 30s then re-run migrations:
sqlx migrate run --database-url "$DATABASE_URL"
```