mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(sdk): add builder functions for workflows, DMs, and presence (#589)
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
use nostr::{EventBuilder, Kind, Tag};
|
||||
|
||||
use crate::client::SproutClient;
|
||||
use crate::error::CliError;
|
||||
use crate::validate::{validate_hex64, validate_uuid};
|
||||
use crate::validate::{parse_uuid, sdk_err};
|
||||
|
||||
/// List DM conversations by querying kind:41010 (DM open) events authored by us.
|
||||
pub async fn cmd_list_dms(client: &SproutClient, limit: Option<u32>) -> Result<(), CliError> {
|
||||
@@ -23,16 +21,8 @@ pub async fn cmd_open_dm(client: &SproutClient, pubkeys: &[String]) -> Result<()
|
||||
if pubkeys.is_empty() || pubkeys.len() > 8 {
|
||||
return Err(CliError::Usage("--pubkey: must provide 1–8 pubkeys".into()));
|
||||
}
|
||||
for pk in pubkeys {
|
||||
validate_hex64(pk)?;
|
||||
}
|
||||
|
||||
let mut tags: Vec<Tag> = Vec::new();
|
||||
for pk in pubkeys {
|
||||
tags.push(Tag::parse(&["p", pk]).map_err(|e| CliError::Other(format!("tag error: {e}")))?);
|
||||
}
|
||||
|
||||
let builder = EventBuilder::new(Kind::Custom(41010), "", tags);
|
||||
let refs: Vec<&str> = pubkeys.iter().map(String::as_str).collect();
|
||||
let builder = sprout_sdk::build_dm_open(&refs).map_err(sdk_err)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
|
||||
let resp = client.submit_event(event).await?;
|
||||
@@ -46,15 +36,9 @@ pub async fn cmd_add_dm_member(
|
||||
channel_id: &str,
|
||||
pubkey: &str,
|
||||
) -> Result<(), CliError> {
|
||||
validate_uuid(channel_id)?;
|
||||
validate_hex64(pubkey)?;
|
||||
let channel_uuid = parse_uuid(channel_id)?;
|
||||
|
||||
let tags = vec![
|
||||
Tag::parse(&["h", channel_id]).map_err(|e| CliError::Other(format!("tag error: {e}")))?,
|
||||
Tag::parse(&["p", pubkey]).map_err(|e| CliError::Other(format!("tag error: {e}")))?,
|
||||
];
|
||||
|
||||
let builder = EventBuilder::new(Kind::Custom(41011), "", tags);
|
||||
let builder = sprout_sdk::build_dm_add_member(channel_uuid, pubkey).map_err(sdk_err)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
|
||||
let resp = client.submit_event(event).await?;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use nostr::{EventBuilder, Kind, Tag};
|
||||
|
||||
use crate::client::SproutClient;
|
||||
use crate::error::CliError;
|
||||
use crate::validate::validate_hex64;
|
||||
@@ -220,22 +218,7 @@ pub async fn cmd_get_presence(client: &SproutClient, pubkeys_csv: &str) -> Resul
|
||||
/// This will fail until the CLI gains a WS publish path. The kind is correct
|
||||
/// per the protocol spec (KIND_PRESENCE_UPDATE = 20001).
|
||||
pub async fn cmd_set_presence(client: &SproutClient, status: &str) -> Result<(), CliError> {
|
||||
match status {
|
||||
"online" | "away" | "offline" => {}
|
||||
_ => {
|
||||
return Err(CliError::Usage(format!(
|
||||
"--status must be one of: online, away, offline (got: {status})"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
let tags =
|
||||
vec![Tag::parse(&["status", status])
|
||||
.map_err(|e| CliError::Other(format!("tag error: {e}")))?];
|
||||
|
||||
// KIND_PRESENCE_UPDATE (20001) — ephemeral, WS-only. HTTP bridge will reject this
|
||||
// until the CLI gains a WebSocket publish path.
|
||||
let builder = EventBuilder::new(Kind::Custom(20001), "", tags);
|
||||
let builder = sprout_sdk::build_presence_update(status).map_err(crate::validate::sdk_err)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
|
||||
let resp = client.submit_event(event).await?;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use nostr::{EventBuilder, Kind, Tag};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::client::SproutClient;
|
||||
use crate::error::CliError;
|
||||
use crate::validate::{read_or_stdin, validate_uuid};
|
||||
use crate::validate::{parse_uuid, read_or_stdin, sdk_err, validate_uuid};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read commands — POST /query
|
||||
@@ -61,17 +60,12 @@ pub async fn cmd_create_workflow(
|
||||
channel_id: &str,
|
||||
yaml: &str,
|
||||
) -> Result<(), CliError> {
|
||||
validate_uuid(channel_id)?;
|
||||
let channel_uuid = parse_uuid(channel_id)?;
|
||||
let yaml_definition = read_or_stdin(yaml)?;
|
||||
|
||||
// Generate a unique d-tag for this workflow
|
||||
let workflow_id = uuid::Uuid::new_v4().to_string();
|
||||
let tags = vec![
|
||||
Tag::parse(&["d", &workflow_id]).map_err(|e| CliError::Other(format!("tag error: {e}")))?,
|
||||
Tag::parse(&["h", channel_id]).map_err(|e| CliError::Other(format!("tag error: {e}")))?,
|
||||
];
|
||||
|
||||
let builder = EventBuilder::new(Kind::Custom(30620), &yaml_definition, tags);
|
||||
let workflow_id = uuid::Uuid::new_v4();
|
||||
let builder = sprout_sdk::build_workflow_def(channel_uuid, workflow_id, &yaml_definition)
|
||||
.map_err(sdk_err)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
|
||||
let resp = client.submit_event(event).await?;
|
||||
@@ -82,17 +76,16 @@ pub async fn cmd_create_workflow(
|
||||
/// Update a workflow — sign and submit an updated kind:30620 event with same d-tag.
|
||||
pub async fn cmd_update_workflow(
|
||||
client: &SproutClient,
|
||||
channel_id: &str,
|
||||
workflow_id: &str,
|
||||
yaml: &str,
|
||||
) -> Result<(), CliError> {
|
||||
validate_uuid(workflow_id)?;
|
||||
let channel_uuid = parse_uuid(channel_id)?;
|
||||
let wf_uuid = parse_uuid(workflow_id)?;
|
||||
let yaml_definition = read_or_stdin(yaml)?;
|
||||
|
||||
let tags =
|
||||
vec![Tag::parse(&["d", workflow_id])
|
||||
.map_err(|e| CliError::Other(format!("tag error: {e}")))?];
|
||||
|
||||
let builder = EventBuilder::new(Kind::Custom(30620), &yaml_definition, tags);
|
||||
let builder = sprout_sdk::build_workflow_update(channel_uuid, wf_uuid, &yaml_definition)
|
||||
.map_err(sdk_err)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
|
||||
let resp = client.submit_event(event).await?;
|
||||
@@ -102,17 +95,11 @@ pub async fn cmd_update_workflow(
|
||||
|
||||
/// Delete a workflow — sign and submit a kind:5 deletion event.
|
||||
pub async fn cmd_delete_workflow(client: &SproutClient, workflow_id: &str) -> Result<(), CliError> {
|
||||
validate_uuid(workflow_id)?;
|
||||
let wf_uuid = parse_uuid(workflow_id)?;
|
||||
let keys = client.keys();
|
||||
|
||||
// NIP-09 deletion targeting the parameterized replaceable event
|
||||
let tags = vec![Tag::parse(&[
|
||||
"a",
|
||||
&format!("30620:{}:{}", keys.public_key().to_hex(), workflow_id),
|
||||
])
|
||||
.map_err(|e| CliError::Other(format!("tag error: {e}")))?];
|
||||
|
||||
let builder = EventBuilder::new(Kind::Custom(5), "", tags);
|
||||
let builder =
|
||||
sprout_sdk::build_workflow_delete(&keys.public_key().to_hex(), wf_uuid).map_err(sdk_err)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
|
||||
let resp = client.submit_event(event).await?;
|
||||
@@ -125,13 +112,9 @@ pub async fn cmd_trigger_workflow(
|
||||
client: &SproutClient,
|
||||
workflow_id: &str,
|
||||
) -> Result<(), CliError> {
|
||||
validate_uuid(workflow_id)?;
|
||||
let wf_uuid = parse_uuid(workflow_id)?;
|
||||
|
||||
let tags =
|
||||
vec![Tag::parse(&["d", workflow_id])
|
||||
.map_err(|e| CliError::Other(format!("tag error: {e}")))?];
|
||||
|
||||
let builder = EventBuilder::new(Kind::Custom(46020), "", tags);
|
||||
let builder = sprout_sdk::build_workflow_trigger(wf_uuid).map_err(sdk_err)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
|
||||
let resp = client.submit_event(event).await?;
|
||||
@@ -148,16 +131,12 @@ pub async fn cmd_approve_step(
|
||||
) -> Result<(), CliError> {
|
||||
validate_uuid(approval_token)?;
|
||||
|
||||
let kind = if approved { 46030 } else { 46031 };
|
||||
let content = note.unwrap_or("");
|
||||
|
||||
// The relay expects d-tag = hex(SHA256(token)), not the raw token UUID.
|
||||
let token_hash = hex::encode(Sha256::digest(approval_token.as_bytes()));
|
||||
let tags =
|
||||
vec![Tag::parse(&["d", &token_hash])
|
||||
.map_err(|e| CliError::Other(format!("tag error: {e}")))?];
|
||||
|
||||
let builder = EventBuilder::new(Kind::Custom(kind), content, tags);
|
||||
let builder =
|
||||
sprout_sdk::build_workflow_approval(&token_hash, approved, content).map_err(sdk_err)?;
|
||||
let event = client.sign_event(builder)?;
|
||||
|
||||
let resp = client.submit_event(event).await?;
|
||||
|
||||
@@ -377,6 +377,8 @@ enum Cmd {
|
||||
},
|
||||
/// Update a workflow
|
||||
UpdateWorkflow {
|
||||
#[arg(long)]
|
||||
channel: String,
|
||||
#[arg(long)]
|
||||
workflow: String,
|
||||
#[arg(long)]
|
||||
@@ -819,9 +821,11 @@ async fn run(cli: Cli) -> Result<(), CliError> {
|
||||
Cmd::CreateWorkflow { channel, yaml } => {
|
||||
commands::workflows::cmd_create_workflow(&client, &channel, &yaml).await
|
||||
}
|
||||
Cmd::UpdateWorkflow { workflow, yaml } => {
|
||||
commands::workflows::cmd_update_workflow(&client, &workflow, &yaml).await
|
||||
}
|
||||
Cmd::UpdateWorkflow {
|
||||
channel,
|
||||
workflow,
|
||||
yaml,
|
||||
} => commands::workflows::cmd_update_workflow(&client, &channel, &workflow, &yaml).await,
|
||||
Cmd::DeleteWorkflow { workflow } => {
|
||||
commands::workflows::cmd_delete_workflow(&client, &workflow).await
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ pub fn validate_uuid(s: &str) -> Result<(), CliError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse and validate a UUID string, returning the parsed Uuid value.
|
||||
pub fn parse_uuid(s: &str) -> Result<uuid::Uuid, CliError> {
|
||||
uuid::Uuid::parse_str(s).map_err(|_| CliError::Usage(format!("invalid UUID: {s}")))
|
||||
}
|
||||
|
||||
/// Validate 64-character lowercase hex string (event_id, pubkey).
|
||||
pub fn validate_hex64(s: &str) -> Result<(), CliError> {
|
||||
if s.len() != 64 || !s.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
@@ -141,6 +146,16 @@ pub fn infer_language(file_path: &str) -> Option<String> {
|
||||
Some(lang.to_string())
|
||||
}
|
||||
|
||||
/// Map `SdkError` to the appropriate `CliError` variant.
|
||||
///
|
||||
/// `InvalidInput` is a user error (exit 1), everything else is internal (exit 4).
|
||||
pub fn sdk_err(e: sprout_sdk::SdkError) -> CliError {
|
||||
match e {
|
||||
sprout_sdk::SdkError::InvalidInput(msg) => CliError::Usage(msg),
|
||||
other => CliError::Other(other.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read content from a string value or stdin if the value is "-".
|
||||
pub fn read_or_stdin(value: &str) -> Result<String, CliError> {
|
||||
if value == "-" {
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
//! The 25 typed event builder functions.
|
||||
//! Typed event builder functions (38 builders).
|
||||
//!
|
||||
//! All functions return `Result<nostr::EventBuilder, SdkError>`.
|
||||
//! The caller signs: `builder.sign_with_keys(&keys)?`.
|
||||
|
||||
use nostr::{EventBuilder, Kind, Tag};
|
||||
use sprout_core::{
|
||||
kind::{KIND_AGENT_OBSERVER_FRAME, KIND_GIT_REPO_ANNOUNCEMENT},
|
||||
kind::{
|
||||
KIND_AGENT_OBSERVER_FRAME, KIND_APPROVAL_DENY, KIND_APPROVAL_GRANT, KIND_DELETION,
|
||||
KIND_DM_ADD_MEMBER, KIND_DM_OPEN, KIND_GIT_REPO_ANNOUNCEMENT, KIND_PRESENCE_UPDATE,
|
||||
KIND_WORKFLOW_DEF, KIND_WORKFLOW_TRIGGER,
|
||||
},
|
||||
observer::{
|
||||
content_looks_like_nip44, OBSERVER_AGENT_TAG, OBSERVER_FRAME_CONTROL, OBSERVER_FRAME_TAG,
|
||||
OBSERVER_FRAME_TELEMETRY,
|
||||
@@ -896,6 +900,175 @@ pub fn build_repo_announcement(
|
||||
))
|
||||
}
|
||||
|
||||
// ── Builder 31: build_workflow_def ────────────────────────────────────────────
|
||||
|
||||
/// Build a workflow definition event (kind 30620).
|
||||
///
|
||||
/// - `channel_id`: the channel this workflow belongs to (h-tag)
|
||||
/// - `workflow_id`: unique workflow UUID (d-tag)
|
||||
/// - `yaml`: workflow YAML definition as content
|
||||
pub fn build_workflow_def(
|
||||
channel_id: Uuid,
|
||||
workflow_id: Uuid,
|
||||
yaml: &str,
|
||||
) -> Result<EventBuilder, SdkError> {
|
||||
check_content(yaml, 64 * 1024)?;
|
||||
let tags = vec![
|
||||
tag(&["d", &workflow_id.to_string()])?,
|
||||
tag(&["h", &channel_id.to_string()])?,
|
||||
];
|
||||
Ok(EventBuilder::new(
|
||||
Kind::Custom(KIND_WORKFLOW_DEF as u16),
|
||||
yaml,
|
||||
tags,
|
||||
))
|
||||
}
|
||||
|
||||
// ── Builder 32: build_workflow_update ─────────────────────────────────────────
|
||||
|
||||
/// Build a workflow update event (kind 30620) for an existing workflow.
|
||||
///
|
||||
/// Updates an existing workflow definition in-place via the parameterized
|
||||
/// replaceable event mechanism — same d-tag overwrites the previous version.
|
||||
/// The h-tag (channel scope) is required by the relay for authorization.
|
||||
pub fn build_workflow_update(
|
||||
channel_id: Uuid,
|
||||
workflow_id: Uuid,
|
||||
yaml: &str,
|
||||
) -> Result<EventBuilder, SdkError> {
|
||||
check_content(yaml, 64 * 1024)?;
|
||||
let tags = vec![
|
||||
tag(&["d", &workflow_id.to_string()])?,
|
||||
tag(&["h", &channel_id.to_string()])?,
|
||||
];
|
||||
Ok(EventBuilder::new(
|
||||
Kind::Custom(KIND_WORKFLOW_DEF as u16),
|
||||
yaml,
|
||||
tags,
|
||||
))
|
||||
}
|
||||
|
||||
// ── Builder 33: build_workflow_delete ─────────────────────────────────────────
|
||||
|
||||
/// Build a NIP-09 deletion event targeting a workflow definition (kind 5).
|
||||
///
|
||||
/// The `a`-tag addresses the parameterized replaceable event
|
||||
/// `<KIND_WORKFLOW_DEF>:<pubkey>:<workflow_id>`.
|
||||
pub fn build_workflow_delete(
|
||||
author_pubkey: &str,
|
||||
workflow_id: Uuid,
|
||||
) -> Result<EventBuilder, SdkError> {
|
||||
let pk = check_pubkey_hex(author_pubkey, "author_pubkey")?;
|
||||
let tags = vec![tag(&[
|
||||
"a",
|
||||
&format!("{}:{pk}:{workflow_id}", KIND_WORKFLOW_DEF),
|
||||
])?];
|
||||
Ok(EventBuilder::new(
|
||||
Kind::Custom(KIND_DELETION as u16),
|
||||
"",
|
||||
tags,
|
||||
))
|
||||
}
|
||||
|
||||
// ── Builder 34: build_workflow_trigger ────────────────────────────────────────
|
||||
|
||||
/// Build a workflow trigger event (kind 46020).
|
||||
pub fn build_workflow_trigger(workflow_id: Uuid) -> Result<EventBuilder, SdkError> {
|
||||
let tags = vec![tag(&["d", &workflow_id.to_string()])?];
|
||||
Ok(EventBuilder::new(
|
||||
Kind::Custom(KIND_WORKFLOW_TRIGGER as u16),
|
||||
"",
|
||||
tags,
|
||||
))
|
||||
}
|
||||
|
||||
// ── Builder 35: build_workflow_approval ───────────────────────────────────────
|
||||
|
||||
/// Build a workflow approval event — kind 46030 (grant) or 46031 (deny).
|
||||
///
|
||||
/// - `token_hash`: hex-encoded SHA-256 of the approval token UUID (d-tag).
|
||||
/// Must be exactly 64 hex characters.
|
||||
/// - `approved`: `true` emits kind 46030 (grant), `false` emits kind 46031 (deny)
|
||||
/// - `note`: optional human-readable note as event content
|
||||
pub fn build_workflow_approval(
|
||||
token_hash: &str,
|
||||
approved: bool,
|
||||
note: &str,
|
||||
) -> Result<EventBuilder, SdkError> {
|
||||
if token_hash.len() != 64 || !token_hash.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Err(SdkError::InvalidInput(
|
||||
"token_hash must be a 64-character hex SHA-256 digest".into(),
|
||||
));
|
||||
}
|
||||
let kind = if approved {
|
||||
KIND_APPROVAL_GRANT
|
||||
} else {
|
||||
KIND_APPROVAL_DENY
|
||||
};
|
||||
let tags = vec![tag(&["d", token_hash])?];
|
||||
Ok(EventBuilder::new(Kind::Custom(kind as u16), note, tags))
|
||||
}
|
||||
|
||||
// ── Builder 36: build_dm_open ────────────────────────────────────────────────
|
||||
|
||||
/// Build a DM open event (kind 41010).
|
||||
///
|
||||
/// `pubkeys` must be 1–8 hex-encoded pubkeys to include in the DM conversation.
|
||||
pub fn build_dm_open(pubkeys: &[&str]) -> Result<EventBuilder, SdkError> {
|
||||
if pubkeys.is_empty() || pubkeys.len() > 8 {
|
||||
return Err(SdkError::InvalidInput(
|
||||
"dm open requires 1-8 pubkeys".into(),
|
||||
));
|
||||
}
|
||||
let mut tags = Vec::with_capacity(pubkeys.len());
|
||||
for pk in pubkeys {
|
||||
let validated = check_pubkey_hex(pk, "pubkey")?;
|
||||
tags.push(tag(&["p", &validated])?);
|
||||
}
|
||||
Ok(EventBuilder::new(
|
||||
Kind::Custom(KIND_DM_OPEN as u16),
|
||||
"",
|
||||
tags,
|
||||
))
|
||||
}
|
||||
|
||||
// ── Builder 37: build_dm_add_member ──────────────────────────────────────────
|
||||
|
||||
/// Build a DM add-member event (kind 41011).
|
||||
pub fn build_dm_add_member(channel_id: Uuid, pubkey: &str) -> Result<EventBuilder, SdkError> {
|
||||
let pk = check_pubkey_hex(pubkey, "pubkey")?;
|
||||
let tags = vec![tag(&["h", &channel_id.to_string()])?, tag(&["p", &pk])?];
|
||||
Ok(EventBuilder::new(
|
||||
Kind::Custom(KIND_DM_ADD_MEMBER as u16),
|
||||
"",
|
||||
tags,
|
||||
))
|
||||
}
|
||||
|
||||
// ── Builder 38: build_presence_update ────────────────────────────────────────
|
||||
|
||||
/// Build a presence update event (kind 20001).
|
||||
///
|
||||
/// `status` must be one of: `"online"`, `"away"`, `"offline"`.
|
||||
/// The status is placed in `event.content` (relay reads it there) and also
|
||||
/// in a `["status", ...]` tag for structured access.
|
||||
pub fn build_presence_update(status: &str) -> Result<EventBuilder, SdkError> {
|
||||
match status {
|
||||
"online" | "away" | "offline" => {}
|
||||
_ => {
|
||||
return Err(SdkError::InvalidInput(format!(
|
||||
"status must be online, away, or offline (got: {status})"
|
||||
)))
|
||||
}
|
||||
}
|
||||
let tags = vec![tag(&["status", status])?];
|
||||
Ok(EventBuilder::new(
|
||||
Kind::Custom(KIND_PRESENCE_UPDATE as u16),
|
||||
status,
|
||||
tags,
|
||||
))
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1964,4 +2137,180 @@ mod tests {
|
||||
assert_eq!(vals[0], "https://relay.example.com/git/abc/multi-clone");
|
||||
assert_eq!(vals[1], "ssh://git@github.com/org/multi-clone.git");
|
||||
}
|
||||
|
||||
// ── Builder 31: build_workflow_def ───────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn workflow_def_happy_path() {
|
||||
let cid = uuid();
|
||||
let wid = uuid();
|
||||
let ev = sign(build_workflow_def(cid, wid, "name: test\ntrigger:\n on: webhook").unwrap());
|
||||
assert_eq!(ev.kind.as_u16(), 30620);
|
||||
assert!(has_tag(&ev, "d", &wid.to_string()));
|
||||
assert!(has_tag(&ev, "h", &cid.to_string()));
|
||||
assert!(ev.content.contains("name: test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workflow_def_rejects_oversized_yaml() {
|
||||
let big = "x".repeat(65 * 1024);
|
||||
let err = build_workflow_def(uuid(), uuid(), &big).unwrap_err();
|
||||
assert!(matches!(err, SdkError::ContentTooLarge { .. }));
|
||||
}
|
||||
|
||||
// ── Builder 32: build_workflow_update ────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn workflow_update_includes_h_tag() {
|
||||
let cid = uuid();
|
||||
let wid = uuid();
|
||||
let ev = sign(build_workflow_update(cid, wid, "name: updated").unwrap());
|
||||
assert_eq!(ev.kind.as_u16(), 30620);
|
||||
assert!(has_tag(&ev, "d", &wid.to_string()));
|
||||
assert!(has_tag(&ev, "h", &cid.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workflow_update_rejects_oversized_yaml() {
|
||||
let big = "x".repeat(65 * 1024);
|
||||
let err = build_workflow_update(uuid(), uuid(), &big).unwrap_err();
|
||||
assert!(matches!(err, SdkError::ContentTooLarge { .. }));
|
||||
}
|
||||
|
||||
// ── Builder 33: build_workflow_delete ────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn workflow_delete_happy_path() {
|
||||
let pk = "a".repeat(64);
|
||||
let wid = uuid();
|
||||
let ev = sign(build_workflow_delete(&pk, wid).unwrap());
|
||||
assert_eq!(ev.kind.as_u16(), 5);
|
||||
let a_vals = tag_values(&ev, "a");
|
||||
assert_eq!(a_vals.len(), 1);
|
||||
assert!(a_vals[0].starts_with("30620:"));
|
||||
assert!(a_vals[0].contains(&wid.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workflow_delete_rejects_bad_pubkey() {
|
||||
let err = build_workflow_delete("bad", uuid()).unwrap_err();
|
||||
assert!(matches!(err, SdkError::InvalidInput(_)));
|
||||
}
|
||||
|
||||
// ── Builder 34: build_workflow_trigger ───────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn workflow_trigger_happy_path() {
|
||||
let wid = uuid();
|
||||
let ev = sign(build_workflow_trigger(wid).unwrap());
|
||||
assert_eq!(ev.kind.as_u16(), 46020);
|
||||
assert!(has_tag(&ev, "d", &wid.to_string()));
|
||||
}
|
||||
|
||||
// ── Builder 35: build_workflow_approval ──────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn workflow_approval_grant() {
|
||||
let hash = "a".repeat(64);
|
||||
let ev = sign(build_workflow_approval(&hash, true, "lgtm").unwrap());
|
||||
assert_eq!(ev.kind.as_u16(), 46030);
|
||||
assert!(has_tag(&ev, "d", &hash));
|
||||
assert_eq!(ev.content, "lgtm");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workflow_approval_deny() {
|
||||
let hash = "b".repeat(64);
|
||||
let ev = sign(build_workflow_approval(&hash, false, "").unwrap());
|
||||
assert_eq!(ev.kind.as_u16(), 46031);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workflow_approval_rejects_bad_token_hash() {
|
||||
let err = build_workflow_approval("not-hex", true, "").unwrap_err();
|
||||
assert!(matches!(err, SdkError::InvalidInput(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workflow_approval_rejects_short_hash() {
|
||||
let short = "a".repeat(32);
|
||||
let err = build_workflow_approval(&short, true, "").unwrap_err();
|
||||
assert!(matches!(err, SdkError::InvalidInput(_)));
|
||||
}
|
||||
|
||||
// ── Builder 36: build_dm_open ───────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn dm_open_happy_path() {
|
||||
let pk = "a".repeat(64);
|
||||
let ev = sign(build_dm_open(&[&pk]).unwrap());
|
||||
assert_eq!(ev.kind.as_u16(), 41010);
|
||||
assert!(has_tag(&ev, "p", &pk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dm_open_rejects_empty_pubkeys() {
|
||||
let err = build_dm_open(&[]).unwrap_err();
|
||||
assert!(matches!(err, SdkError::InvalidInput(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dm_open_rejects_over_8_pubkeys() {
|
||||
let pk = "a".repeat(64);
|
||||
let pks: Vec<&str> = vec![pk.as_str(); 9];
|
||||
let err = build_dm_open(&pks).unwrap_err();
|
||||
assert!(matches!(err, SdkError::InvalidInput(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dm_open_rejects_bad_pubkey() {
|
||||
let err = build_dm_open(&["bad-hex"]).unwrap_err();
|
||||
assert!(matches!(err, SdkError::InvalidInput(_)));
|
||||
}
|
||||
|
||||
// ── Builder 37: build_dm_add_member ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn dm_add_member_happy_path() {
|
||||
let cid = uuid();
|
||||
let pk = "b".repeat(64);
|
||||
let ev = sign(build_dm_add_member(cid, &pk).unwrap());
|
||||
assert_eq!(ev.kind.as_u16(), 41011);
|
||||
assert!(has_tag(&ev, "h", &cid.to_string()));
|
||||
assert!(has_tag(&ev, "p", &pk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dm_add_member_rejects_bad_pubkey() {
|
||||
let err = build_dm_add_member(uuid(), "short").unwrap_err();
|
||||
assert!(matches!(err, SdkError::InvalidInput(_)));
|
||||
}
|
||||
|
||||
// ── Builder 38: build_presence_update ────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn presence_update_content_is_status() {
|
||||
let ev = sign(build_presence_update("online").unwrap());
|
||||
assert_eq!(ev.kind.as_u16(), 20001);
|
||||
assert_eq!(ev.content, "online");
|
||||
assert!(has_tag(&ev, "status", "online"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presence_update_away() {
|
||||
let ev = sign(build_presence_update("away").unwrap());
|
||||
assert_eq!(ev.content, "away");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presence_update_offline() {
|
||||
let ev = sign(build_presence_update("offline").unwrap());
|
||||
assert_eq!(ev.content, "offline");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presence_update_rejects_invalid_status() {
|
||||
let err = build_presence_update("dnd").unwrap_err();
|
||||
assert!(matches!(err, SdkError::InvalidInput(_)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user