feat(cli): add --visibility flag to channels update (#5119)

## Why
`buzz channels update` could already change name, description, and TTL,
but the SDK/relay/DB path for channel visibility was unreachable from
the CLI.

## What
- Add `--visibility open|private` to `buzz channels update`
- Pass the visibility value through to `build_update_channel`
- Add guard tests proving empty updates still fail and visibility-only
updates are accepted

## Risk Assessment
Low — this is limited to the buzz-cli update command and uses existing
SDK validation plus existing relay/DB handling.

## References
- Spike notes: `RESEARCH/SPIKE_CHANNEL_VISIBILITY_TOGGLE.md`
- Local validation: `cargo test -p buzz-cli`

Generated with Codex

Signed-off-by: Cameron Hotchkies <chotchkies@block.xyz>
Co-authored-by: Lazy Joe <dbd8c9941ba6dafebcef0abc015b65e75d52e7452f2ce483c9c3fd4d180f2504@buzz.block.builderlab.xyz>
This commit is contained in:
Cameron Hotchkies
2026-08-10 14:48:30 -07:00
committed by GitHub
co-authored by Lazy Joe
parent 3f2f32641f
commit f8f2ef0440
2 changed files with 47 additions and 10 deletions
+40 -9
View File
@@ -829,11 +829,27 @@ fn validate_ttl_seconds(secs: i64) -> Result<i32, CliError> {
.map_err(|_| CliError::Usage(format!("--ttl is too large (max {} seconds)", i32::MAX)))
}
fn validate_update_channel_fields(
name: Option<&str>,
description: Option<&str>,
visibility: Option<&str>,
ttl_change: Option<Option<i32>>,
) -> Result<(), CliError> {
if name.is_none() && description.is_none() && visibility.is_none() && ttl_change.is_none() {
return Err(CliError::Usage(
"at least one field required (--name, --description, --visibility, --ttl, --no-ttl)"
.into(),
));
}
Ok(())
}
pub async fn cmd_update_channel(
client: &BuzzClient,
channel_id: &str,
name: Option<&str>,
description: Option<&str>,
visibility: Option<&str>,
ttl: Option<i64>,
no_ttl: bool,
) -> Result<(), CliError> {
@@ -845,15 +861,12 @@ pub async fn cmd_update_channel(
(None, false) => None,
};
if name.is_none() && description.is_none() && ttl_change.is_none() {
return Err(CliError::Usage(
"at least one field required (--name, --description, --ttl, --no-ttl)".into(),
));
}
validate_update_channel_fields(name, description, visibility, ttl_change)?;
let channel_uuid = parse_uuid(channel_id)?;
let builder = buzz_sdk::build_update_channel(channel_uuid, name, description, None, ttl_change)
.map_err(|e| CliError::Other(format!("build_update_channel failed: {e}")))?;
let builder =
buzz_sdk::build_update_channel(channel_uuid, name, description, visibility, ttl_change)
.map_err(|e| CliError::Other(format!("build_update_channel failed: {e}")))?;
let event = client.sign_event(builder)?;
let resp = client.submit_event(event).await?;
@@ -1128,14 +1141,17 @@ pub async fn dispatch(
channel,
name,
description,
visibility,
ttl,
no_ttl,
} => {
let visibility = visibility.as_ref().map(|v| v.to_string());
cmd_update_channel(
client,
&channel,
name.as_deref(),
description.as_deref(),
visibility.as_deref(),
ttl,
no_ttl,
)
@@ -1178,8 +1194,8 @@ mod tests {
use super::{
apply_cardinality_rule, build_template_report, cmd_set_add_policy,
finalize_roster_resolution, name_matches, resolve_roster_with_archive_filter,
validate_ttl_seconds, ArchivedExclusion, ChannelSummary, ResolvedAgent, RosterResolution,
SkippedSlug,
validate_ttl_seconds, validate_update_channel_fields, ArchivedExclusion, ChannelSummary,
ResolvedAgent, RosterResolution, SkippedSlug,
};
use crate::client::BuzzClient;
use crate::CliError;
@@ -1291,6 +1307,21 @@ mod tests {
assert!(validate_ttl_seconds(i32::MAX as i64 + 1).is_err());
}
#[test]
fn update_channel_fields_rejects_empty_update() {
let result = validate_update_channel_fields(None, None, None, None);
assert!(matches!(result, Err(CliError::Usage(_))));
let msg = result.unwrap_err().to_string();
assert!(msg.contains("at least one field required"));
assert!(msg.contains("--visibility"));
}
#[test]
fn update_channel_fields_accepts_visibility_only_update() {
let result = validate_update_channel_fields(None, None, Some("open"), None);
assert!(result.is_ok(), "visibility-only update should be accepted");
}
// --- BUZZ_ACP_ALLOWED_CHANNEL_ADD_POLICIES gate ---
fn check_allowed_channel_add_policy(allowed_raw: &str, policy: &str) -> Result<(), CliError> {
+7 -1
View File
@@ -596,7 +596,10 @@ pub enum ChannelsCmd {
#[arg(long, value_name = "PATH")]
templates_file: Option<String>,
},
/// Update channel name, description, or ephemeral TTL
/// Update channel name, description, visibility, or ephemeral TTL
#[command(
after_help = "Examples:\n buzz channels update --channel <uuid> --name general\n buzz channels update --channel <uuid> --visibility open\n buzz channels update --channel <uuid> --visibility private"
)]
Update {
/// Channel UUID
#[arg(long)]
@@ -607,6 +610,9 @@ pub enum ChannelsCmd {
/// New channel description
#[arg(long)]
description: Option<String>,
/// New channel visibility
#[arg(long, value_enum)]
visibility: Option<ChannelVisibility>,
/// Make the channel ephemeral (or change its lifetime): seconds until
/// the relay archives it after the last message. Conflicts with --no-ttl.
#[arg(long, value_name = "SECONDS", conflicts_with = "no_ttl")]