Files
buzz/crates/buzz-test-client/tests/e2e_human_edit_agent_content.rs
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

992 lines
34 KiB
Rust

//! End-to-end tests for human owners editing/managing content authored by
//! their agents — all five authorization predicate sites:
//!
//! - kind:40003 message edit (`validate_edit_ownership`)
//! - kind:5 standard deletion (`validate_standard_deletion_event`)
//! - kind:9005 DELETE_EVENT (`validate_admin_event` 9005 branch)
//! - kind:9002 EDIT_METADATA privileged-tag branch
//! - kind:9008 DELETE_GROUP
//!
//! The owner→agent relationship is established via NIP-OA: the agent
//! connects and authenticates with an `auth` tag signed by the owner.
//!
//! # Running
//!
//! Start the relay, then run:
//!
//! ```text
//! cargo test --test e2e_human_edit_agent_content -- --ignored
//! ```
use buzz_sdk::nip_oa;
use buzz_test_client::BuzzTestClient;
use nostr::{EventBuilder, Keys, Kind, Tag};
fn relay_url() -> String {
std::env::var("RELAY_URL").unwrap_or_else(|_| "ws://localhost:3000".to_string())
}
fn relay_http_url() -> String {
relay_url()
.replace("wss://", "https://")
.replace("ws://", "http://")
.trim_end_matches('/')
.to_string()
}
/// Create a fresh channel owned by `owner_keys`, return the channel UUID string.
async fn create_agent_owned_channel(agent_keys: &Keys) -> String {
let http = reqwest::Client::new();
let channel_uuid = uuid::Uuid::new_v4();
let event = EventBuilder::new(Kind::Custom(9007), "")
.tags(vec![
Tag::parse(["h", &channel_uuid.to_string()]).unwrap(),
Tag::parse(["name", &format!("haec-test-{}", channel_uuid.simple())]).unwrap(),
Tag::parse(["channel_type", "stream"]).unwrap(),
Tag::parse(["visibility", "open"]).unwrap(),
])
.sign_with_keys(agent_keys)
.unwrap();
let resp = http
.post(format!("{}/events", relay_http_url()))
.header("X-Pubkey", &agent_keys.public_key().to_hex())
.header("Content-Type", "application/json")
.body(serde_json::to_string(&event).unwrap())
.send()
.await
.expect("submit create-channel event");
assert!(
resp.status().is_success(),
"channel creation failed: {}",
resp.status()
);
let body: serde_json::Value = resp.json().await.unwrap();
assert!(
body["accepted"].as_bool().unwrap_or(false),
"channel creation not accepted: {body}"
);
channel_uuid.to_string()
}
/// Build a NIP-OA auth tag for `agent_keys` signed by `owner_keys`.
fn make_nip_oa_auth_tag(owner_keys: &Keys, agent_keys: &Keys) -> Tag {
let tag_json = nip_oa::compute_auth_tag(owner_keys, &agent_keys.public_key(), "kind=9")
.expect("compute_auth_tag");
nip_oa::parse_auth_tag(&tag_json).expect("parse_auth_tag")
}
/// Connect `agent_keys` to the relay with NIP-OA, establishing owner→agent in the DB.
/// Returns the connected (authenticated) client for the agent.
async fn connect_agent_with_owner(agent_keys: &Keys, owner_keys: &Keys) -> BuzzTestClient {
let url = relay_url();
let auth_tag = make_nip_oa_auth_tag(owner_keys, agent_keys);
let mut client = BuzzTestClient::connect_unauthenticated(&url)
.await
.expect("connect agent unauthenticated");
client
.authenticate_with_nip_oa(agent_keys, &auth_tag)
.await
.expect("NIP-OA auth");
client
}
// ─── kind:40003 message edit ───────────────────────────────────────────────
/// Owner can edit a message authored by their agent via kind:40003.
#[tokio::test]
#[ignore]
async fn test_owner_can_edit_agent_message() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let channel_id = create_agent_owned_channel(&agent_keys).await;
// Establish NIP-OA ownership in DB.
let mut agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
// Agent sends a message.
let content = format!("agent-msg-{}", uuid::Uuid::new_v4());
let ok = agent_client
.send_text_message(&agent_keys, &channel_id, &content, 9)
.await
.expect("agent send message");
assert!(ok.accepted, "agent message rejected: {}", ok.message);
let msg_event_id = ok.event_id;
// Owner sends a kind:40003 edit event targeting the agent's message.
let mut owner_client = BuzzTestClient::connect(&relay_url(), &owner_keys)
.await
.expect("connect owner");
let edit_event = EventBuilder::new(Kind::Custom(40003), "corrected content")
.tags(vec![
Tag::parse(["e", &msg_event_id]).unwrap(),
Tag::parse(["h", &channel_id]).unwrap(),
])
.sign_with_keys(&owner_keys)
.unwrap();
let ok = owner_client
.send_event(edit_event)
.await
.expect("send edit");
assert!(
ok.accepted,
"owner edit of agent message rejected: {}",
ok.message
);
agent_client.disconnect().await.ok();
owner_client.disconnect().await.ok();
}
/// An unrelated third party cannot edit an agent's message.
#[tokio::test]
#[ignore]
async fn test_third_party_cannot_edit_agent_message() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let third_party_keys = Keys::generate();
let channel_id = create_agent_owned_channel(&agent_keys).await;
let mut agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
let content = format!("agent-msg-{}", uuid::Uuid::new_v4());
let ok = agent_client
.send_text_message(&agent_keys, &channel_id, &content, 9)
.await
.expect("agent send message");
assert!(ok.accepted, "agent message rejected: {}", ok.message);
let msg_event_id = ok.event_id;
let mut third_party_client = BuzzTestClient::connect(&relay_url(), &third_party_keys)
.await
.expect("connect third party");
let edit_event = EventBuilder::new(Kind::Custom(40003), "malicious edit")
.tags(vec![
Tag::parse(["e", &msg_event_id]).unwrap(),
Tag::parse(["h", &channel_id]).unwrap(),
])
.sign_with_keys(&third_party_keys)
.unwrap();
let ok = third_party_client
.send_event(edit_event)
.await
.expect("send edit attempt");
assert!(
!ok.accepted,
"third party should NOT be able to edit agent message, but was accepted"
);
agent_client.disconnect().await.ok();
third_party_client.disconnect().await.ok();
}
/// The agent itself can still edit its own message (self-edit unchanged).
#[tokio::test]
#[ignore]
async fn test_agent_can_self_edit_message() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let channel_id = create_agent_owned_channel(&agent_keys).await;
let mut agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
let content = format!("agent-msg-{}", uuid::Uuid::new_v4());
let ok = agent_client
.send_text_message(&agent_keys, &channel_id, &content, 9)
.await
.expect("agent send message");
assert!(ok.accepted, "agent message rejected: {}", ok.message);
let msg_event_id = ok.event_id;
let edit_event = EventBuilder::new(Kind::Custom(40003), "agent self-edit")
.tags(vec![
Tag::parse(["e", &msg_event_id]).unwrap(),
Tag::parse(["h", &channel_id]).unwrap(),
])
.sign_with_keys(&agent_keys)
.unwrap();
let ok = agent_client
.send_event(edit_event)
.await
.expect("send edit");
assert!(ok.accepted, "agent self-edit rejected: {}", ok.message);
agent_client.disconnect().await.ok();
}
// ─── kind:9005 DELETE_EVENT ─────────────────────────────────────────────────
/// Owner can delete a message authored by their agent via kind:9005.
#[tokio::test]
#[ignore]
async fn test_owner_can_delete_agent_message() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let channel_id = create_agent_owned_channel(&agent_keys).await;
let mut agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
let content = format!("agent-msg-{}", uuid::Uuid::new_v4());
let ok = agent_client
.send_text_message(&agent_keys, &channel_id, &content, 9)
.await
.expect("agent send message");
assert!(ok.accepted, "agent message rejected: {}", ok.message);
let msg_event_id = ok.event_id;
let mut owner_client = BuzzTestClient::connect(&relay_url(), &owner_keys)
.await
.expect("connect owner");
let delete_event = EventBuilder::new(Kind::Custom(9005), "")
.tags(vec![
Tag::parse(["e", &msg_event_id]).unwrap(),
Tag::parse(["h", &channel_id]).unwrap(),
])
.sign_with_keys(&owner_keys)
.unwrap();
let ok = owner_client
.send_event(delete_event)
.await
.expect("send delete");
assert!(
ok.accepted,
"owner delete of agent message rejected: {}",
ok.message
);
agent_client.disconnect().await.ok();
owner_client.disconnect().await.ok();
}
/// An unrelated third party cannot delete an agent's message.
#[tokio::test]
#[ignore]
async fn test_third_party_cannot_delete_agent_message() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let third_party_keys = Keys::generate();
let channel_id = create_agent_owned_channel(&agent_keys).await;
let mut agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
let content = format!("agent-msg-{}", uuid::Uuid::new_v4());
let ok = agent_client
.send_text_message(&agent_keys, &channel_id, &content, 9)
.await
.expect("agent send message");
assert!(ok.accepted, "agent message rejected: {}", ok.message);
let msg_event_id = ok.event_id;
let mut third_party_client = BuzzTestClient::connect(&relay_url(), &third_party_keys)
.await
.expect("connect third party");
let delete_event = EventBuilder::new(Kind::Custom(9005), "")
.tags(vec![
Tag::parse(["e", &msg_event_id]).unwrap(),
Tag::parse(["h", &channel_id]).unwrap(),
])
.sign_with_keys(&third_party_keys)
.unwrap();
let ok = third_party_client
.send_event(delete_event)
.await
.expect("send delete attempt");
assert!(
!ok.accepted,
"third party should NOT be able to delete agent message, but was accepted"
);
agent_client.disconnect().await.ok();
third_party_client.disconnect().await.ok();
}
// ─── kind:5 standard deletion ───────────────────────────────────────────────
/// Owner can delete a message authored by their agent via standard NIP-09
/// kind:5 (the deletion kind the desktop app sends).
#[tokio::test]
#[ignore]
async fn test_owner_can_delete_agent_message_kind5() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let channel_id = create_agent_owned_channel(&agent_keys).await;
let mut agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
let content = format!("agent-msg-{}", uuid::Uuid::new_v4());
let ok = agent_client
.send_text_message(&agent_keys, &channel_id, &content, 9)
.await
.expect("agent send message");
assert!(ok.accepted, "agent message rejected: {}", ok.message);
let msg_event_id = ok.event_id;
let mut owner_client = BuzzTestClient::connect(&relay_url(), &owner_keys)
.await
.expect("connect owner");
let delete_event = EventBuilder::new(Kind::Custom(5), "")
.tags(vec![
Tag::parse(["e", &msg_event_id]).unwrap(),
Tag::parse(["h", &channel_id]).unwrap(),
])
.sign_with_keys(&owner_keys)
.unwrap();
let ok = owner_client
.send_event(delete_event)
.await
.expect("send delete");
assert!(
ok.accepted,
"owner kind:5 delete of agent message rejected: {}",
ok.message
);
agent_client.disconnect().await.ok();
owner_client.disconnect().await.ok();
}
/// An unrelated third party cannot delete an agent's message via kind:5.
#[tokio::test]
#[ignore]
async fn test_third_party_cannot_delete_agent_message_kind5() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let third_party_keys = Keys::generate();
let channel_id = create_agent_owned_channel(&agent_keys).await;
let mut agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
let content = format!("agent-msg-{}", uuid::Uuid::new_v4());
let ok = agent_client
.send_text_message(&agent_keys, &channel_id, &content, 9)
.await
.expect("agent send message");
assert!(ok.accepted, "agent message rejected: {}", ok.message);
let msg_event_id = ok.event_id;
let mut third_party_client = BuzzTestClient::connect(&relay_url(), &third_party_keys)
.await
.expect("connect third party");
let delete_event = EventBuilder::new(Kind::Custom(5), "")
.tags(vec![
Tag::parse(["e", &msg_event_id]).unwrap(),
Tag::parse(["h", &channel_id]).unwrap(),
])
.sign_with_keys(&third_party_keys)
.unwrap();
let ok = third_party_client
.send_event(delete_event)
.await
.expect("send delete attempt");
assert!(
!ok.accepted,
"third party should NOT be able to kind:5-delete agent message, but was accepted"
);
agent_client.disconnect().await.ok();
third_party_client.disconnect().await.ok();
}
// ─── kind:9002 EDIT_METADATA ────────────────────────────────────────────────
/// Owner can edit metadata (name/archived) of a channel owned by their agent,
/// even when the owner is not a channel member.
#[tokio::test]
#[ignore]
async fn test_owner_can_edit_agent_channel_metadata() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
// Agent creates the channel (agent is the channel owner-member).
let channel_id = create_agent_owned_channel(&agent_keys).await;
// Establish NIP-OA ownership.
let agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
agent_client.disconnect().await.ok();
// Owner sends kind:9002 to rename the channel — owner is NOT a member.
let mut owner_client = BuzzTestClient::connect(&relay_url(), &owner_keys)
.await
.expect("connect owner");
let edit_event = EventBuilder::new(Kind::Custom(9002), "")
.tags(vec![
Tag::parse(["h", &channel_id]).unwrap(),
Tag::parse(["name", "owner-renamed-channel"]).unwrap(),
])
.sign_with_keys(&owner_keys)
.unwrap();
let ok = owner_client
.send_event(edit_event)
.await
.expect("send edit metadata");
assert!(
ok.accepted,
"owner edit of agent channel metadata rejected: {}",
ok.message
);
owner_client.disconnect().await.ok();
}
/// Owner can archive an agent's channel via kind:9002.
#[tokio::test]
#[ignore]
async fn test_owner_can_archive_agent_channel() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let channel_id = create_agent_owned_channel(&agent_keys).await;
let agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
agent_client.disconnect().await.ok();
let mut owner_client = BuzzTestClient::connect(&relay_url(), &owner_keys)
.await
.expect("connect owner");
let archive_event = EventBuilder::new(Kind::Custom(9002), "")
.tags(vec![
Tag::parse(["h", &channel_id]).unwrap(),
Tag::parse(["archived", "true"]).unwrap(),
])
.sign_with_keys(&owner_keys)
.unwrap();
let ok = owner_client
.send_event(archive_event)
.await
.expect("send archive");
assert!(
ok.accepted,
"owner archive of agent channel rejected: {}",
ok.message
);
owner_client.disconnect().await.ok();
}
/// Unrelated third party cannot edit metadata of an agent's channel.
#[tokio::test]
#[ignore]
async fn test_third_party_cannot_edit_agent_channel_metadata() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let third_party_keys = Keys::generate();
let channel_id = create_agent_owned_channel(&agent_keys).await;
let agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
agent_client.disconnect().await.ok();
let mut third_party_client = BuzzTestClient::connect(&relay_url(), &third_party_keys)
.await
.expect("connect third party");
let edit_event = EventBuilder::new(Kind::Custom(9002), "")
.tags(vec![
Tag::parse(["h", &channel_id]).unwrap(),
Tag::parse(["name", "hijacked-name"]).unwrap(),
])
.sign_with_keys(&third_party_keys)
.unwrap();
let ok = third_party_client
.send_event(edit_event)
.await
.expect("send edit attempt");
assert!(
!ok.accepted,
"third party should NOT be able to edit agent channel metadata, but was accepted"
);
third_party_client.disconnect().await.ok();
}
// ─── kind:9008 DELETE_GROUP ─────────────────────────────────────────────────
/// Owner can delete a channel owned by their agent via kind:9008,
/// even when the owner is not a channel member.
#[tokio::test]
#[ignore]
async fn test_owner_can_delete_agent_channel() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let channel_id = create_agent_owned_channel(&agent_keys).await;
// Establish NIP-OA ownership.
let agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
agent_client.disconnect().await.ok();
// Owner sends kind:9008 to delete the channel — owner is NOT a member.
let mut owner_client = BuzzTestClient::connect(&relay_url(), &owner_keys)
.await
.expect("connect owner");
let delete_event = EventBuilder::new(Kind::Custom(9008), "")
.tags(vec![Tag::parse(["h", &channel_id]).unwrap()])
.sign_with_keys(&owner_keys)
.unwrap();
let ok = owner_client
.send_event(delete_event)
.await
.expect("send delete group");
assert!(
ok.accepted,
"owner delete of agent channel rejected: {}",
ok.message
);
owner_client.disconnect().await.ok();
}
/// Unrelated third party cannot delete an agent's channel.
#[tokio::test]
#[ignore]
async fn test_third_party_cannot_delete_agent_channel() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let third_party_keys = Keys::generate();
let channel_id = create_agent_owned_channel(&agent_keys).await;
let agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
agent_client.disconnect().await.ok();
let mut third_party_client = BuzzTestClient::connect(&relay_url(), &third_party_keys)
.await
.expect("connect third party");
let delete_event = EventBuilder::new(Kind::Custom(9008), "")
.tags(vec![Tag::parse(["h", &channel_id]).unwrap()])
.sign_with_keys(&third_party_keys)
.unwrap();
let ok = third_party_client
.send_event(delete_event)
.await
.expect("send delete attempt");
assert!(
!ok.accepted,
"third party should NOT be able to delete agent channel, but was accepted"
);
third_party_client.disconnect().await.ok();
}
/// Create a fresh **private** channel owned by `agent_keys`, return the channel UUID string.
async fn create_private_agent_owned_channel(agent_keys: &Keys) -> String {
let http = reqwest::Client::new();
let channel_uuid = uuid::Uuid::new_v4();
let event = EventBuilder::new(Kind::Custom(9007), "")
.tags(vec![
Tag::parse(["h", &channel_uuid.to_string()]).unwrap(),
Tag::parse([
"name",
&format!("haec-private-test-{}", channel_uuid.simple()),
])
.unwrap(),
Tag::parse(["channel_type", "stream"]).unwrap(),
Tag::parse(["visibility", "private"]).unwrap(),
])
.sign_with_keys(agent_keys)
.unwrap();
let resp = http
.post(format!("{}/events", relay_http_url()))
.header("X-Pubkey", &agent_keys.public_key().to_hex())
.header("Content-Type", "application/json")
.body(serde_json::to_string(&event).unwrap())
.send()
.await
.expect("submit create-private-channel event");
assert!(
resp.status().is_success(),
"private channel creation failed: {}",
resp.status()
);
let body: serde_json::Value = resp.json().await.unwrap();
assert!(
body["accepted"].as_bool().unwrap_or(false),
"private channel creation not accepted: {body}"
);
channel_uuid.to_string()
}
// ─── Private-channel coverage (Fix 1 regression guard) ──────────────────────
//
// These tests verify that the membership-gate bypass works on private channels —
// i.e., a non-member owning human can act on private agent-owned content.
// Previously all four predicates were unreachable for private channels because
// check_channel_membership rejected non-members before the per-kind validators ran.
/// Owner can edit a message in a private agent-owned channel (non-member owner allowed).
#[tokio::test]
#[ignore]
async fn test_owner_can_edit_agent_message_in_private_channel() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let channel_id = create_private_agent_owned_channel(&agent_keys).await;
// Establish NIP-OA ownership; agent sends a message.
let mut agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
let content = format!("private-agent-msg-{}", uuid::Uuid::new_v4());
let ok = agent_client
.send_text_message(&agent_keys, &channel_id, &content, 9)
.await
.expect("agent send message to private channel");
assert!(
ok.accepted,
"agent message to private channel rejected: {}",
ok.message
);
let msg_event_id = ok.event_id;
agent_client.disconnect().await.ok();
// Owner (not a channel member) edits the agent's message.
let mut owner_client = BuzzTestClient::connect(&relay_url(), &owner_keys)
.await
.expect("connect owner");
let edit_event = EventBuilder::new(Kind::Custom(40003), "edited content")
.tags(vec![
Tag::parse(["e", &msg_event_id]).unwrap(),
Tag::parse(["h", &channel_id]).unwrap(),
])
.sign_with_keys(&owner_keys)
.unwrap();
let ok = owner_client
.send_event(edit_event)
.await
.expect("send edit");
assert!(
ok.accepted,
"owner edit of agent message in private channel rejected (membership gate not bypassed): {}",
ok.message
);
owner_client.disconnect().await.ok();
}
/// Owner can delete a message in a private agent-owned channel (non-member owner allowed).
#[tokio::test]
#[ignore]
async fn test_owner_can_delete_agent_message_in_private_channel() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let channel_id = create_private_agent_owned_channel(&agent_keys).await;
let mut agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
let content = format!("private-agent-msg-{}", uuid::Uuid::new_v4());
let ok = agent_client
.send_text_message(&agent_keys, &channel_id, &content, 9)
.await
.expect("agent send message to private channel");
assert!(
ok.accepted,
"agent message to private channel rejected: {}",
ok.message
);
let msg_event_id = ok.event_id;
agent_client.disconnect().await.ok();
let mut owner_client = BuzzTestClient::connect(&relay_url(), &owner_keys)
.await
.expect("connect owner");
let delete_event = EventBuilder::new(Kind::Custom(9005), "")
.tags(vec![
Tag::parse(["e", &msg_event_id]).unwrap(),
Tag::parse(["h", &channel_id]).unwrap(),
])
.sign_with_keys(&owner_keys)
.unwrap();
let ok = owner_client
.send_event(delete_event)
.await
.expect("send delete");
assert!(
ok.accepted,
"owner delete of agent message in private channel rejected (membership gate not bypassed): {}",
ok.message
);
owner_client.disconnect().await.ok();
}
/// Owner can edit metadata of a private agent-owned channel (non-member owner allowed).
#[tokio::test]
#[ignore]
async fn test_owner_can_edit_metadata_of_private_agent_channel() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let channel_id = create_private_agent_owned_channel(&agent_keys).await;
let agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
agent_client.disconnect().await.ok();
let mut owner_client = BuzzTestClient::connect(&relay_url(), &owner_keys)
.await
.expect("connect owner");
let edit_event = EventBuilder::new(Kind::Custom(9002), "")
.tags(vec![
Tag::parse(["h", &channel_id]).unwrap(),
Tag::parse(["name", "owner-renamed-private-channel"]).unwrap(),
])
.sign_with_keys(&owner_keys)
.unwrap();
let ok = owner_client
.send_event(edit_event)
.await
.expect("send edit metadata");
assert!(
ok.accepted,
"owner edit of private agent channel metadata rejected (membership gate not bypassed): {}",
ok.message
);
owner_client.disconnect().await.ok();
}
/// Owner can delete a private agent-owned channel (non-member owner allowed).
#[tokio::test]
#[ignore]
async fn test_owner_can_delete_private_agent_channel() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let channel_id = create_private_agent_owned_channel(&agent_keys).await;
let agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
agent_client.disconnect().await.ok();
let mut owner_client = BuzzTestClient::connect(&relay_url(), &owner_keys)
.await
.expect("connect owner");
let delete_event = EventBuilder::new(Kind::Custom(9008), "")
.tags(vec![Tag::parse(["h", &channel_id]).unwrap()])
.sign_with_keys(&owner_keys)
.unwrap();
let ok = owner_client
.send_event(delete_event)
.await
.expect("send delete group");
assert!(
ok.accepted,
"owner delete of private agent channel rejected (membership gate not bypassed): {}",
ok.message
);
owner_client.disconnect().await.ok();
}
/// Agent itself can still delete its own channel (self-delete unchanged).
#[tokio::test]
#[ignore]
async fn test_agent_can_self_delete_channel() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let channel_id = create_agent_owned_channel(&agent_keys).await;
let mut agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
let delete_event = EventBuilder::new(Kind::Custom(9008), "")
.tags(vec![Tag::parse(["h", &channel_id]).unwrap()])
.sign_with_keys(&agent_keys)
.unwrap();
let ok = agent_client
.send_event(delete_event)
.await
.expect("send self-delete group");
assert!(
ok.accepted,
"agent self-delete of own channel rejected: {}",
ok.message
);
agent_client.disconnect().await.ok();
}
// ─── Removed-author rejection tests (Option A regression guard) ─────────────
//
// These tests verify that removing a user from a private channel revokes their
// ability to edit or delete their own historical messages. Before Option A,
// adding 40003/9005 to skip_membership also widened the self-author fast-path,
// allowing removed users to mutate private-channel history.
/// A user removed from a private channel CANNOT edit their own old messages.
#[tokio::test]
#[ignore]
async fn test_removed_author_cannot_edit_own_message_in_private_channel() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let victim_keys = Keys::generate();
let channel_id = create_private_agent_owned_channel(&agent_keys).await;
// Establish NIP-OA ownership; agent is now in the channel.
let mut agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
// Agent adds victim to the private channel via kind:9000 (PUT_USER).
let add_event = EventBuilder::new(Kind::Custom(9000), "")
.tags(vec![
Tag::parse(["h", &channel_id]).unwrap(),
Tag::parse(["p", &victim_keys.public_key().to_hex()]).unwrap(),
])
.sign_with_keys(&agent_keys)
.unwrap();
let ok = agent_client
.send_event(add_event)
.await
.expect("send PUT_USER");
assert!(
ok.accepted,
"agent failed to add victim to private channel: {}",
ok.message
);
// Victim connects and sends a message while still a member.
let mut victim_client = BuzzTestClient::connect(&relay_url(), &victim_keys)
.await
.expect("connect victim");
let content = format!("victim-msg-{}", uuid::Uuid::new_v4());
let ok = victim_client
.send_text_message(&victim_keys, &channel_id, &content, 9)
.await
.expect("victim send message");
assert!(
ok.accepted,
"victim message rejected while still a member: {}",
ok.message
);
let msg_event_id = ok.event_id;
// Agent removes victim from the channel via kind:9001 (REMOVE_USER).
let remove_event = EventBuilder::new(Kind::Custom(9001), "")
.tags(vec![
Tag::parse(["h", &channel_id]).unwrap(),
Tag::parse(["p", &victim_keys.public_key().to_hex()]).unwrap(),
])
.sign_with_keys(&agent_keys)
.unwrap();
let ok = agent_client
.send_event(remove_event)
.await
.expect("send REMOVE_USER");
assert!(
ok.accepted,
"agent failed to remove victim from private channel: {}",
ok.message
);
agent_client.disconnect().await.ok();
// Victim (now removed) attempts to edit their old message — must be rejected.
let edit_event = EventBuilder::new(Kind::Custom(40003), "edited content after removal")
.tags(vec![
Tag::parse(["e", &msg_event_id]).unwrap(),
Tag::parse(["h", &channel_id]).unwrap(),
])
.sign_with_keys(&victim_keys)
.unwrap();
let ok = victim_client
.send_event(edit_event)
.await
.expect("send edit attempt");
assert!(
!ok.accepted,
"removed author should NOT be able to edit old message in private channel, but was accepted"
);
victim_client.disconnect().await.ok();
}
/// A user removed from a private channel CANNOT delete their own old messages.
#[tokio::test]
#[ignore]
async fn test_removed_author_cannot_delete_own_message_in_private_channel() {
let owner_keys = Keys::generate();
let agent_keys = Keys::generate();
let victim_keys = Keys::generate();
let channel_id = create_private_agent_owned_channel(&agent_keys).await;
// Establish NIP-OA ownership; agent is now in the channel.
let mut agent_client = connect_agent_with_owner(&agent_keys, &owner_keys).await;
// Agent adds victim to the private channel via kind:9000 (PUT_USER).
let add_event = EventBuilder::new(Kind::Custom(9000), "")
.tags(vec![
Tag::parse(["h", &channel_id]).unwrap(),
Tag::parse(["p", &victim_keys.public_key().to_hex()]).unwrap(),
])
.sign_with_keys(&agent_keys)
.unwrap();
let ok = agent_client
.send_event(add_event)
.await
.expect("send PUT_USER");
assert!(
ok.accepted,
"agent failed to add victim to private channel: {}",
ok.message
);
// Victim connects and sends a message while still a member.
let mut victim_client = BuzzTestClient::connect(&relay_url(), &victim_keys)
.await
.expect("connect victim");
let content = format!("victim-msg-{}", uuid::Uuid::new_v4());
let ok = victim_client
.send_text_message(&victim_keys, &channel_id, &content, 9)
.await
.expect("victim send message");
assert!(
ok.accepted,
"victim message rejected while still a member: {}",
ok.message
);
let msg_event_id = ok.event_id;
// Agent removes victim from the channel via kind:9001 (REMOVE_USER).
let remove_event = EventBuilder::new(Kind::Custom(9001), "")
.tags(vec![
Tag::parse(["h", &channel_id]).unwrap(),
Tag::parse(["p", &victim_keys.public_key().to_hex()]).unwrap(),
])
.sign_with_keys(&agent_keys)
.unwrap();
let ok = agent_client
.send_event(remove_event)
.await
.expect("send REMOVE_USER");
assert!(
ok.accepted,
"agent failed to remove victim from private channel: {}",
ok.message
);
agent_client.disconnect().await.ok();
// Victim (now removed) attempts to delete their old message — must be rejected.
let delete_event = EventBuilder::new(Kind::Custom(9005), "")
.tags(vec![
Tag::parse(["e", &msg_event_id]).unwrap(),
Tag::parse(["h", &channel_id]).unwrap(),
])
.sign_with_keys(&victim_keys)
.unwrap();
let ok = victim_client
.send_event(delete_event)
.await
.expect("send delete attempt");
assert!(
!ok.accepted,
"removed author should NOT be able to delete old message in private channel, but was accepted"
);
victim_client.disconnect().await.ok();
}