fix(cli): fix search p-gate, visibility filter, and forum post routing

Three bugs found during E2E testing against a local relay:

1. `messages search` sent no `kinds` filter, triggering the relay's
   p-gate (403). Now sends `kinds: [9, 40002, 45001, 45003]` matching
   the MCP server's search implementation.

2. `channels list --visibility open` always returned `[]` because the
   filter checked for `["visibility", "open"]` tags, but the relay
   emits `["public"]`/`["private"]` per NIP-29. Rewritten to match
   the relay's actual tag convention.

3. `--kind` flag on `messages send` was dead code (`#[allow(dead_code)]`).
   Now routes to `build_forum_post` (kind 45001) and `build_forum_comment`
   (kind 45003) SDK builders, enabling forum voting via CLI.

TESTING.md updated with relay behavior notes for channels leave
(last-owner guard), workflows trigger (DB indexing delay), workflows
update (missing --channel flag), and corrected expected error messages.
This commit is contained in:
Will Pfleger
2026-05-21 11:31:35 -04:00
parent a0264f48cd
commit 058a498b57
3 changed files with 58 additions and 20 deletions

View File

@@ -154,8 +154,11 @@ sprout 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.
sprout channels leave --channel "$CHANNEL_ID" | jq .
# Expected: {"event_id":"...","accepted":true,"message":"..."}
# Expected: {"event_id":"...","accepted":true,"message":"..."} (or 400 if last owner)
# Re-join so we can send messages
sprout channels join --channel "$CHANNEL_ID" | jq .
@@ -370,8 +373,8 @@ sprout workflows list --channel "$CHANNEL_ID" | jq .
sprout workflows get --workflow "$WF_ID" | jq .
# Expected: {"workflow_id":"...","content":"<yaml>","created_at":N,"pubkey":"..."} or null
# workflows update
sprout workflows update --workflow "$WF_ID" \
# workflows update (requires --channel)
sprout workflows update --channel "$CHANNEL_ID" --workflow "$WF_ID" \
--yaml 'name: test-wf-updated
trigger:
on: webhook
@@ -381,6 +384,9 @@ steps:
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.
sprout workflows trigger --workflow "$WF_ID" | jq .
# workflows runs
@@ -439,9 +445,9 @@ 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
# Exit 1: Invalid --type value (clap validates the enum — multi-line error)
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)"}
# 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
@@ -456,7 +462,7 @@ sprout users set-profile 2>&1; echo "exit: $?"
# 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)"}
# stderr: {"error":"auth_error","message":"auth error: SPROUT_PRIVATE_KEY is required (use --private-key or set env var)"}
# exit: 3
# Not-found returns null, not an error (exit 0)
@@ -479,7 +485,7 @@ SPROUT_PRIVATE_KEY="nsec1..." sprout channels list | jq .
# 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)"}
# stderr: {"error":"auth_error","message":"auth error: SPROUT_PRIVATE_KEY is required (use --private-key or set env var)"}
# exit: 3
```

View File

@@ -69,15 +69,20 @@ pub async fn cmd_list_channels(
.iter()
.filter(|e| {
if let Some(vis) = visibility {
// Client-side visibility filter: check for ["visibility", vis] in tags
// NIP-29: relay emits ["public"] or ["private"] single-element tags
let nip29_tag = match vis {
"open" => "public",
_ => vis,
};
e.get("tags")
.and_then(|t| t.as_array())
.map(|tags| {
tags.iter().any(|tag| {
tag.as_array()
.map(|a| {
a.first().and_then(|v| v.as_str()) == Some("visibility")
&& a.get(1).and_then(|v| v.as_str()) == Some(vis)
a.len() == 1
&& a.first().and_then(|v| v.as_str())
== Some(nip29_tag)
})
.unwrap_or(false)
})

View File

@@ -299,6 +299,7 @@ pub async fn cmd_search(
) -> Result<(), CliError> {
let limit = limit.unwrap_or(20).min(100);
let filter = serde_json::json!({
"kinds": [9, 40002, 45001, 45003],
"search": query,
"limit": limit
});
@@ -315,7 +316,6 @@ pub async fn cmd_search(
pub struct SendMessageParams {
pub channel_id: String,
pub content: String,
#[allow(dead_code)] // reserved for future kind routing
pub kind: Option<u16>,
pub reply_to: Option<String>,
pub broadcast: bool,
@@ -381,15 +381,42 @@ pub async fn cmd_send_message(
merge_mentions(&mut merged, &auto_resolved, MENTION_CAP);
let mention_refs: Vec<&str> = merged.iter().map(|s| s.as_str()).collect();
let builder = sprout_sdk::build_message(
channel_uuid,
&final_content,
thread_ref.as_ref(),
&mention_refs,
p.broadcast,
&media_tags,
)
.map_err(|e| CliError::Other(format!("build_message failed: {e}")))?;
let builder = match p.kind {
Some(45001) => sprout_sdk::build_forum_post(
channel_uuid,
&final_content,
&mention_refs,
&media_tags,
)
.map_err(|e| CliError::Other(format!("build_forum_post failed: {e}")))?,
Some(45003) => {
let tr = thread_ref.as_ref().ok_or_else(|| {
CliError::Usage("--reply-to is required for forum comments (kind 45003)".into())
})?;
sprout_sdk::build_forum_comment(
channel_uuid,
&final_content,
tr,
&mention_refs,
&media_tags,
)
.map_err(|e| CliError::Other(format!("build_forum_comment failed: {e}")))?
}
None | Some(9) => sprout_sdk::build_message(
channel_uuid,
&final_content,
thread_ref.as_ref(),
&mention_refs,
p.broadcast,
&media_tags,
)
.map_err(|e| CliError::Other(format!("build_message failed: {e}")))?,
Some(k) => {
return Err(CliError::Usage(format!(
"--kind {k} is not supported (use 9, 45001, or 45003)"
)))
}
};
let event = client.sign_event(builder)?;