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 and Redis
(started automatically if not already running). Neither task runs the E2E suites in
buzz-test-client — those are marked #[ignore] and require a running relay:
# Start a relay first (see below), then:
cargo test -p buzz-test-client -- --ignoredThe fastest way to exercise the relay end-to-end is to build the release
binaries once, run buzz-relay, and drive it with the buzz CLI. The
CLI signs every request with NIP-98, so you don't need nak or hand-rolled
curl.
. ./bin/activate-hermit # activate pinned toolchain
cp .env.example .env # one-time
just setup # start Docker services, run migrationsAlready running Buzz Desktop? Desktop uses the same Docker container names (
buzz-postgres,buzz-redis) and the same default ports (:5432,:6379).just setupwill reuse those services, so your test relay writes into Desktop's database. That's fine for read/write smoke tests, but:just resetwipes 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=buzz-dev docker compose …).
just reset wipes all local data and starts over — including Buzz
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
BUZZ_AUTH_TAG,BUZZ_RELAY_URL, orBUZZ_PRIVATE_KEYfrom a prior session (or a staging config),unsetthem before continuing. A staleBUZZ_AUTH_TAGfails the local dev relay withauth_error: signature verification failedon the first CLI write — it is not tolerated.unset BUZZ_AUTH_TAG BUZZ_RELAY_URL BUZZ_PRIVATE_KEY
cargo build --release -p buzz-relay -p buzz-cli -p buzz-admin
export PATH="$PWD/target/release:$PATH"Rebuild after any code change — the steps below use the release binaries.
In a separate terminal (it runs in the foreground):
buzz-relay # release binary from step 2, serves ws://localhost:3000
# alternatives:
# cargo run --release -p buzz-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):
curl -s http://localhost:3000/health # → ok
curl -s http://localhost:8080/_readiness # → {"status":"ready"}Health/readiness/liveness live on a separate port (default
8080,BUZZ_HEALTH_PORT) so K8s probes bypass auth middleware. The main app port also exposes/healthfor convenience.
The relay starts in dev mode (BUZZ_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 Buzz Desktop (or another relay) on
:3000/:8080/:9102? Buzz 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
buzz-relay):export BUZZ_BIND_ADDR=0.0.0.0:3030 export BUZZ_HEALTH_PORT=8088 export BUZZ_METRICS_PORT=9202 export RELAY_URL=ws://localhost:3030 # advertised in NIP-42 challenges buzz-relayIn your working / CLI terminal (for steps 4+ and the ACP harness):
export BUZZ_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 snippet later in this doc shows the defaults. When you see
localhost:3000/:8080in a code block, mentally substitute your overrides — or the CLI will end up talking to Buzz Desktop's relay.
Ignore
just setup's "Next steps" banner. It still printsjust relay(a debug build). Usebuzz-relayfrom 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 buzz-relay. Leaving
it running will collide with the next reviewer who follows this doc on
the same machine.
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.
# Generate a keypair
GEN=$(buzz-admin generate-key)
export BUZZ_PRIVATE_KEY=$(echo "$GEN" | awk '/Secret key:/ {print $3}')
PUBKEY=$(echo "$GEN" | awk '/Public key:/ {print $3}')
echo "pubkey: $PUBKEY"
# Create a channel — the UUID is returned in the response
CHANNEL=$(buzz channels create --name "smoke-$$" --type stream --visibility open | jq -r '.channel_id')
echo "channel: $CHANNEL"
# Send a message and read it back
SEND=$(buzz messages send --channel "$CHANNEL" --content "hello from smoke test")
EVENT_ID=$(echo "$SEND" | jq -r '.event_id')
buzz messages get --channel "$CHANNEL" --limit 5 | jq .
# Fetch the reply chain for a specific message (empty array on a leaf — that's fine)
buzz 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. thread returns []
for a leaf message — populated only after a reply comes in (see §5).
For full coverage of every CLI command (54 subcommands across 12 groups),
follow crates/buzz-cli/TESTING.md.
The relay's HTTP bridge accepts three endpoints — useful if you're testing
a client other than buzz-cli:
| Endpoint | Purpose |
|---|---|
POST /events |
Submit a signed Nostr event |
POST /query |
NIP-01 filter query (returns events) |
POST /count |
NIP-45 count query |
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 buzz messages thread.
buzz-acp connects an ACP-speaking agent (goose, codex, claude code,
buzz-agent) to the relay. The harness listens for events, drives the
agent over stdio, and the agent replies through MCP tools.
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 (BUZZ_ACP_RESPOND_TO=anyone still skips events
the agent signed itself).
cargo build --release -p buzz-acp
export PATH="$PWD/target/release:$PATH"
# 1. Save your sender identity from step 4 — you'll need it to @mention the agent
SENDER_SK="$BUZZ_PRIVATE_KEY"
# 2. Mint a fresh agent identity and capture its pubkey
AGENT_GEN=$(buzz-admin generate-key)
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.
buzz channels add-member --channel "$CHANNEL" --pubkey "$AGENT_PUBKEY" --role member
# 4. Switch to the agent identity and start it.
# buzz-acp wants ws:// (not http://). If you set BUZZ_RELAY_URL to an
# http:// URL in step 3, set the ws:// equivalent here — same host/port.
export BUZZ_PRIVATE_KEY="$AGENT_SK"
export BUZZ_RELAY_URL=ws://localhost:3000 # match step 3 (e.g. ws://localhost:3030 if overridden)
export BUZZ_ACP_RESPOND_TO=anyone # default is owner-only; opens the gate for testing
# NIP-AE core-memory prompt injection is on by default; set BUZZ_ACP_NO_MEMORY=true to opt out.
export GOOSE_MODE=auto # must be 'auto' or goose hangs on prompts
buzz-acp # foreground; logs to stdout (run in a separate terminal)
# Optional: turn on per-turn tracing if the default log is too quiet.
# RUST_LOG=buzz_acp=debug buzz-acpUsing a different ACP agent? The default recipe assumes
gooseis on$PATHand configured (goose --versionshould print). For codex / claude code / buzz-agent, setBUZZ_ACP_AGENT_COMMANDandBUZZ_ACP_AGENT_ARGSaccordingly — seecrates/buzz-acp/README.md. Without these, buzz-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 …).
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/buzz-acp/README.md for parallel agents, heartbeats,
respond-to gates, and forum subscriptions.
Send the agent a task — switch your shell back to the sender identity from step 4 and @mention the agent:
export BUZZ_PRIVATE_KEY=$SENDER_SK # the key from step 4
buzz messages send --channel "$CHANNEL" \
--content "Hey agent, reply PONG only."
# 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
# `buzz messages get` is how you confirm it ran.
buzz messages get --channel "$CHANNEL" --limit 5 | jq '.[] | {pubkey, content}'Replies are kind:9 in the same channel; buzz messages thread --channel <id> --event <event_id> fetches the reply chain for a specific mention.
The relay reads all configuration from environment variables. Defaults work
out of the box with just setup or just relay. Common overrides:
| Variable | Default | Notes |
|---|---|---|
BUZZ_BIND_ADDR |
0.0.0.0:3000 |
Main app port |
BUZZ_HEALTH_PORT |
8080 |
/_liveness, /_readiness |
BUZZ_METRICS_PORT |
9102 |
Prometheus /metrics |
RELAY_URL |
ws://localhost:3000 |
Advertised in NIP-11 / NIP-42 challenges. Note: no BUZZ_ prefix. |
DATABASE_URL |
postgres://buzz:buzz_dev@localhost:5432/buzz |
|
REDIS_URL |
redis://localhost:6379 |
|
BUZZ_REQUIRE_AUTH_TOKEN |
false |
When true, REST requires NIP-98 (no X-Pubkey fallback) |
BUZZ_REQUIRE_RELAY_MEMBERSHIP |
false |
When true, only pubkeys in relay_members can connect |
BUZZ_AUTO_MIGRATE |
false |
Opt in with true/1/yes/on to run embedded SQLx migrations on relay startup |
RELAY_OWNER_PUBKEY |
unset | Bootstrapped as owner in relay_members at first start |
BUZZ_ALLOW_NIP_OA_AUTH |
false |
Enable NIP-OA owner attestation for membership |
CLI-side, only two matter for testing:
| Variable | Default | Notes |
|---|---|---|
BUZZ_RELAY_URL |
http://localhost:3000 |
CLI relay base; accepts ws(s):// and normalises |
BUZZ_PRIVATE_KEY |
— (required) | nsec1… or 64-char hex |
BUZZ_AUTH_TAG |
unset | Optional NIP-OA owner attestation JSON |
| 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 (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 buzz-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: BUZZ_PRIVATE_KEY is required |
Env not exported into the CLI's shell | export BUZZ_PRIVATE_KEY=... (or pass --private-key) |
auth_error: BUZZ_AUTH_TAG verification failed … signature verification failed |
A stale BUZZ_AUTH_TAG inherited from a parent shell. The local dev relay rejects it. |
unset BUZZ_AUTH_TAG (see the scrub block in step 1) |
auth-required: verification failed on a closed relay |
NIP-OA attestation needed | Set BUZZ_AUTH_TAG to the owner-issued JSON, or relax BUZZ_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 | BUZZ_ACP_RESPOND_TO=owner-only (default) with no owner configured |
Set BUZZ_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 | buzz 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) |