use cortex_mcp::serve::handle_line;
use cortex_mcp::tool_handler::{GateId, ToolError, ToolHandler};
use cortex_mcp::tool_registry::ToolRegistry;
use serde_json::Value;
struct EchoTool;
impl ToolHandler for EchoTool {
fn name(&self) -> &'static str {
"mock_echo"
}
fn gate_set(&self) -> &'static [GateId] {
&[GateId::HealthRead]
}
fn call(&self, params: Value) -> Result<Value, ToolError> {
Ok(params)
}
}
struct FailingTool;
impl ToolHandler for FailingTool {
fn name(&self) -> &'static str {
"mock_fail"
}
fn gate_set(&self) -> &'static [GateId] {
&[GateId::HealthRead]
}
fn call(&self, _params: Value) -> Result<Value, ToolError> {
Err(ToolError::InvalidParams("deliberately bad".into()))
}
}
fn registry_with_echo() -> ToolRegistry {
let mut r = ToolRegistry::new();
r.register(Box::new(EchoTool));
r
}
fn registry_with_failing_tool() -> ToolRegistry {
let mut r = ToolRegistry::new();
r.register(Box::new(FailingTool));
r
}
fn registry_with_suggest() -> ToolRegistry {
use cortex_mcp::tools::suggest::CortexSuggestTool;
use rusqlite::Connection;
use std::sync::{Arc, Mutex};
let conn = Connection::open_in_memory().expect("in-memory sqlite");
cortex_store::migrate::apply_pending(&conn).expect("apply_pending");
let pool = Arc::new(Mutex::new(conn));
let mut r = ToolRegistry::new();
r.register(Box::new(CortexSuggestTool::new(pool)));
r
}
#[test]
fn valid_jsonrpc_request_routes_to_tool() {
let registry = registry_with_echo();
let line = r#"{"jsonrpc":"2.0","method":"mock_echo","params":{"key":"value"},"id":42}"#;
let resp = handle_line(line, ®istry).expect("expected a response for a valid request");
assert!(
resp.get("error").is_none(),
"response must not contain an error field: {resp}"
);
assert_eq!(
resp["result"]["key"], "value",
"result must carry the echoed params"
);
assert_eq!(resp["id"], 42, "id must be echoed back");
assert_eq!(resp["jsonrpc"], "2.0", "jsonrpc version must be 2.0");
}
#[test]
fn malformed_json_returns_parse_error_32700() {
let registry = ToolRegistry::new();
let resp =
handle_line("not json", ®istry).expect("parse error must produce a response, not None");
assert_eq!(
resp["error"]["code"], -32700,
"malformed JSON must return code -32700: {resp}"
);
assert_eq!(
resp["id"],
Value::Null,
"id must be null when request cannot be parsed"
);
}
#[test]
fn unknown_method_returns_method_not_found_32601() {
let registry = ToolRegistry::new();
let line = r#"{"jsonrpc":"2.0","method":"nonexistent","params":{},"id":1}"#;
let resp = handle_line(line, ®istry).expect("method-not-found must produce a response");
assert_eq!(
resp["error"]["code"], -32601,
"unknown method must return code -32601: {resp}"
);
assert_eq!(
resp["id"], 1,
"id must be echoed back even on method-not-found"
);
}
#[test]
fn tool_error_returns_application_error_32000() {
let registry = registry_with_failing_tool();
let line = r#"{"jsonrpc":"2.0","method":"mock_fail","params":{},"id":99}"#;
let resp = handle_line(line, ®istry).expect("tool error must produce a response");
assert_eq!(
resp["error"]["code"], -32000,
"ToolError must map to code -32000: {resp}"
);
assert_eq!(
resp["id"], 99,
"id must be echoed back on application error"
);
let message = resp["error"]["message"]
.as_str()
.expect("error.message must be a string");
assert!(
message.contains("deliberately bad"),
"error message must surface the ToolError detail: {message}"
);
let data = &resp["error"]["data"];
assert_eq!(
data["schema"], "cortex_refusal_resolution.v1",
"application errors must carry a structured resolution envelope: {resp}"
);
assert_eq!(data["kind"], "invalid_params");
assert!(
data["next_actions"]
.as_array()
.is_some_and(|a| !a.is_empty()),
"resolution envelope must include operator-facing next actions: {data}"
);
}
#[test]
fn null_id_is_notification_no_response() {
let registry = registry_with_echo();
let line = r#"{"jsonrpc":"2.0","method":"mock_echo","params":{"x":1},"id":null}"#;
let result = handle_line(line, ®istry);
assert!(
result.is_none(),
"a request with id=null is a notification and must produce no response"
);
}
#[test]
fn missing_id_is_notification_no_response() {
let registry = registry_with_echo();
let line = r#"{"jsonrpc":"2.0","method":"mock_echo","params":{"x":1}}"#;
let result = handle_line(line, ®istry);
assert!(
result.is_none(),
"a request without an id field is a notification and must produce no response"
);
}
#[test]
fn cortex_suggest_empty_store_returns_empty_array() {
let registry = registry_with_suggest();
let line = r#"{"jsonrpc":"2.0","method":"cortex_suggest","params":{},"id":1}"#;
let resp = handle_line(line, ®istry).expect("cortex_suggest must produce a response");
assert!(
resp.get("error").is_none(),
"cortex_suggest must not return an error on an empty store: {resp}"
);
assert_eq!(
resp["result"]["suggestions"],
serde_json::json!([]),
"cortex_suggest on empty store must return an empty suggestions array"
);
assert!(
resp["result"]["query_used"].is_null(),
"cortex_suggest without query must return null query_used"
);
}
struct SizeLimitedTool;
impl ToolHandler for SizeLimitedTool {
fn name(&self) -> &'static str {
"mock_size_limited"
}
fn gate_set(&self) -> &'static [GateId] {
&[GateId::SessionWrite, GateId::CommitWrite]
}
fn call(&self, params: Value) -> Result<Value, ToolError> {
const MAX_BYTES: usize = 5_242_880;
let events_json = params["events_json"]
.as_str()
.ok_or_else(|| ToolError::InvalidParams("events_json is required".into()))?;
if events_json.len() > MAX_BYTES {
return Err(ToolError::SizeLimitExceeded(
"events_json exceeds maximum size".into(),
));
}
Ok(serde_json::json!({ "ingested": 0 }))
}
}
#[test]
fn oversized_events_json_rejected() {
let mut registry = ToolRegistry::new();
registry.register(Box::new(SizeLimitedTool));
let oversized_value = "x".repeat(5_242_881);
let request = serde_json::json!({
"jsonrpc": "2.0",
"method": "mock_size_limited",
"params": { "events_json": oversized_value },
"id": 1
});
let line = serde_json::to_string(&request).expect("serialise request");
let resp = handle_line(&line, ®istry).expect("size-limit error must produce a response");
assert_eq!(
resp["error"]["code"], -32000,
"oversized payload must return code -32000: {resp}"
);
let message = resp["error"]["message"]
.as_str()
.expect("error.message must be a string");
assert!(
message.contains("exceeds maximum size"),
"error message must mention size limit: {message}"
);
}