use serde_json::Value;
use trusty_common::console_metrics::{ServiceHealth, make_report};
use crate::service::{
AppState,
handlers::{DepInfo, DepStatus, compute_status},
};
pub(crate) fn descriptor() -> Value {
serde_json::json!({
"name": trusty_common::console_metrics::CONSOLE_METRICS_METHOD,
"description": "Return health and operational metrics for trusty-console polling. \
No arguments required. Returns a ConsoleMetricsReport JSON envelope \
with service_id='trusty-review', version, status (ok/degraded/error), and \
a metrics object containing reviewer_model, dry_run, inference status, \
search_reachable, and analyze_reachable.",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": false
}
})
}
pub(crate) async fn handle_console_metrics(state: &AppState) -> Value {
let reviewer_model = state.config.role_models.reviewer.model.clone();
let version = env!("CARGO_PKG_VERSION");
let search_reachable = state.search.health().await.is_ok_and(|r| r.is_healthy());
let analyze_reachable = match &state.analyze {
Some(a) => a.health().await.is_ok(),
None => false,
};
let inference = state
.inference_probe
.probe(&state.llm, &reviewer_model)
.await;
let deps = DepStatus {
trusty_search: DepInfo {
required: true,
reachable: search_reachable,
},
trusty_analyze: DepInfo {
required: false,
reachable: analyze_reachable,
},
};
let health_str = compute_status(inference, &deps);
let status = if health_str == "ok" {
ServiceHealth::Ok
} else if health_str == "error" {
ServiceHealth::Error
} else {
ServiceHealth::Degraded
};
let metrics = serde_json::json!({
"reviewer_model": reviewer_model,
"dry_run": state.config.dry_run,
"version": version,
"inference": inference,
"search_reachable": search_reachable,
"analyze_reachable": analyze_reachable,
});
let report = make_report(
"trusty-review",
"Trusty Review",
version,
status,
metrics,
1, );
serde_json::to_value(&report).unwrap_or_else(|e| {
serde_json::json!({
"error": format!("console_metrics: to_value failed: {e}")
})
})
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::json;
use super::*;
use crate::{
config::ReviewConfig,
integrations::search_client::{
EmbedderState, HealthResponse as SearchHealth, IndexInfo, SearchClient,
SearchClientError, SearchResult,
},
llm::{LlmError, LlmProvider, LlmRequest, LlmResponse},
service::AppState,
};
use trusty_common::console_metrics::parse_report;
struct OkLlm;
#[async_trait]
impl LlmProvider for OkLlm {
fn name(&self) -> &str {
"ok-cm-stub"
}
async fn complete(&self, req: LlmRequest) -> Result<LlmResponse, LlmError> {
Ok(LlmResponse {
text: "ok".into(),
model: req.model.clone(),
input_tokens: 1,
output_tokens: 1,
latency_ms: 0,
cost_usd: 0.0,
finish_reason: None,
})
}
}
struct AuthErrorLlm;
#[async_trait]
impl LlmProvider for AuthErrorLlm {
fn name(&self) -> &str {
"auth-error-cm-stub"
}
async fn complete(&self, _req: LlmRequest) -> Result<LlmResponse, LlmError> {
Err(LlmError::AccessDenied("bad key".into()))
}
}
struct OkSearch;
#[async_trait]
impl SearchClient for OkSearch {
async fn health(&self) -> Result<SearchHealth, SearchClientError> {
Ok(SearchHealth {
status: "ok".into(),
embedder: EmbedderState::Bool(true),
})
}
async fn list_indexes(&self) -> Result<Vec<IndexInfo>, SearchClientError> {
Ok(vec![])
}
async fn search(
&self,
_: &str,
_: &str,
_: Option<u32>,
) -> Result<Vec<SearchResult>, SearchClientError> {
Ok(vec![])
}
}
struct DownSearch;
#[async_trait]
impl SearchClient for DownSearch {
async fn health(&self) -> Result<SearchHealth, SearchClientError> {
Err(SearchClientError::Unavailable("down".to_string()))
}
async fn list_indexes(&self) -> Result<Vec<IndexInfo>, SearchClientError> {
Err(SearchClientError::Unavailable("down".to_string()))
}
async fn search(
&self,
_: &str,
_: &str,
_: Option<u32>,
) -> Result<Vec<SearchResult>, SearchClientError> {
Err(SearchClientError::Unavailable("down".to_string()))
}
}
fn ok_state() -> AppState {
AppState::new(
ReviewConfig::load(None),
Arc::new(OkLlm),
Arc::new(OkSearch),
None,
)
}
fn degraded_state() -> AppState {
AppState::new(
ReviewConfig::load(None),
Arc::new(OkLlm),
Arc::new(DownSearch),
None,
)
}
fn auth_error_state() -> AppState {
AppState::new(
ReviewConfig::load(None),
Arc::new(AuthErrorLlm),
Arc::new(OkSearch),
None,
)
}
#[test]
fn descriptor_name_matches_contract() {
let d = descriptor();
assert_eq!(
d.get("name").and_then(Value::as_str),
Some(trusty_common::console_metrics::CONSOLE_METRICS_METHOD),
"descriptor name must match CONSOLE_METRICS_METHOD"
);
}
#[test]
fn descriptor_has_input_schema() {
let d = descriptor();
assert!(
d.get("inputSchema").is_some(),
"descriptor must have inputSchema"
);
}
#[test]
fn parse_report_round_trip() {
use trusty_common::console_metrics::make_report;
let report = make_report(
"trusty-review",
"Trusty Review",
"0.1.0",
ServiceHealth::Degraded,
json!({
"reviewer_model": "test-model",
"dry_run": true,
"version": "0.1.0",
"inference": "auth_error",
"search_reachable": false,
"analyze_reachable": false,
}),
1,
);
let raw_value = serde_json::to_value(&report).expect("to_value must succeed");
let wrapped = serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&raw_value)
.expect("to_string_pretty must succeed"),
}],
"isError": false,
});
let decoded = parse_report(&wrapped).expect("parse must succeed");
assert_eq!(decoded.service_id, "trusty-review");
assert_eq!(decoded.display_name, "Trusty Review");
assert_eq!(decoded.version, "0.1.0");
assert_eq!(decoded.status, ServiceHealth::Degraded);
assert_eq!(decoded.metrics_schema_version, 1);
assert_eq!(decoded.metrics["search_reachable"], false);
assert_eq!(decoded.metrics["dry_run"], true);
}
#[tokio::test]
async fn console_metrics_ok_state() {
let state = ok_state();
let raw = handle_console_metrics(&state).await;
let wrapped = serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&raw).expect("to_string_pretty"),
}],
"isError": false,
});
let report = parse_report(&wrapped).expect("parse must succeed");
assert_eq!(report.service_id, "trusty-review");
assert_eq!(report.display_name, "Trusty Review");
assert_eq!(report.status, ServiceHealth::Ok);
assert_eq!(report.metrics["search_reachable"], true);
assert!(report.metrics["reviewer_model"].is_string());
assert!(report.metrics["dry_run"].is_boolean());
assert!(report.metrics["version"].is_string());
assert_eq!(report.metrics["inference"], "ok");
}
#[tokio::test]
async fn console_metrics_degraded_search_down() {
let state = degraded_state();
let raw = handle_console_metrics(&state).await;
let wrapped = serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&raw).expect("to_string_pretty"),
}],
"isError": false,
});
let report = parse_report(&wrapped).expect("parse must succeed");
assert_eq!(report.status, ServiceHealth::Degraded);
assert_eq!(report.metrics["search_reachable"], false);
}
#[tokio::test]
async fn console_metrics_degraded_auth_error() {
let state = auth_error_state();
let raw = handle_console_metrics(&state).await;
let wrapped = serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&raw).expect("to_string_pretty"),
}],
"isError": false,
});
let report = parse_report(&wrapped).expect("parse must succeed");
assert_eq!(report.status, ServiceHealth::Degraded);
let inference = report.metrics["inference"].as_str().unwrap_or("");
assert_ne!(
inference, "ok",
"auth_error state must not report inference=ok"
);
}
#[test]
fn service_health_error_serialises_correctly() {
use trusty_common::console_metrics::make_report;
let report = make_report(
"trusty-review",
"Trusty Review",
"0.1.0",
ServiceHealth::Error,
serde_json::json!({"reason": "catastrophic failure"}),
1,
);
let raw_value = serde_json::to_value(&report).expect("to_value must succeed");
assert_eq!(
raw_value["status"].as_str(),
Some("error"),
"ServiceHealth::Error must serialise to \"error\""
);
let wrapped = serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&raw_value)
.expect("to_string_pretty must succeed"),
}],
"isError": false,
});
let decoded = parse_report(&wrapped).expect("parse must succeed");
assert_eq!(
decoded.status,
ServiceHealth::Error,
"parse_report must decode \"error\" back to ServiceHealth::Error"
);
}
}