use super::*;
use crate::core::registry::IndexRegistry;
use crate::service::stall_tracker::EmbedderStallTracker;
use axum::extract::State;
use axum::Json;
use std::sync::Arc;
fn ready_state_with_tracker() -> (Arc<SearchAppState>, Arc<EmbedderStallTracker>) {
use crate::core::embed::MockEmbedder;
let registry = IndexRegistry::new();
let state =
Arc::new(SearchAppState::new(registry).with_embedder(Arc::new(MockEmbedder::new(4))));
let tracker = Arc::clone(&state.embedder_stall_tracker);
(state, tracker)
}
#[tokio::test]
async fn health_reports_ready_when_no_timeouts() {
let (state, _tracker) = ready_state_with_tracker();
let Json(resp) = health_handler(State(state)).await;
assert_eq!(resp.embedder, "ready", "no timeouts → must be ready");
assert_eq!(
resp.embedder_recent_timeout_count, 0,
"zero timeouts expected"
);
assert!(
resp.embedder_last_ok_secs_ago.is_none(),
"no embed call yet → last_ok_secs_ago must be absent"
);
}
#[tokio::test]
async fn health_reports_stalled_after_embed_timeouts() {
let (state, tracker) = ready_state_with_tracker();
tracker.record_timeout();
tracker.record_timeout();
let Json(resp) = health_handler(State(state)).await;
assert_eq!(
resp.embedder, "stalled",
"recent timeouts → embedder must be \"stalled\"; got {:?}",
resp.embedder
);
assert_eq!(
resp.embedder_recent_timeout_count, 2,
"two timeouts must be reflected in the count"
);
assert!(
resp.embedder_last_ok_secs_ago.is_none(),
"no success yet → last_ok_secs_ago must be absent"
);
}
#[tokio::test]
async fn health_recovers_to_ready_after_stall_clears() {
let (state, tracker) = ready_state_with_tracker();
tracker.record_timeout();
tracker.record_timeout();
tracker.record_timeout();
tracker.record_success();
let Json(resp) = health_handler(State(state)).await;
assert_eq!(
resp.embedder, "ready",
"after success the stall must clear; got {:?}",
resp.embedder
);
assert_eq!(
resp.embedder_recent_timeout_count, 0,
"success must reset timeout count to 0"
);
let ago = resp
.embedder_last_ok_secs_ago
.expect("last_ok_secs_ago must be present after a success");
assert!(
ago < 5,
"last_ok_secs_ago should be near-zero seconds; got {ago}"
);
}
#[test]
fn health_response_contains_stall_fields() {
use crate::service::server::health::HealthResponse;
use crate::service::server::state::WarmBootSummary;
use serde_json::Value;
let resp = HealthResponse {
status: "ok",
version: "0.0.0",
indexes: 1,
uptime_secs: 10,
embedder: "stalled",
embedder_error: None,
embedder_last_ok_secs_ago: Some(120),
embedder_recent_timeout_count: 3,
rss_mb: 0,
rss_limit_mb: 0,
disk_bytes: 0,
cpu_pct: 0.0,
embedder_info: None,
embedderd_rss_mb: None,
background_reindex_queue_depth: 0,
update_available: None,
warmboot_summary: WarmBootSummary::default(),
};
let json: Value = serde_json::to_value(&resp).expect("serialize");
assert_eq!(
json["embedder"].as_str(),
Some("stalled"),
"embedder field must be 'stalled'"
);
assert_eq!(
json["embedder_recent_timeout_count"].as_u64(),
Some(3),
"timeout count must be present"
);
assert_eq!(
json["embedder_last_ok_secs_ago"].as_u64(),
Some(120),
"last_ok_secs_ago must be present when Some"
);
}
#[test]
fn health_response_omits_last_ok_when_none() {
use crate::service::server::health::HealthResponse;
use crate::service::server::state::WarmBootSummary;
use serde_json::Value;
let resp = HealthResponse {
status: "ok",
version: "0.0.0",
indexes: 0,
uptime_secs: 0,
embedder: "initializing",
embedder_error: None,
embedder_last_ok_secs_ago: None,
embedder_recent_timeout_count: 0,
rss_mb: 0,
rss_limit_mb: 0,
disk_bytes: 0,
cpu_pct: 0.0,
embedder_info: None,
embedderd_rss_mb: None,
background_reindex_queue_depth: 0,
update_available: None,
warmboot_summary: WarmBootSummary::default(),
};
let json: Value = serde_json::to_value(&resp).expect("serialize");
assert!(
json.get("embedder_last_ok_secs_ago").is_none(),
"embedder_last_ok_secs_ago must be absent (not null) when None; \
json={json}"
);
}