use crate::models::field_names;
use anyhow::Result;
use clap::Args;
use serde_json::{Value, json};
use crate::cli::CliOutput;
use crate::storage as db;
#[derive(Args, Debug, Clone)]
pub struct SubscribeArgs {
#[arg(long, value_name = "URL")]
pub url: String,
#[arg(long, value_name = "CSV")]
pub events: Option<String>,
#[arg(long, value_name = "SECRET")]
pub secret: Option<String>,
#[arg(long = "namespace-filter", value_name = "NS")]
pub namespace_filter: Option<String>,
#[arg(long = "agent-filter", value_name = "AGENT_ID")]
pub agent_filter: Option<String>,
#[arg(long = "event-types", value_name = "CSV", value_delimiter = ',')]
pub event_types: Vec<String>,
#[arg(long)]
pub json: bool,
}
pub fn cmd_subscribe(
db_path: &std::path::Path,
args: &SubscribeArgs,
out: &mut CliOutput<'_>,
) -> Result<()> {
let conn = db::open(db_path)?;
let mut params = json!({"url": args.url});
if let Some(e) = &args.events {
params["events"] = json!(e);
}
if let Some(s) = &args.secret {
params["secret"] = json!(s);
}
if let Some(ns) = &args.namespace_filter {
params[field_names::NAMESPACE_FILTER] = json!(ns);
}
if let Some(a) = &args.agent_filter {
params[field_names::AGENT_FILTER] = json!(a);
}
if !args.event_types.is_empty() {
params[field_names::EVENT_TYPES] = json!(args.event_types);
}
let envelope = crate::mcp::handle_subscribe(&conn, ¶ms, None)
.map_err(|e| anyhow::anyhow!("subscribe: {e}"))?;
if args.json {
writeln!(out.stdout, "{}", serde_json::to_string(&envelope)?)?;
return Ok(());
}
let id = envelope.get("id").and_then(Value::as_str).unwrap_or("?");
let url = envelope.get("url").and_then(Value::as_str).unwrap_or("?");
writeln!(out.stdout, "subscribe: id={id} url={url}")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::test_utils::TestEnv;
#[test]
fn subscribe_cli_unregistered_agent_returns_err() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = SubscribeArgs {
url: "https://example.com/hook".into(),
events: None,
secret: Some("topsecret".into()),
namespace_filter: None,
agent_filter: None,
event_types: vec![],
json: true,
};
let mut out = env.output();
let err = cmd_subscribe(&db, &args, &mut out).expect_err("must fail");
assert!(
err.to_string().contains("subscribe") || err.to_string().contains("register"),
"got: {err}"
);
}
#[test]
fn subscribe_cli_success_json_with_all_params() {
crate::config::set_active_hooks_hmac_secret(None);
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
{
let conn = db::open(&db).unwrap();
let agent_id = crate::identity::resolve_agent_id(None, None).unwrap();
db::register_agent(&conn, &agent_id, "test", &[]).expect("register");
}
let args = SubscribeArgs {
url: "https://example.com/hook".into(),
events: Some("memory_store,memory_link_created".into()),
secret: Some("topsecret".into()),
namespace_filter: Some("ns".into()),
agent_filter: Some("ai:other".into()),
event_types: vec!["memory_store".into()],
json: true,
};
{
let mut out = env.output();
cmd_subscribe(&db, &args, &mut out).expect("subscribe ok");
}
let envelope: Value = serde_json::from_str(env.stdout_str().trim()).expect("json");
assert!(envelope["id"].is_string());
assert_eq!(envelope["url"], "https://example.com/hook");
}
#[test]
fn subscribe_cli_success_text_output() {
crate::config::set_active_hooks_hmac_secret(None);
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
{
let conn = db::open(&db).unwrap();
let agent_id = crate::identity::resolve_agent_id(None, None).unwrap();
db::register_agent(&conn, &agent_id, "test", &[]).expect("register");
}
let args = SubscribeArgs {
url: "https://example.com/hook2".into(),
events: None,
secret: Some("topsecret".into()),
namespace_filter: None,
agent_filter: None,
event_types: vec![],
json: false,
};
{
let mut out = env.output();
cmd_subscribe(&db, &args, &mut out).expect("subscribe ok");
}
let stdout = env.stdout_str();
assert!(stdout.contains("subscribe: id="), "got: {stdout}");
assert!(
stdout.contains("url=https://example.com/hook2"),
"got: {stdout}"
);
}
}