use crate::mcp::param_names;
use serde_json::{Value, json};
pub fn handle_quota_status(conn: &rusqlite::Connection, params: &Value) -> Result<Value, String> {
let agent_id = params.get(param_names::AGENT_ID).and_then(Value::as_str);
let namespace = params.get(param_names::NAMESPACE).and_then(Value::as_str);
match (agent_id, namespace) {
(Some(aid), Some(ns)) => {
let row = crate::quotas::get_status(conn, aid, ns).map_err(|e| e.to_string())?;
Ok(json!({
"agent_id": aid,
"namespace": ns,
"quota": row,
}))
}
(Some(aid), None) => {
let row = crate::quotas::get_aggregate_status(conn, aid).map_err(|e| e.to_string())?;
Ok(json!({
"agent_id": aid,
"namespace": crate::quotas::GLOBAL_NAMESPACE,
"quota": row,
}))
}
(None, Some(ns)) => {
let rows = crate::quotas::list_status(conn, Some(ns)).map_err(|e| e.to_string())?;
Ok(json!({
"count": rows.len(),
"namespace": ns,
"quotas": rows,
}))
}
(None, None) => {
let rows = crate::quotas::list_status(conn, None).map_err(|e| e.to_string())?;
Ok(json!({
"count": rows.len(),
"quotas": rows,
}))
}
}
}
use crate::mcp::registry::McpTool;
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
#[allow(dead_code)]
pub struct QuotaStatusRequest {
#[serde(default)]
pub agent_id: Option<String>,
#[serde(default)]
pub namespace: Option<String>,
}
#[allow(dead_code)]
pub struct QuotaStatusTool;
impl McpTool for QuotaStatusTool {
fn name() -> &'static str {
crate::mcp::registry::tool_names::MEMORY_QUOTA_STATUS
}
fn description() -> &'static str {
"Report per-agent + per-namespace quota usage. Operator-facing."
}
fn docs() -> &'static str {
"K8/#1156: per-agent + per-namespace quota usage (memories/day, \
storage bytes, links/day). Omit agent_id for all. Omit namespace \
for the aggregate view (sum across namespaces). Supply both for \
the single row."
}
fn input_schema() -> Value {
crate::mcp::registry::input_schema_for::<QuotaStatusRequest>()
}
fn family() -> &'static str {
crate::profile::Family::Power.name()
}
}
#[cfg(test)]
mod d1_5_986_tests {
use super::*;
use crate::mcp::parity_test_helpers::{
assert_descriptions_match, assert_property_set_parity, derived_props_for,
};
#[test]
fn quota_status_parity_986() {
let derived = derived_props_for::<QuotaStatusRequest>();
assert_property_set_parity("memory_quota_status", &derived);
assert_descriptions_match("memory_quota_status", &derived);
}
#[test]
fn quota_status_tool_metadata_986() {
assert_eq!(QuotaStatusTool::name(), "memory_quota_status");
assert_eq!(QuotaStatusTool::family(), "power");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage as db;
fn fresh_conn() -> rusqlite::Connection {
db::open(std::path::Path::new(":memory:")).expect("open in-memory db")
}
#[test]
fn per_agent_returns_aggregate_for_unknown_id() {
let conn = fresh_conn();
let resp = handle_quota_status(&conn, &json!({"agent_id": "ai:alice"})).expect("ok");
assert_eq!(resp["agent_id"].as_str(), Some("ai:alice"));
assert_eq!(resp["namespace"].as_str(), Some("_global"));
let quota = &resp["quota"];
assert!(quota.is_object());
assert_eq!(quota["agent_id"].as_str(), Some("ai:alice"));
assert!(quota["max_memories_per_day"].as_i64().unwrap_or(0) > 0);
}
#[test]
fn per_agent_namespace_returns_single_row() {
let conn = fresh_conn();
let resp = handle_quota_status(
&conn,
&json!({"agent_id": "ai:alice", "namespace": "team/policies"}),
)
.expect("ok");
assert_eq!(resp["agent_id"].as_str(), Some("ai:alice"));
assert_eq!(resp["namespace"].as_str(), Some("team/policies"));
let quota = &resp["quota"];
assert_eq!(quota["namespace"].as_str(), Some("team/policies"));
}
#[test]
fn list_path_returns_count_and_rows() {
let conn = fresh_conn();
let _ = handle_quota_status(&conn, &json!({"agent_id": "ai:bob"})).expect("seed bob");
let _ = handle_quota_status(&conn, &json!({"agent_id": "ai:carol"})).expect("seed carol");
let resp = handle_quota_status(&conn, &json!({})).expect("ok");
assert!(resp["count"].as_u64().unwrap() >= 2);
let quotas = resp["quotas"].as_array().expect("quotas array");
assert!(quotas.len() >= 2);
}
#[test]
fn list_path_namespace_filter_only_returns_matching_rows() {
let conn = fresh_conn();
let _ = handle_quota_status(
&conn,
&json!({"agent_id": "ai:bob", "namespace": "team/policies"}),
)
.expect("seed");
let _ = handle_quota_status(
&conn,
&json!({"agent_id": "ai:carol", "namespace": "team/policies"}),
)
.expect("seed");
let _ = handle_quota_status(
&conn,
&json!({"agent_id": "ai:bob", "namespace": "alice/scratch"}),
)
.expect("seed");
let resp = handle_quota_status(&conn, &json!({"namespace": "team/policies"})).expect("ok");
assert_eq!(resp["namespace"].as_str(), Some("team/policies"));
let quotas = resp["quotas"].as_array().expect("quotas array");
for q in quotas {
assert_eq!(q["namespace"].as_str(), Some("team/policies"));
}
}
#[test]
fn list_path_empty_db_returns_zero() {
let conn = fresh_conn();
let resp = handle_quota_status(&conn, &json!({})).expect("ok");
assert_eq!(resp["count"].as_u64(), Some(0));
assert_eq!(resp["quotas"].as_array().unwrap().len(), 0);
}
#[test]
fn per_agent_idempotent_repeated_reads() {
let conn = fresh_conn();
let one = handle_quota_status(&conn, &json!({"agent_id": "ai:dup"})).expect("ok1");
let two = handle_quota_status(&conn, &json!({"agent_id": "ai:dup"})).expect("ok2");
assert_eq!(one["agent_id"], two["agent_id"]);
assert_eq!(
one["quota"]["max_memories_per_day"],
two["quota"]["max_memories_per_day"]
);
}
}