travelagent-core 1.11.1

Core library for travelagent code review tool
Documentation
//! Ingress-side guards that both MCP surfaces run on agent-supplied input
//! before touching session state, the forge, or the channel between the
//! bridge and the TUI.
//!
//! The underlying byte-size caps and the canonical error message live in
//! [`crate::mcp_limits`]. This module layers the MCP-shaped JSON envelope
//! on top so both servers emit identical `{"error": "..."}` payloads
//! without hand-rolling JSON escaping.

use crate::mcp_limits::check_body_size;

/// Run the shared byte-size check against a free-form agent body. Returns
/// the ready-to-send MCP error envelope (`{"error": "..."}`) when the body
/// is over the cap, or `None` when it fits.
///
/// Callers pass the user-facing name of the input (e.g. "comment body",
/// "review body") so the error mirrors the wording in
/// [`crate::mcp_limits::check_body_size`] exactly.
///
/// The message is serialized through `serde_json` so arbitrary user text —
/// including quotes, backslashes, newlines, and non-ASCII — can't produce
/// malformed JSON. On the (effectively impossible) serialization failure a
/// static fallback envelope is returned.
#[must_use]
pub fn check_body_size_json(name: &str, body: &str, limit: usize) -> Option<String> {
    let msg = check_body_size(name, body, limit)?;
    Some(
        serde_json::to_string(&serde_json::json!({ "error": msg }))
            .unwrap_or_else(|_| r#"{"error":"input exceeds limit"}"#.to_string()),
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::mcp_limits::MCP_COMMENT_BODY_MAX;

    #[test]
    fn body_at_limit_returns_none() {
        let body = "x".repeat(MCP_COMMENT_BODY_MAX);
        assert!(check_body_size_json("comment body", &body, MCP_COMMENT_BODY_MAX).is_none());
    }

    #[test]
    fn body_over_limit_returns_json_envelope() {
        let body = "x".repeat(MCP_COMMENT_BODY_MAX + 1);
        let envelope = check_body_size_json("comment body", &body, MCP_COMMENT_BODY_MAX)
            .expect("over-limit body must return an envelope");
        let parsed: serde_json::Value =
            serde_json::from_str(&envelope).expect("envelope must parse as JSON");
        let err = parsed["error"].as_str().expect("error field");
        assert!(err.contains("comment body"));
        assert!(err.contains("Input exceeds"));
    }

    #[test]
    fn weird_characters_in_name_stay_escaped() {
        // Agents don't supply `name`, but guard against a caller that passes
        // a dynamically-built label anyway.
        let body = "x".repeat(MCP_COMMENT_BODY_MAX + 1);
        let envelope =
            check_body_size_json("weird\"name\nwith\\breaks", &body, MCP_COMMENT_BODY_MAX)
                .expect("over-limit body must return an envelope");
        let parsed: serde_json::Value =
            serde_json::from_str(&envelope).expect("envelope must parse as JSON");
        assert!(parsed["error"].is_string());
    }

    #[test]
    fn empty_body_fits_every_limit() {
        assert!(check_body_size_json("any", "", MCP_COMMENT_BODY_MAX).is_none());
    }
}