use std::time::Duration;
use do_memory_mcp::monitoring::MonitoringSystem;
use do_memory_mcp::monitoring::types::{MonitoringConfig, PerformanceMetrics, ToolPerformance};
const SLEEP: Duration = Duration::from_millis(30);
const MIN_REPORTED_MS: u64 = 20;
const MAX_REPORTED_MS: u64 = 200;
const MIN_REPORTED_F64: f64 = 20.0;
const MAX_REPORTED_F64: f64 = 200.0;
fn monitoring() -> MonitoringSystem {
MonitoringSystem::new(MonitoringConfig {
enabled: true,
..Default::default()
})
}
async fn timed_request(monitoring: &MonitoringSystem, tool: &str) {
let id = monitoring
.start_request(format!("{tool}-req"), tool.to_string())
.await;
tokio::time::sleep(SLEEP).await;
monitoring.end_request(&id, true, None).await;
}
fn tool_perf<'a>(perf: &'a PerformanceMetrics, tool: &str) -> &'a ToolPerformance {
assert!(
perf.tool_metrics.contains_key(tool),
"no tool metrics recorded for '{tool}'"
);
&perf.tool_metrics[tool]
}
#[tokio::test]
async fn sub_second_requests_report_millisecond_latency() {
let monitoring = monitoring();
for _ in 0..5 {
timed_request(&monitoring, "memory_search").await;
}
let perf = monitoring.get_performance();
let tool = tool_perf(&perf, "memory_search");
assert_eq!(tool.total_calls, 5);
assert!(
tool.min_response_time_ms >= MIN_REPORTED_MS,
"a 30ms request must not report {0}ms; second-truncated wall clock reported 0 \
whenever the request stayed inside one second",
tool.min_response_time_ms
);
assert!(
tool.max_response_time_ms <= MAX_REPORTED_MS,
"a 30ms request must not report {0}ms; a request straddling a second boundary was \
rounded up to a full 1000ms",
tool.max_response_time_ms
);
assert!(
tool.avg_response_time_ms >= MIN_REPORTED_F64
&& tool.avg_response_time_ms <= MAX_REPORTED_F64,
"average latency {:.1}ms is outside the plausible band",
tool.avg_response_time_ms
);
}
#[tokio::test]
async fn stats_aggregate_the_monotonic_duration() {
let monitoring = monitoring();
timed_request(&monitoring, "record_episode").await;
let stats = monitoring.get_stats();
assert_eq!(stats.total_requests, 1);
assert_eq!(stats.successful_requests, 1);
assert!(
stats.avg_response_time_ms >= MIN_REPORTED_F64,
"stats recorded {:.1}ms for a {}ms request",
stats.avg_response_time_ms,
SLEEP.as_millis()
);
assert!(
stats.avg_response_time_ms < 1000.0,
"stats recorded {:.1}ms — still quantised to whole seconds",
stats.avg_response_time_ms
);
}
#[tokio::test]
async fn latency_is_non_negative_and_unknown_requests_are_ignored() {
let monitoring = monitoring();
let id = monitoring
.start_request("orphan".to_string(), "tool_a".to_string())
.await;
assert_eq!(monitoring.active_request_count().await, 1);
monitoring.end_request("never-tracked", true, None).await;
assert_eq!(
monitoring.active_request_count().await,
1,
"ending an untracked request must not consume a live one"
);
monitoring
.end_request(&id, false, Some("boom".to_string()))
.await;
assert_eq!(monitoring.active_request_count().await, 0);
let perf = monitoring.get_performance();
let tool = tool_perf(&perf, "tool_a");
assert_eq!(tool.failed_calls, 1);
assert_eq!(tool.successful_calls, 0);
assert!(
tool.min_response_time_ms <= tool.max_response_time_ms,
"min {} must never exceed max {}",
tool.min_response_time_ms,
tool.max_response_time_ms
);
}
#[tokio::test]
async fn exports_remain_valid_and_key_stable() {
let monitoring = monitoring();
timed_request(&monitoring, "memory_search").await;
timed_request(&monitoring, "record_episode").await;
let stats_json = serde_json::to_string(&monitoring.get_stats()).unwrap();
let perf_json = serde_json::to_string(&monitoring.get_performance()).unwrap();
let stats: serde_json::Value = serde_json::from_str(&stats_json).unwrap();
let perf: serde_json::Value = serde_json::from_str(&perf_json).unwrap();
assert!(
stats["avg_response_time_ms"].is_number(),
"avg_response_time_ms must stay a number, got {stats}"
);
assert!(
stats["avg_response_time_ms"].as_f64().unwrap_or_default() > 0.0,
"exported JSON must carry the corrected sub-second value"
);
let tools = perf["tool_metrics"]
.as_object()
.expect("tool_metrics object");
let mut keys: Vec<&str> = tools.keys().map(String::as_str).collect();
keys.sort_unstable();
assert_eq!(
keys,
vec!["memory_search", "record_episode"],
"one label per tool, no new or dropped metric keys"
);
for tool in tools.values() {
assert!(
tool["min_response_time_ms"].is_number() && tool["max_response_time_ms"].is_number(),
"latency fields must stay numeric in the export, got {tool}"
);
}
}