Revert "fix(cli,relay): resolve agents by verified owner" (#3168)

Reverts block/buzz#2615
This commit is contained in:
Will Pfleger
2026-07-27 14:10:16 -04:00
committed by GitHub
parent d98da7389e
commit a041e2d21e
8 changed files with 9 additions and 568 deletions
-1
View File
@@ -56,7 +56,6 @@ buzz reactions get --event <event-id>
buzz users get # your own profile
buzz users get --pubkey <hex> # single user
buzz users get --pubkey <hex> --pubkey <hex> # batch (max 200)
buzz users get --name Honey --owner me # exact-name lookup scoped to your verified owner
buzz users set-presence --status online
# DMs
+7 -94
View File
@@ -1,5 +1,3 @@
use nostr::PublicKey;
use crate::client::{normalize_write_response, BuzzClient};
use crate::error::CliError;
use crate::validate::validate_hex64;
@@ -15,7 +13,6 @@ pub async fn cmd_get_users(
client: &BuzzClient,
pubkeys: &[String],
name: Option<&str>,
owner: Option<&str>,
format: &crate::OutputFormat,
) -> Result<(), CliError> {
if let Some(query) = name {
@@ -24,7 +21,7 @@ pub async fn cmd_get_users(
"--name and --pubkey are mutually exclusive".into(),
));
}
return search_by_name(client, query, owner, format).await;
return search_by_name(client, query, format).await;
}
for pk in pubkeys {
@@ -41,18 +38,12 @@ pub async fn cmd_get_users(
pubkeys.iter().map(|s| s.as_str()).collect()
};
let owner = resolve_owner(client, owner)?;
let filter = serde_json::json!({
"kinds": [0],
"authors": authors,
"limit": authors.len(),
"include_agent_owner": true,
"agent_owner": owner,
"limit": authors.len()
});
let resp = client.query(&filter).await?;
let my_owner_pubkey = client
.auth_tag_owner_hex()
.unwrap_or_else(|| client.keys().public_key().to_hex());
let events: Vec<serde_json::Value> = serde_json::from_str(&resp).unwrap_or_default();
let profiles: Vec<serde_json::Value> = events
.iter()
@@ -64,7 +55,6 @@ pub async fn cmd_get_users(
"pubkey".to_string(),
serde_json::json!(e.get("pubkey").and_then(|v| v.as_str()).unwrap_or("")),
);
copy_owner_fields(e, obj, &my_owner_pubkey);
}
Some(profile)
})
@@ -76,9 +66,6 @@ pub async fn cmd_get_users(
.map(|p| serde_json::json!({
"pubkey": p.get("pubkey").cloned().unwrap_or_default(),
"display_name": p.get("display_name").or_else(|| p.get("name")).cloned().unwrap_or_default(),
"owner_pubkey": p.get("owner_pubkey").cloned().unwrap_or_default(),
"owner_display_name": p.get("owner_display_name").cloned().unwrap_or_default(),
"owned_by_me": p.get("owned_by_me").cloned().unwrap_or_default(),
}))
.collect();
serde_json::to_string(&compact).unwrap_or_default()
@@ -89,70 +76,23 @@ pub async fn cmd_get_users(
Ok(())
}
fn copy_owner_fields(
event: &serde_json::Value,
profile: &mut serde_json::Map<String, serde_json::Value>,
my_owner_pubkey: &str,
) {
if let Some(value) = event.get("agent_owner_pubkey") {
profile.insert("owner_pubkey".to_string(), value.clone());
}
if let Some(value) = event.get("agent_owner_display_name") {
profile.insert("owner_display_name".to_string(), value.clone());
}
if let Some(owner) = event
.get("agent_owner_pubkey")
.and_then(|value| value.as_str())
{
profile.insert(
"owned_by_me".to_string(),
serde_json::json!(owner == my_owner_pubkey),
);
}
}
fn resolve_owner(client: &BuzzClient, owner: Option<&str>) -> Result<Option<String>, CliError> {
owner
.map(|owner| {
if owner == "me" {
Ok(client
.auth_tag_owner_hex()
.unwrap_or_else(|| client.keys().public_key().to_hex()))
} else {
PublicKey::parse(owner)
.map(|pubkey| pubkey.to_hex())
.map_err(|e| {
CliError::Usage(format!("--owner must be `me`, a pubkey, or npub: {e}"))
})
}
})
.transpose()
}
/// Search for users by display name via NIP-50 full-text search on kind:0 profiles.
/// Returns [] if the relay does not implement NIP-50 search.
async fn search_by_name(
client: &BuzzClient,
query: &str,
owner: Option<&str>,
format: &crate::OutputFormat,
) -> Result<(), CliError> {
if query.trim().is_empty() {
return Err(CliError::Usage("--name cannot be empty".into()));
}
let owner = resolve_owner(client, owner)?;
let filter = serde_json::json!({
"kinds": [0],
"search": query,
"limit": 100,
"include_agent_owner": true,
"agent_owner": owner,
"limit": 100
});
let raw = client.query(&filter).await?;
let my_owner_pubkey = client
.auth_tag_owner_hex()
.unwrap_or_else(|| client.keys().public_key().to_hex());
// Parse and filter client-side for case-insensitive substring match
// on display_name or name fields (NIP-50 may return broader matches).
@@ -186,7 +126,6 @@ async fn search_by_name(
"pubkey".to_string(),
serde_json::json!(event.get("pubkey").and_then(|v| v.as_str()).unwrap_or("")),
);
copy_owner_fields(event, obj, &my_owner_pubkey);
}
Some(profile)
})
@@ -198,9 +137,6 @@ async fn search_by_name(
.map(|p| serde_json::json!({
"pubkey": p.get("pubkey").cloned().unwrap_or_default(),
"display_name": p.get("display_name").or_else(|| p.get("name")).cloned().unwrap_or_default(),
"owner_pubkey": p.get("owner_pubkey").cloned().unwrap_or_default(),
"owner_display_name": p.get("owner_display_name").cloned().unwrap_or_default(),
"owned_by_me": p.get("owned_by_me").cloned().unwrap_or_default(),
}))
.collect();
serde_json::to_string(&compact).unwrap_or_default()
@@ -375,11 +311,9 @@ pub async fn dispatch(
) -> Result<(), CliError> {
use crate::UsersCmd;
match cmd {
UsersCmd::Get {
pubkeys,
name,
owner,
} => cmd_get_users(client, &pubkeys, name.as_deref(), owner.as_deref(), format).await,
UsersCmd::Get { pubkeys, name } => {
cmd_get_users(client, &pubkeys, name.as_deref(), format).await
}
UsersCmd::SetProfile {
name,
avatar,
@@ -402,30 +336,9 @@ pub async fn dispatch(
#[cfg(test)]
mod tests {
use super::{copy_owner_fields, presence_subject};
use super::presence_subject;
use serde_json::json;
#[test]
fn copy_owner_fields_marks_authenticated_owner() {
let owner = "a".repeat(64);
let event = json!({
"agent_owner_pubkey": owner,
"agent_owner_display_name": "John",
});
let mut profile = serde_json::Map::new();
copy_owner_fields(&event, &mut profile, &"a".repeat(64));
assert_eq!(profile["owner_display_name"], "John");
assert_eq!(profile["owned_by_me"], true);
}
#[test]
fn copy_owner_fields_marks_other_owner() {
let event = json!({"agent_owner_pubkey": "a".repeat(64)});
let mut profile = serde_json::Map::new();
copy_owner_fields(&event, &mut profile, &"b".repeat(64));
assert_eq!(profile["owned_by_me"], false);
}
#[test]
fn presence_subject_uses_p_tag() {
let event = json!({"pubkey": "relay", "tags": [["p", "user"]]});
-3
View File
@@ -808,9 +808,6 @@ pub enum UsersCmd {
/// Search by display name (case-insensitive substring match)
#[arg(long = "name")]
name: Option<String>,
/// Filter agents by verified owner (`me`, 64-char hex, or npub)
#[arg(long = "owner")]
owner: Option<String>,
},
/// Update the current identity's profile
#[command(name = "set-profile")]
-18
View File
@@ -1843,24 +1843,6 @@ impl Db {
user::search_users(&self.pool, community_id, query, limit).await
}
/// Return agent pubkeys whose verified owner matches `owner_pubkey`.
pub async fn list_agent_pubkeys_by_owner(
&self,
community_id: CommunityId,
owner_pubkey: &[u8],
) -> Result<Vec<Vec<u8>>> {
user::list_agent_pubkeys_by_owner(&self.pool, community_id, owner_pubkey).await
}
/// Fetch verified ownership metadata for the requested agent pubkeys.
pub async fn get_agent_owners(
&self,
community_id: CommunityId,
agent_pubkeys: &[Vec<u8>],
) -> Result<Vec<user::AgentOwner>> {
user::get_agent_owners(&self.pool, community_id, agent_pubkeys).await
}
/// Atomically set agent owner — only if no owner is currently assigned.
/// Returns Ok(true) if set, Ok(false) if an owner already exists.
pub async fn set_agent_owner(
+1 -1
View File
@@ -560,7 +560,7 @@ mod tests {
let mut migrations: Vec<_> = MIGRATOR.iter().collect();
migrations.sort_by_key(|migration| migration.version);
assert_eq!(migrations.len(), 25);
assert_eq!(migrations.len(), 24);
assert_eq!(migrations[0].version, 1);
assert_eq!(&*migrations[0].description, "initial schema");
assert!(migrations[0]
-121
View File
@@ -5,17 +5,6 @@ use buzz_core::CommunityId;
use sqlx::PgPool;
use sqlx::Row;
/// Ownership metadata for an agent profile.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentOwner {
/// Raw agent public key.
pub agent_pubkey: Vec<u8>,
/// Raw owner public key, verified when the agent authenticated via NIP-OA.
pub owner_pubkey: Vec<u8>,
/// Owner's current display name, when available.
pub owner_display_name: Option<String>,
}
/// A user's profile fields.
#[derive(Debug, Clone)]
pub struct UserProfile {
@@ -291,65 +280,6 @@ pub async fn search_users(
.collect())
}
/// Return agent pubkeys whose verified owner matches `owner_pubkey`.
pub async fn list_agent_pubkeys_by_owner(
pool: &PgPool,
community_id: CommunityId,
owner_pubkey: &[u8],
) -> Result<Vec<Vec<u8>>> {
let rows = sqlx::query_scalar::<_, Vec<u8>>(
r#"
SELECT pubkey
FROM users
WHERE community_id = $1 AND agent_owner_pubkey = $2
"#,
)
.bind(community_id.as_uuid())
.bind(owner_pubkey)
.fetch_all(pool)
.await?;
Ok(rows)
}
/// Fetch verified ownership metadata for the requested agent pubkeys.
pub async fn get_agent_owners(
pool: &PgPool,
community_id: CommunityId,
agent_pubkeys: &[Vec<u8>],
) -> Result<Vec<AgentOwner>> {
if agent_pubkeys.is_empty() {
return Ok(Vec::new());
}
let rows = sqlx::query_as::<_, (Vec<u8>, Vec<u8>, Option<String>)>(
r#"
SELECT agent.pubkey, agent.agent_owner_pubkey, owner.display_name
FROM users agent
LEFT JOIN users owner
ON owner.community_id = agent.community_id
AND owner.pubkey = agent.agent_owner_pubkey
WHERE agent.community_id = $1
AND agent.pubkey = ANY($2)
AND agent.agent_owner_pubkey IS NOT NULL
"#,
)
.bind(community_id.as_uuid())
.bind(agent_pubkeys)
.fetch_all(pool)
.await?;
Ok(rows
.into_iter()
.map(
|(agent_pubkey, owner_pubkey, owner_display_name)| AgentOwner {
agent_pubkey,
owner_pubkey,
owner_display_name,
},
)
.collect())
}
/// Set the owner pubkey for an agent user.
/// The owner pubkey must already exist in the users table (FK constraint).
/// Returns an error if the agent pubkey is not found (rows_affected == 0).
@@ -534,57 +464,6 @@ mod tests {
);
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn test_agent_owner_queries_are_scoped_and_include_display_name() {
let db = setup_db().await;
let community = make_community(&db.pool).await;
let agent_pk = random_pubkey();
let other_agent_pk = random_pubkey();
let owner_pk = random_pubkey();
let other_owner_pk = random_pubkey();
for pubkey in [&agent_pk, &other_agent_pk, &owner_pk, &other_owner_pk] {
ensure_user(&db.pool, community, pubkey).await.unwrap();
}
update_user_profile(
&db.pool,
community,
&owner_pk,
Some("Owner Name"),
None,
None,
None,
)
.await
.unwrap();
set_agent_owner(&db.pool, community, &agent_pk, &owner_pk)
.await
.unwrap();
set_agent_owner(&db.pool, community, &other_agent_pk, &other_owner_pk)
.await
.unwrap();
assert_eq!(
list_agent_pubkeys_by_owner(&db.pool, community, &owner_pk)
.await
.unwrap(),
vec![agent_pk.clone()]
);
assert_eq!(
get_agent_owners(&db.pool, community, &[agent_pk.clone(), other_agent_pk],)
.await
.unwrap()
.into_iter()
.find(|owner| owner.agent_pubkey == agent_pk)
.unwrap(),
AgentOwner {
agent_pubkey: agent_pk,
owner_pubkey: owner_pk,
owner_display_name: Some("Owner Name".into()),
}
);
}
/// set_channel_add_policy should persist each of the three valid policies.
#[tokio::test]
#[ignore = "requires Postgres"]
+1 -327
View File
@@ -280,112 +280,6 @@ fn extension_flag(raw: &Value, key: &str) -> bool {
raw.get(key).and_then(Value::as_bool).unwrap_or(false)
}
fn extract_agent_owner(raw: &Value) -> Result<Option<Vec<u8>>, (StatusCode, Json<Value>)> {
let Some(value) = raw.get("agent_owner") else {
return Ok(None);
};
if value.is_null() {
return Ok(None);
}
let owner = value.as_str().ok_or_else(|| {
api_error(
StatusCode::BAD_REQUEST,
"agent_owner must be a 64-char hex pubkey",
)
})?;
let bytes = hex::decode(owner)
.ok()
.filter(|bytes| bytes.len() == 32)
.ok_or_else(|| {
api_error(
StatusCode::BAD_REQUEST,
"agent_owner must be a 64-char hex pubkey",
)
})?;
Ok(Some(bytes))
}
fn include_agent_owner(raw_filters: &[Value]) -> bool {
raw_filters
.iter()
.any(|raw| extension_flag(raw, "include_agent_owner"))
}
fn apply_agent_owner_authors(raw: &mut Value, owned_pubkeys: &[Vec<u8>]) {
let mut owned_hex: std::collections::HashSet<String> =
owned_pubkeys.iter().map(hex::encode).collect();
if let Some(requested) = raw.get("authors").and_then(Value::as_array) {
let requested: std::collections::HashSet<&str> =
requested.iter().filter_map(Value::as_str).collect();
owned_hex.retain(|pubkey| requested.contains(pubkey.as_str()));
}
raw["authors"] = Value::Array(owned_hex.into_iter().map(Value::String).collect());
}
fn reject_unsupported_agent_owner_filter(raw: &Value) -> Result<(), (StatusCode, Json<Value>)> {
if raw.get("agent_owner").is_some()
&& (extension_flag(raw, "top_level")
|| raw.get("feed_types").is_some()
|| raw.get("depth_limit").is_some())
{
return Err(api_error(
StatusCode::BAD_REQUEST,
"agent_owner is not supported with channel-window, feed, or thread filters",
));
}
Ok(())
}
async fn enrich_agent_owners(
state: &AppState,
tenant: &buzz_core::tenant::TenantContext,
events: &mut [Value],
) -> Result<(), (StatusCode, Json<Value>)> {
let pubkeys: Vec<Vec<u8>> = events
.iter()
.filter_map(|event| event.get("pubkey").and_then(Value::as_str))
.filter_map(|pubkey| hex::decode(pubkey).ok())
.collect();
let owners = state
.db
.get_agent_owners(tenant.community(), &pubkeys)
.await
.map_err(|e| internal_error(&format!("agent owner lookup error: {e}")))?;
let owners: std::collections::HashMap<Vec<u8>, buzz_db::user::AgentOwner> = owners
.into_iter()
.map(|owner| (owner.agent_pubkey.clone(), owner))
.collect();
for event in events {
let Some(agent_pubkey) = event
.get("pubkey")
.and_then(Value::as_str)
.and_then(|pubkey| hex::decode(pubkey).ok())
else {
continue;
};
let Some(owner) = owners.get(&agent_pubkey) else {
continue;
};
let Some(object) = event.as_object_mut() else {
continue;
};
object.insert(
"agent_owner_pubkey".into(),
Value::String(hex::encode(&owner.owner_pubkey)),
);
object.insert(
"agent_owner_display_name".into(),
owner
.owner_display_name
.clone()
.map(Value::String)
.unwrap_or(Value::Null),
);
}
Ok(())
}
fn extract_depth_limit(raw: &Value) -> Option<u32> {
raw.get("depth_limit")?
.as_u64()
@@ -1073,20 +967,8 @@ async fn query_events_authed(
// Two-pass parse: preserve raw JSON for custom extension fields (before_id,
// depth_limit, feed_types) that nostr::Filter silently drops.
let mut raw_filters: Vec<Value> = serde_json::from_slice(body)
let raw_filters: Vec<Value> = serde_json::from_slice(body)
.map_err(|e| api_error(StatusCode::BAD_REQUEST, &format!("invalid filters: {e}")))?;
for raw in &mut raw_filters {
reject_unsupported_agent_owner_filter(raw)?;
let Some(owner_pubkey) = extract_agent_owner(raw)? else {
continue;
};
let owned_pubkeys = state
.db
.list_agent_pubkeys_by_owner(tenant.community(), &owner_pubkey)
.await
.map_err(|e| internal_error(&format!("agent owner filter error: {e}")))?;
apply_agent_owner_authors(raw, &owned_pubkeys);
}
let filters: Vec<nostr::Filter> = raw_filters
.iter()
.map(|v| serde_json::from_value(v.clone()))
@@ -1323,14 +1205,6 @@ async fn query_events_authed(
if handled.contains(&idx) {
continue;
}
if raw.get("agent_owner").is_some()
&& raw
.get("authors")
.and_then(Value::as_array)
.is_some_and(Vec::is_empty)
{
continue;
}
if let Some(ch_id) = extract_channel_from_filter(filter) {
if !accessible_channels.contains(&ch_id) {
@@ -1432,9 +1306,6 @@ async fn query_events_authed(
}
}
if include_agent_owner(&raw_filters) {
enrich_agent_owners(state, tenant, &mut events).await?;
}
Ok(Json(Value::Array(events)))
}
@@ -1779,14 +1650,6 @@ async fn handle_bridge_search(
for (raw, filter) in raw_filters.iter().zip(filters) {
let search_mode = extract_search_mode(raw);
let search_page = extract_search_page(raw);
if raw.get("agent_owner").is_some()
&& raw
.get("authors")
.and_then(Value::as_array)
.is_some_and(Vec::is_empty)
{
continue;
}
let search_text = match &filter.search {
Some(s) if !s.is_empty() => s.clone(),
_ => continue,
@@ -1900,9 +1763,6 @@ async fn handle_bridge_search(
}
}
if include_agent_owner(raw_filters) {
enrich_agent_owners(state, tenant, &mut events).await?;
}
Ok(Json(Value::Array(events)))
}
@@ -2928,66 +2788,6 @@ mod tests {
);
}
#[test]
fn extract_agent_owner_accepts_hex_and_rejects_invalid_values() {
let owner = "a".repeat(64);
assert_eq!(
extract_agent_owner(&serde_json::json!({"agent_owner": owner}))
.unwrap()
.unwrap(),
vec![0xaa; 32]
);
assert!(extract_agent_owner(&serde_json::json!({"agent_owner": "bad"})).is_err());
assert!(extract_agent_owner(&serde_json::json!({"agent_owner": true})).is_err());
assert!(extract_agent_owner(&serde_json::json!({"agent_owner": 42})).is_err());
assert_eq!(
extract_agent_owner(&serde_json::json!({"agent_owner": null})).unwrap(),
None
);
assert_eq!(extract_agent_owner(&serde_json::json!({})).unwrap(), None);
}
#[test]
fn agent_owner_authors_fail_closed_and_intersect_requested_authors() {
let agent_a = vec![0xaa; 32];
let agent_b = vec![0xbb; 32];
let owned = vec![agent_a.clone(), agent_b];
let mut unscoped = serde_json::json!({});
apply_agent_owner_authors(&mut unscoped, &owned);
let authors = unscoped["authors"].as_array().unwrap();
assert_eq!(authors.len(), 2);
assert!(authors.contains(&serde_json::json!(hex::encode(&agent_a))));
let mut intersected = serde_json::json!({
"authors": [hex::encode(&agent_a), "cc".repeat(32)]
});
apply_agent_owner_authors(&mut intersected, &owned);
assert_eq!(
intersected["authors"],
serde_json::json!([hex::encode(agent_a)])
);
let mut empty = serde_json::json!({"authors": ["cc".repeat(32)]});
apply_agent_owner_authors(&mut empty, &owned);
assert_eq!(empty["authors"], serde_json::json!([]));
}
#[test]
fn agent_owner_rejects_specialized_filters_that_cannot_enforce_authors() {
for raw in [
serde_json::json!({"agent_owner": "aa".repeat(32), "top_level": true}),
serde_json::json!({"agent_owner": "aa".repeat(32), "feed_types": ["activity"]}),
serde_json::json!({"agent_owner": "aa".repeat(32), "depth_limit": 1}),
] {
assert!(reject_unsupported_agent_owner_filter(&raw).is_err());
}
assert!(reject_unsupported_agent_owner_filter(
&serde_json::json!({"agent_owner": "aa".repeat(32), "kinds": [0]})
)
.is_ok());
}
/// `nip42_expected_relay_url` derives scheme from `config_relay_url`'s
/// prefix: `wss://` → `wss`, everything else → `ws`. Deployments that run
/// `ws://` in dev/test must produce a `ws://` URL that matches what
@@ -3555,36 +3355,6 @@ mod tests {
/// Drive a single POST /events request through the router and return the
/// HTTP status code.
async fn post_query(
state: Arc<crate::state::AppState>,
host: &str,
pubkey_hex: &str,
body: &[u8],
) -> (axum::http::StatusCode, Value) {
use axum::body::{to_bytes, Body};
use axum::http::{header, Request};
use tower::ServiceExt;
let response = crate::router::build_router(state)
.oneshot(
Request::builder()
.method("POST")
.uri("/query")
.header(header::HOST, host)
.header("x-pubkey", pubkey_hex)
.body(Body::from(body.to_vec()))
.expect("build request"),
)
.await
.expect("router oneshot");
let status = response.status();
let bytes = to_bytes(response.into_body(), usize::MAX)
.await
.expect("read response body");
let body = serde_json::from_slice(&bytes).expect("parse response JSON");
(status, body)
}
async fn post_events(
state: Arc<crate::state::AppState>,
host: &str,
@@ -3610,102 +3380,6 @@ mod tests {
.status()
}
#[tokio::test]
#[ignore = "requires Postgres and Redis"]
async fn query_agent_owner_returns_only_verified_owner_matches() {
let state = bridge_handler_test_state()
.await
.expect("local Postgres and Redis required");
let host = format!("bridge-owner-{}.local", uuid::Uuid::new_v4().simple());
let community = state
.db
.ensure_configured_community(&host)
.await
.expect("ensure community")
.id;
let requester = Keys::generate();
let owner_a = Keys::generate();
let owner_b = Keys::generate();
let agent_a = Keys::generate();
let agent_b = Keys::generate();
for keys in [&requester, &owner_a, &owner_b, &agent_a, &agent_b] {
state
.db
.ensure_user(community, &keys.public_key().to_bytes())
.await
.expect("ensure user");
}
state
.db
.set_agent_owner(
community,
&agent_a.public_key().to_bytes(),
&owner_a.public_key().to_bytes(),
)
.await
.expect("set first owner");
state
.db
.set_agent_owner(
community,
&agent_b.public_key().to_bytes(),
&owner_b.public_key().to_bytes(),
)
.await
.expect("set second owner");
for keys in [&agent_a, &agent_b] {
let event = EventBuilder::new(Kind::Metadata, r#"{"display_name":"Honey"}"#)
.sign_with_keys(keys)
.expect("sign profile");
state
.db
.insert_event(community, &event, None)
.await
.expect("insert profile");
}
let filter = serde_json::json!([{
"kinds": [0],
"search": "Honey",
"agent_owner": owner_a.public_key().to_hex(),
"include_agent_owner": true,
"limit": 100
}]);
let (status, body) = post_query(
state.clone(),
&host,
&requester.public_key().to_hex(),
filter.to_string().as_bytes(),
)
.await;
assert_eq!(status, StatusCode::OK, "query response: {body}");
let results = body.as_array().expect("array response");
assert_eq!(results.len(), 1);
assert_eq!(results[0]["pubkey"], agent_a.public_key().to_hex());
assert_eq!(
results[0]["agent_owner_pubkey"],
owner_a.public_key().to_hex()
);
let no_match = serde_json::json!([{
"kinds": [0],
"search": "Honey",
"agent_owner": requester.public_key().to_hex(),
"limit": 100
}]);
let (status, body) = post_query(
state,
&host,
&requester.public_key().to_hex(),
no_match.to_string().as_bytes(),
)
.await;
assert_eq!(status, StatusCode::OK, "query response: {body}");
assert_eq!(body, serde_json::json!([]));
}
/// Collect buzz_events_rejected_total with (transport, reason) labels from
/// a DebuggingRecorder snapshot.
fn http_reject_counts(
@@ -1,3 +0,0 @@
CREATE INDEX idx_users_agent_owner
ON users (community_id, agent_owner_pubkey)
WHERE agent_owner_pubkey IS NOT NULL;