mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Completes the definition surface: `buzz teams create|list|get|delete` writes kind:30176 over the personas from [3/4], from flags or a Desktop `.team.json` export. Membership resolution is the load-bearing part. A team event stores persona d-tags, and Desktop publishes personas under their record id — usually a UUID — while its team export names members only by display name. Matching on the slugified name would therefore resolve nothing for every Desktop-exported roster. `--persona` accepts a d-tag, a slugified d-tag, or a unique display name, in that order; an ambiguous display name is an error rather than a guess, and a member with no published persona is refused rather than published as a seat that silently stays empty. A team id is used verbatim, never normalized. The relay enforces the slug grammar on persona d-tags but only a length bound on team ids, and Desktop writes raw UUIDs and ids like `builtin-team:welcome` — normalizing would address a different coordinate than the one Desktop published, making its teams unreachable. Since kind:30176 has no envelope validator on the relay, the CLI is the only guard against a blank or oversized id. `instructions` and `persona_ids` are always published. On the wire an absent field means "publisher predates always-publish, membership unknown, preserve local" — distinct from an explicit empty. A new client must never claim the former, or a write meant to clear a roster reads as "leave it alone" and one meant to leave it alone wipes it. The e2e suite gains the relay rule the delete path depends on: a tombstone older than its target head is accepted, deletes nothing, and still reports OK. Signed-off-by: Max Lampert <maxwell@squareup.com>
756 lines
27 KiB
Markdown
756 lines
27 KiB
Markdown
# buzz-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
|
||
# buzz-postgres healthy
|
||
# buzz-redis healthy
|
||
```
|
||
|
||
If not running: `just setup` from the repo root.
|
||
|
||
Tools: `jq`, `curl`, Rust toolchain.
|
||
|
||
---
|
||
|
||
## 2. Build the CLI
|
||
|
||
```bash
|
||
cargo build -p buzz-cli
|
||
```
|
||
|
||
Use `cargo run -p buzz-cli --` or the built binary at `target/debug/buzz`.
|
||
|
||
---
|
||
|
||
## 3. Start the Relay
|
||
|
||
In a separate terminal:
|
||
|
||
```bash
|
||
cd REPOS/buzz-nostr
|
||
set -a && source .env && set +a
|
||
cargo run -p buzz-relay
|
||
```
|
||
|
||
Verify:
|
||
|
||
```bash
|
||
curl -s http://localhost:3000/_liveness
|
||
# "ok" or 200 status
|
||
```
|
||
|
||
The `.env` should have `BUZZ_REQUIRE_AUTH_TOKEN=false` for local dev.
|
||
|
||
---
|
||
|
||
## 4. Mint Test Credentials
|
||
|
||
### Option A: buzz-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="${DATABASE_URL:?set DATABASE_URL for the local Buzz database}" \
|
||
cargo run -p buzz-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 `BUZZ_PRIVATE_KEY` testing
|
||
|
||
Export:
|
||
|
||
```bash
|
||
export BUZZ_RELAY_URL="http://localhost:3000"
|
||
export BUZZ_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`, `users set-status` |
|
||
| `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 buzz-cli
|
||
# Expected: see cargo test -p buzz-cli for current count
|
||
|
||
cargo clippy -p buzz-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)
|
||
buzz channels create --name "test-stream" --type stream --visibility open \
|
||
--description "CLI test channel" | jq .
|
||
# Save the channel ID:
|
||
CHANNEL_ID=$(buzz channels create --name "test-cli" --type stream --visibility open | jq -r '.channel_id')
|
||
# Expected: {"event_id":"...","accepted":true,"message":"...","channel_id":"<uuid>"}
|
||
|
||
# channels create (forum) — needed for messages vote later
|
||
FORUM_ID=$(buzz channels create --name "test-forum" --type forum --visibility open | jq -r '.channel_id')
|
||
|
||
# channels list
|
||
buzz channels list | jq .
|
||
# Expected: [{"channel_id":"...","name":"...","description":"...","created_at":N}]
|
||
buzz channels list --visibility open | jq .
|
||
buzz channels list --member | jq .
|
||
|
||
# channels get
|
||
buzz channels get --channel "$CHANNEL_ID" | jq .
|
||
# Expected: {"channel_id":"...","name":"...","description":"...","created_at":N,"pubkey":"..."} or null
|
||
|
||
# channels update
|
||
buzz channels update --channel "$CHANNEL_ID" --name "test-cli-updated" \
|
||
--description "Updated" | jq .
|
||
# Expected: {"event_id":"...","accepted":true,"message":"..."}
|
||
|
||
# channels topic
|
||
buzz channels topic --channel "$CHANNEL_ID" --topic "Test topic" | jq .
|
||
# Expected: {"event_id":"...","accepted":true,"message":"..."}
|
||
|
||
# channels purpose
|
||
buzz channels purpose --channel "$CHANNEL_ID" --purpose "Testing" | jq .
|
||
# Expected: {"event_id":"...","accepted":true,"message":"..."}
|
||
|
||
# channels join (may already be a member from create)
|
||
buzz channels join --channel "$CHANNEL_ID" | jq .
|
||
# Expected: {"event_id":"...","accepted":true,"message":"..."}
|
||
|
||
# channels leave
|
||
# NOTE: Fails with 400 "cannot remove the last owner" if this identity is the
|
||
# sole owner (which it is after channels create). To test leave successfully,
|
||
# first add-member a second pubkey as owner. The relay enforces ≥1 owner.
|
||
buzz channels leave --channel "$CHANNEL_ID" | jq .
|
||
# Expected: {"event_id":"...","accepted":true,"message":"..."} (or 400 if last owner)
|
||
|
||
# Re-join so we can send messages
|
||
buzz channels join --channel "$CHANNEL_ID" | jq .
|
||
# Expected: {"event_id":"...","accepted":true,"message":"..."}
|
||
|
||
# channels archive (requires admin:channels scope)
|
||
buzz channels archive --channel "$CHANNEL_ID" | jq .
|
||
# Expected: {"event_id":"...","accepted":true,"message":"..."}
|
||
|
||
# channels unarchive
|
||
buzz channels unarchive --channel "$CHANNEL_ID" | jq .
|
||
# Expected: {"event_id":"...","accepted":true,"message":"..."}
|
||
```
|
||
|
||
### 6.2 Canvas
|
||
|
||
```bash
|
||
# canvas set
|
||
buzz canvas set --channel "$CHANNEL_ID" --content "# Test Canvas" | jq .
|
||
|
||
# canvas set from stdin
|
||
echo "# Canvas from stdin" | buzz canvas set --channel "$CHANNEL_ID" --content - | jq .
|
||
|
||
# canvas get
|
||
buzz canvas get --channel "$CHANNEL_ID"
|
||
# Expected: raw markdown string, or: null
|
||
```
|
||
|
||
### 6.3 Messages
|
||
|
||
```bash
|
||
# messages send
|
||
MSG=$(buzz messages send --channel "$CHANNEL_ID" --content "Hello from CLI test" | jq .)
|
||
echo "$MSG"
|
||
EVENT_ID=$(echo "$MSG" | jq -r '.event_id')
|
||
|
||
# messages send with reply + broadcast
|
||
REPLY=$(buzz messages send --channel "$CHANNEL_ID" --content "Reply" \
|
||
--reply-to "$EVENT_ID" --broadcast | jq .)
|
||
echo "$REPLY"
|
||
REPLY_ID=$(echo "$REPLY" | jq -r '.event_id')
|
||
|
||
# messages send with mentions — @name in content is auto-resolved, no flag needed
|
||
buzz messages send --channel "$CHANNEL_ID" --content "Hey @someone" | jq .
|
||
|
||
# messages send with NIP-27 nostr:npub1… inline mention — auto-resolved to p-tag
|
||
buzz messages send --channel "$CHANNEL_ID" \
|
||
--content "Check with nostr:npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg on this" | 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.' \
|
||
| buzz messages send --channel "$CHANNEL_ID" --content - | jq .
|
||
|
||
# messages get
|
||
buzz messages get --channel "$CHANNEL_ID" | jq .
|
||
buzz messages get --channel "$CHANNEL_ID" --limit 5 | jq .
|
||
|
||
# messages thread
|
||
buzz messages thread --channel "$CHANNEL_ID" --event "$EVENT_ID" | jq .
|
||
|
||
# messages search
|
||
buzz messages search --query "Hello" | jq .
|
||
buzz messages search --query "CLI test" --limit 5 | jq .
|
||
|
||
# messages edit
|
||
buzz messages edit --event "$EVENT_ID" --content "Edited by CLI test" | jq .
|
||
|
||
# messages delete
|
||
buzz 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() {}' | buzz messages send-diff \
|
||
--channel "$CHANNEL_ID" \
|
||
--diff - \
|
||
--repo "https://github.com/example/repo" \
|
||
--commit "abcdef1234567890abcdef1234567890abcdef12" | jq .
|
||
|
||
# messages send-diff with metadata
|
||
echo "diff content" | buzz 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" | buzz 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=$(buzz messages send --channel "$CHANNEL_ID" --content "React to this")
|
||
REACT_ID=$(echo "$REACT_MSG" | jq -r '.event_id')
|
||
|
||
# reactions add
|
||
buzz reactions add --event "$REACT_ID" --emoji "👍" | jq .
|
||
|
||
# reactions get
|
||
buzz reactions get --event "$REACT_ID" | jq .
|
||
# Expected: {"reactions":[{"emoji":"...","count":N,"pubkeys":["..."]}]}
|
||
|
||
# reactions remove
|
||
buzz reactions remove --event "$REACT_ID" --emoji "👍" | jq .
|
||
```
|
||
|
||
### 6.6 DMs
|
||
|
||
```bash
|
||
# dms list
|
||
buzz dms list | jq .
|
||
# Expected: [{"dm_id":"...","participants":["..."],"created_at":N}]
|
||
|
||
# dms open (needs a real pubkey — use your own or a test one)
|
||
# Get your own pubkey first:
|
||
MY_PUBKEY=$(buzz users get | jq -r '.[0].pubkey // empty')
|
||
echo "My pubkey: $MY_PUBKEY"
|
||
|
||
# dms open with a synthetic pubkey (relay will create the user)
|
||
DM_RESULT=$(buzz dms open --pubkey "0000000000000000000000000000000000000000000000000000000000000001")
|
||
echo "$DM_RESULT" | jq .
|
||
# Expected: {"event_id":"...","accepted":true,"message":"...","dm_id":"<uuid>"}
|
||
DM_ID=$(echo "$DM_RESULT" | jq -r '.dm_id')
|
||
|
||
# dms add-member (requires messages:write scope — NOT admin:channels)
|
||
buzz dms add-member --channel "$DM_ID" \
|
||
--pubkey "0000000000000000000000000000000000000000000000000000000000000002" | jq .
|
||
```
|
||
|
||
### 6.7 Users & Presence
|
||
|
||
```bash
|
||
# users get — own profile (0 pubkeys)
|
||
buzz users get | jq .
|
||
# Expected: [{...profile...}] — always returns an array, even for single results
|
||
|
||
# users get — single pubkey
|
||
buzz users get --pubkey "$MY_PUBKEY" | jq .
|
||
|
||
# users get — batch (2+ pubkeys)
|
||
buzz users get --pubkey "$MY_PUBKEY" --pubkey "$MY_PUBKEY" | jq .
|
||
|
||
# users set-profile
|
||
buzz users set-profile --name "CLI Test Agent" --about "Testing buzz-cli" | jq .
|
||
|
||
# users presence
|
||
buzz users presence --pubkeys "$MY_PUBKEY" | jq .
|
||
|
||
# users set-presence
|
||
buzz users set-presence --status online | jq .
|
||
buzz users set-presence --status away | jq .
|
||
buzz users set-presence --status offline | jq .
|
||
# Note: set-presence may fail — kind:20001 is ephemeral and rejected by the HTTP bridge
|
||
|
||
# users set-status — NIP-38 kind:30315 on the d:general coordinate
|
||
buzz users set-status --text "reviewing PRs" --emoji "🔍" | jq .
|
||
buzz users set-status --text "no emoji this time" | jq .
|
||
|
||
# users set-status — emoji-only status (intentional: text is blank, emoji is kept)
|
||
buzz users set-status --text "" --emoji "🎶" | jq .
|
||
|
||
# users set-status --clear — removes the status (empty content, d:general only)
|
||
buzz users set-status --clear | jq .
|
||
|
||
# --clear is mutually exclusive with --text/--emoji
|
||
buzz users set-status --clear --text "nope" 2>&1; echo "exit: $?"
|
||
# Expected: exit 1 — clap conflict error
|
||
```
|
||
|
||
### 6.8 Channel Members (add/remove require admin:channels)
|
||
|
||
```bash
|
||
# channels add-member
|
||
buzz channels add-member --channel "$CHANNEL_ID" \
|
||
--pubkey "0000000000000000000000000000000000000000000000000000000000000001" \
|
||
--role member | jq .
|
||
|
||
# channels members
|
||
buzz channels members --channel "$CHANNEL_ID" | jq .
|
||
# Expected: [{"pubkey":"...","role":"..."}]
|
||
|
||
# channels remove-member
|
||
buzz 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=$(buzz 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 '.workflow_id')
|
||
|
||
# workflows list
|
||
buzz workflows list --channel "$CHANNEL_ID" | jq .
|
||
|
||
# workflows get
|
||
buzz workflows get --workflow "$WF_ID" | jq .
|
||
# Expected: {"workflow_id":"...","content":"<yaml>","created_at":N,"pubkey":"..."} or null
|
||
|
||
# workflows update (requires --channel)
|
||
buzz workflows update --channel "$CHANNEL_ID" --workflow "$WF_ID" \
|
||
--yaml 'name: test-wf-updated
|
||
trigger:
|
||
on: webhook
|
||
steps:
|
||
- id: step1
|
||
action: send_message
|
||
text: "Updated"' | jq .
|
||
|
||
# workflows trigger
|
||
# NOTE: May return 400 "workflow not found" — the relay indexes workflow
|
||
# definitions into a DB table asynchronously. If the definition event hasn't
|
||
# been indexed yet, the trigger handler won't find it.
|
||
buzz workflows trigger --workflow "$WF_ID" | jq .
|
||
|
||
# workflows runs
|
||
buzz workflows runs --workflow "$WF_ID" | jq .
|
||
# Expected: [] — relay stores runs in DB, not as Nostr events; empty is normal
|
||
|
||
# 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:
|
||
buzz 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: buzz workflows approve --token <UUID> --approved false
|
||
|
||
# workflows delete
|
||
buzz workflows delete --workflow "$WF_ID" | jq .
|
||
```
|
||
|
||
### 6.10 Feed
|
||
|
||
```bash
|
||
buzz feed get | jq .
|
||
buzz feed get --limit 5 | jq .
|
||
# Expected: [{id,pubkey,kind,content,created_at,tags}] — sig-stripped, sorted newest-first
|
||
```
|
||
|
||
### 6.11 Forum & Voting
|
||
|
||
```bash
|
||
# Send a forum post (kind 45001) to the forum channel
|
||
FORUM_POST=$(buzz 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 '.event_id')
|
||
|
||
# messages vote (up)
|
||
buzz messages vote --event "$FORUM_EVENT_ID" --direction up | jq .
|
||
|
||
# messages vote (down)
|
||
buzz messages vote --event "$FORUM_EVENT_ID" --direction down | jq .
|
||
```
|
||
|
||
### 6.12 Notes (NIP-23 long-form, kind:30023)
|
||
|
||
Editable team-knowledge notes keyed by `(kind:30023, you, d=slug)`. `set` is an
|
||
idempotent upsert; `rm` is a NIP-09 a-tag deletion. Output is plain text (refs),
|
||
not JSON — except `get`/`ls`, which emit JSON.
|
||
|
||
```bash
|
||
# set (first publish — --title required, body from stdin)
|
||
cat <<'EOF' | buzz notes set --name dco-check --title "DCO Check" \
|
||
--summary "How we verify DCO" --tag dco --tag ci --content -
|
||
Run `git log --format='%(trailers:key=Signed-off-by)'` ...
|
||
EOF
|
||
# → prints event_id / naddr / coordinate / slug / title
|
||
|
||
# set (edit — omit --title to carry it forward; published_at preserved)
|
||
echo "Updated body." | buzz notes set --name dco-check --content -
|
||
|
||
# get by name (own author resolves directly; cross-author #d query otherwise)
|
||
buzz notes get --name dco-check | jq .
|
||
buzz notes get --name dco-check --content-only
|
||
|
||
# get by naddr (exact coordinate; paste the naddr from a set/get above)
|
||
buzz notes get --naddr "$NADDR" | jq .
|
||
|
||
# ls (own by default; --author all across the team; --tag filters)
|
||
buzz notes ls | jq .
|
||
buzz notes ls --tag dco | jq .
|
||
buzz notes ls --author all --limit 10 | jq .
|
||
|
||
# rm (NIP-09 a-tag deletion; subsequent get must 404)
|
||
buzz notes rm --name dco-check
|
||
# → prints deleted <coordinate> / deletion <event-id>
|
||
buzz notes get --name dco-check # exits non-zero: not found
|
||
|
||
# rm of a slug you never published → NotFound, no kind:5 emitted
|
||
buzz notes rm --name does-not-exist # exits non-zero
|
||
```
|
||
|
||
### 6.13 Personas & Teams (NIP-AP, kind:30175 / kind:30176)
|
||
|
||
Owner-authored agent definitions and rosters. These are the same coordinates
|
||
Buzz Desktop reads, so run them with the **same key as the Desktop you expect
|
||
the definitions to appear in** — a different key writes to a different
|
||
coordinate space and Desktop shows nothing.
|
||
|
||
```bash
|
||
# create (flags)
|
||
buzz personas create --display-name "Herring" --prompt "Ask the annoying question." \
|
||
--runtime claude --model claude-opus-5 | jq .
|
||
# → {event_id, accepted, message, slug} — slug is the d-tag ("herring")
|
||
|
||
# create (from a Desktop export; --replace required to overwrite)
|
||
buzz personas create --from ~/Downloads/Herring.agent.json --replace | jq .
|
||
|
||
# avatars: --avatar takes a local image, --avatar-url takes a hosted URL.
|
||
# Every raster is downscaled to 512px and re-encoded, then measured: it rides
|
||
# inline in the event when it fits (so Desktop renders it without a fetch),
|
||
# and uploads when it doesn't. Flat art usually lands back inside the bound.
|
||
buzz personas create --display-name "Herring" --prompt x --avatar ~/Pictures/herring.png | jq .
|
||
|
||
# a photo straight off a camera — carries EXIF, which media storage refuses.
|
||
# Re-encoding drops it, so this must succeed rather than fail with a 422.
|
||
buzz personas create --display-name "Herring" --prompt x --avatar ~/Pictures/IMG_1234.jpg --replace | jq .
|
||
# check the avatar is upright: a rotated result means EXIF orientation was
|
||
# dropped instead of applied.
|
||
|
||
# a small photo rides inline, which skips media storage entirely — so confirm
|
||
# the CLI, not the validator, is what dropped the EXIF:
|
||
buzz personas get herring --json | jq -r '.[0].content' \
|
||
| jq -r .avatar_url | sed 's/^data:[^,]*,//' | base64 -d | exiftool -
|
||
# → no GPS, no Make/Model, no colour profile
|
||
|
||
# list / get (the slug is positional, matching `projects` and `mem`)
|
||
buzz personas list
|
||
buzz personas get herring
|
||
buzz personas get herring --json | jq . # sig-stripped array of one
|
||
|
||
# create a team; members resolve by d-tag OR persona display name
|
||
buzz teams create --name "Red team" --persona Herring --persona quinby | jq .
|
||
# → {event_id, accepted, message, team_id}
|
||
buzz teams create --from ~/Downloads/Red\ team.team.json --replace | jq .
|
||
buzz teams get red-team
|
||
|
||
# delete (NIP-09 a-tag tombstone; the CLI re-reads to confirm the coordinate
|
||
# is gone, because the relay accepts a tombstone that deleted nothing)
|
||
buzz teams delete red-team
|
||
buzz personas delete herring
|
||
buzz personas get herring # exits non-zero: not found
|
||
```
|
||
|
||
Checks worth making by hand:
|
||
|
||
```bash
|
||
# A member with no published persona is refused rather than seated empty
|
||
buzz teams create --name "Ghost team" --persona nobody-published; echo "exit: $?" # 1
|
||
|
||
# Desktop rejects invisible characters in definition text; so must the CLI
|
||
buzz personas create --display-name $'Reviewer' --prompt x; echo "exit: $?" # 1
|
||
|
||
# Re-creating without --replace is a write conflict, not a silent overwrite
|
||
buzz personas create --display-name "Herring" --prompt y; echo "exit: $?" # 5
|
||
|
||
# --replace keeps catalog visibility: --shared survives a replace that omits it
|
||
buzz personas create --display-name "Herring" --prompt x --shared --replace
|
||
buzz personas create --display-name "Herring" --prompt y --replace
|
||
buzz personas get herring | grep shared # → shared: true
|
||
|
||
# A team created from --name is addressed by its slug; the miss says so
|
||
buzz teams create --name "Red team" --persona herring
|
||
buzz teams get "Red team"; echo "exit: $?" # 1, error names 'red-team'
|
||
buzz teams get red-team # found
|
||
|
||
# A team past the relay's content cap fails locally, before members resolve
|
||
buzz teams create --name "Red team" --instructions "$(head -c 300000 /dev/zero | tr '\0' a)"
|
||
echo "exit: $?" # 1, "too large to publish"
|
||
|
||
# A non-image --avatar is refused locally rather than after a round trip
|
||
buzz personas create --display-name "Herring" --prompt x --avatar ./notes.txt; echo "exit: $?" # 1
|
||
|
||
# An --avatar-url Desktop's reader drops is refused rather than published to
|
||
# render as nothing
|
||
buzz personas create --display-name "Herring" --prompt x \
|
||
--avatar-url 'ftp://example.test/h.png'; echo "exit: $?" # 1
|
||
|
||
# Bounds Desktop enforces at mint, so the CLI must not publish past them
|
||
buzz personas create --display-name "Herring" --prompt x --parallelism 99; echo "exit: $?"
|
||
# → "99 is not in 1..=32", exit 1
|
||
|
||
# Re-importing a persona Desktop already published must not mint a second
|
||
# coordinate. Desktop publishes in-app personas under their record UUID, so
|
||
# the import adopts that id rather than the slug its name derives.
|
||
buzz personas create --from ~/Downloads/Herring.agent.json; echo "exit: $?"
|
||
# → note: 'Herring' is already published as '<uuid>'
|
||
# → exit 5, "persona '<uuid>' already exists — pass --replace to overwrite it"
|
||
buzz personas create --from ~/Downloads/Herring.agent.json --replace | jq -r .slug
|
||
# → the UUID, not "herring". `buzz personas list` still shows one Herring.
|
||
|
||
# Only an identical definition adopts. Edit the prompt and it is a different
|
||
# persona, published at its own slug rather than overwriting Desktop's.
|
||
jq '.definition.systemPrompt = "Something else."' ~/Downloads/Herring.agent.json > /tmp/other.agent.json
|
||
buzz personas create --from /tmp/other.agent.json | jq -r .slug
|
||
# → note: a different persona named 'Herring' is published as '<uuid>'
|
||
# → "herring"
|
||
|
||
# A snapshot import is fail-closed: a wrong-typed field is refused, not
|
||
# dropped, so the persona published always matches the file
|
||
jq '.definition.namePool = ["Herring", 42]' ~/Downloads/Herring.agent.json > /tmp/bad.agent.json
|
||
buzz personas create --from /tmp/bad.agent.json; echo "exit: $?"
|
||
# → "not a valid v1 agent snapshot", exit 1
|
||
|
||
# Past the 5 MiB Desktop itself refuses to import
|
||
head -c 6000000 /dev/zero | tr '\0' a > /tmp/big.agent.json
|
||
buzz personas create --from /tmp/big.agent.json; echo "exit: $?" # 1, "snapshot limit"
|
||
|
||
# Same for a team snapshot, at Desktop's 25 MiB team limit
|
||
jq '.team.instructions = ["be terse"]' ~/Downloads/Red\ team.team.json > /tmp/bad.team.json
|
||
buzz teams create --from /tmp/bad.team.json; echo "exit: $?"
|
||
# → "not a valid v1 team snapshot", exit 1
|
||
```
|
||
|
||
Cross-check in Desktop: after `personas create`, the definition appears in the
|
||
agent picker; after `teams create`, the team appears with every member seated.
|
||
|
||
---
|
||
|
||
## 7. Error Path Testing
|
||
|
||
Verify the CLI produces correct JSON on stderr and correct exit codes.
|
||
|
||
```bash
|
||
# Exit 1: Invalid UUID
|
||
buzz 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
|
||
buzz 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 (clap validates the enum — multi-line error)
|
||
buzz channels create --name x --type invalid --visibility open 2>&1; echo "exit: $?"
|
||
# stderr: {"error":"user_error","message":"error: invalid value 'invalid' for '--type <CHANNEL_TYPE>'\n [possible values: stream, forum]\n..."}
|
||
# exit: 1
|
||
|
||
# Exit 1: Invalid --direction value
|
||
buzz messages vote --event "$(printf '0%.0s' {1..64})" \
|
||
--direction sideways 2>&1; echo "exit: $?"
|
||
# exit: 1
|
||
|
||
# Exit 1: Empty body guard
|
||
buzz users set-profile 2>&1; echo "exit: $?"
|
||
# exit: 1 (at least one field required)
|
||
|
||
# Exit 3: No auth configured
|
||
env -u BUZZ_PRIVATE_KEY \
|
||
cargo run -p buzz-cli -- channels list 2>&1; echo "exit: $?"
|
||
# stderr: {"error":"auth_error","message":"auth error: BUZZ_PRIVATE_KEY is required (use --private-key or set env var)"}
|
||
# exit: 3
|
||
|
||
# Not-found returns null, not an error (exit 0)
|
||
buzz channels get --channel "00000000-0000-0000-0000-000000000000"
|
||
# stdout: null
|
||
# exit: 0
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Auth Testing
|
||
|
||
Test authentication.
|
||
|
||
```bash
|
||
# Private key (BUZZ_PRIVATE_KEY)
|
||
BUZZ_PRIVATE_KEY="nsec1..." buzz channels list | jq .
|
||
# Should succeed
|
||
|
||
# No auth → exit 3
|
||
env -u BUZZ_PRIVATE_KEY \
|
||
cargo run -p buzz-cli -- channels list 2>&1; echo "exit: $?"
|
||
# stderr: {"error":"auth_error","message":"auth error: BUZZ_PRIVATE_KEY is required (use --private-key or set env var)"}
|
||
# exit: 3
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Cleanup
|
||
|
||
```bash
|
||
# Delete test channels
|
||
buzz channels delete --channel "$CHANNEL_ID" | jq .
|
||
buzz 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 | `repos protect list` | ☐ | Empty/populated rules; unknown rules visible; malformed rule reported in validation_error |
|
||
| 53 | `repos protect set` | ☐ | Create and replace complete exact-ref rule; verify metadata is preserved |
|
||
| 54 | `repos protect remove` | ☐ | Remove exact ref; missing rule → NotFound |
|
||
| 55 | `upload file` | ☐ | |
|
||
| 56 | `pack validate` | ☐ | Local, no relay |
|
||
| 57 | `pack inspect` | ☐ | Local, no relay |
|
||
| 58 | `notes set` | ☐ | First publish, edit/carry, --clear-tags, ambiguity, empty-stdin guard |
|
||
| 59 | `notes get` | ☐ | By name, by naddr, --content-only, cross-author, ambiguous → exit 1 |
|
||
| 60 | `notes ls` | ☐ | Own, --author all, --tag, --limit |
|
||
| 61 | `notes rm` | ☐ | Delete→get 404, double-delete idempotent, missing slug → NotFound |
|
||
| 62 | `users set-status` | ☐ | Text+emoji, text only, emoji-only (`--text ""`), `--clear`, `--clear` + `--text` → exit 1 |
|
||
| 63 | `personas create` | ☐ | Flags, `--from` snapshot, `--replace` conflict → exit 5, invisible-character reject → exit 1 |
|
||
| 63a | `personas create --avatar` | ☐ | Small image inlines with its metadata stripped; large flat art downscales back to inline; EXIF-bearing photo succeeds and lands upright; non-image → exit 1; `--avatar-url` conflict or non-http(s) URL → exit 1 |
|
||
| 64 | `personas list` / `get` | ☐ | `--json` is a sig-stripped array |
|
||
| 65 | `personas delete` | ☐ | Delete→get 404; warns when a published team still lists it |
|
||
| 66 | `teams create` | ☐ | Flags, `--from` snapshot, members by d-tag and by display name, unpublished member → exit 1 |
|
||
| 67 | `teams list` / `get` | ☐ | Members absent vs. empty render differently |
|
||
| 68 | `teams delete` | ☐ | Delete→get 404; verify in Desktop the team is gone |
|