huskies: merge 768

This commit is contained in:
dave
2026-04-28 10:06:18 +00:00
parent fb5a21cfbb
commit 0c2789b2c1
11 changed files with 13 additions and 209 deletions
-2
View File
@@ -197,7 +197,6 @@ pub async fn run(
// open supplementary mesh connections for resilience.
{
let sync_handler = poem::get(crate::crdt_sync::crdt_sync_handler);
let health_handler = poem::get(crate::http::health::health);
// Build a minimal AppContext for the crdt_sync_handler (the handler
// receives it via Data<> but doesn't use it — the underscore prefix
@@ -207,7 +206,6 @@ pub async fn run(
let app = poem::Route::new()
.at("/crdt-sync", sync_handler)
.at("/health", health_handler)
.data(agent_ctx_arc);
let bind_addr = format!("0.0.0.0:{port}");
-1
View File
@@ -47,7 +47,6 @@ pub fn build_gateway_route(state_arc: Arc<GatewayState>) -> impl poem::Endpoint
"/mcp",
poem::post(gateway_mcp_post_handler).get(gateway_mcp_get_handler),
)
.at("/health", poem::get(gateway_health_handler))
// Agent join endpoints.
.at("/gateway/mode", poem::get(gateway_mode_handler))
.at(
-23
View File
@@ -806,29 +806,6 @@ pub async fn gateway_event_push_handler(
.into_response()
}
// ── Health handler ──────────────────────────────────────────────────────────
/// HTTP GET `/health` handler for the gateway.
#[handler]
pub async fn gateway_health_handler(state: Data<&Arc<GatewayState>>) -> Response {
let (all_healthy, statuses) = gateway::health_check_all(&state).await;
let body = json!({
"status": if all_healthy { "ok" } else { "degraded" },
"projects": statuses,
});
let status = if all_healthy {
StatusCode::OK
} else {
StatusCode::SERVICE_UNAVAILABLE
};
Response::builder()
.status(status)
.header("Content-Type", "application/json")
.body(Body::from(serde_json::to_vec(&body).unwrap_or_default()))
}
// ── Gateway Web UI ──────────────────────────────────────────────────────────
/// `GET /api/gateway` — returns the list of registered projects and the active project.
-66
View File
@@ -1,66 +0,0 @@
//! Health check endpoint — thin HTTP adapter over `service::health`.
//!
//! Domain logic (the `HealthStatus` type and check function) lives in
//! `service::health`; this module is a thin adapter: call service → shape
//! response.
pub use crate::service::health::HealthStatus;
use poem::handler;
use poem_openapi::{OpenApi, Tags, payload::Json};
/// Health check endpoint.
///
/// Returns a static "ok" response to indicate the server is running.
#[handler]
pub fn health() -> &'static str {
"ok"
}
#[derive(Tags)]
enum HealthTags {
Health,
}
pub struct HealthApi;
#[OpenApi(tag = "HealthTags::Health")]
impl HealthApi {
/// Health check endpoint.
///
/// Returns a JSON status object to confirm the server is running.
#[oai(path = "/health", method = "get")]
async fn health(&self) -> Json<HealthStatus> {
Json(crate::service::health::check())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn handler_health_returns_ok() {
let app = poem::Route::new().at("/health", poem::get(health));
let cli = poem::test::TestClient::new(app);
let resp = cli.get("/health").send().await;
resp.assert_status_is_ok();
resp.assert_text("ok").await;
}
#[test]
fn health_status_serializes_to_json() {
let status = HealthStatus {
status: "ok".to_string(),
};
let json = serde_json::to_value(&status).unwrap();
assert_eq!(json["status"], "ok");
}
#[tokio::test]
async fn api_health_returns_ok_status() {
let api = HealthApi;
let response = api.health().await;
assert_eq!(response.0.status, "ok");
}
}
-6
View File
@@ -8,7 +8,6 @@ pub mod bot_config;
pub mod chat;
pub mod context;
pub mod events;
pub mod health;
pub mod identity;
pub mod io;
pub mod mcp;
@@ -30,7 +29,6 @@ use bot_command::BotCommandApi;
use bot_config::BotConfigApi;
use chat::ChatApi;
use context::AppContext;
use health::HealthApi;
use io::IoApi;
use model::ModelApi;
use poem::EndpointExt;
@@ -92,7 +90,6 @@ pub fn build_routes(
"/mcp",
post(mcp::mcp_post_handler).get(mcp::mcp_get_handler),
)
.at("/health", get(health::health))
.at("/identity", get(identity::identity_handler))
.at(
"/oauth/authorize",
@@ -204,7 +201,6 @@ type ApiTuple = (
ChatApi,
AgentsApi,
SettingsApi,
HealthApi,
BotCommandApi,
wizard::WizardApi,
BotConfigApi,
@@ -222,7 +218,6 @@ pub fn build_openapi_service(ctx: Arc<AppContext>) -> (ApiService, ApiService) {
ChatApi { ctx: ctx.clone() },
AgentsApi { ctx: ctx.clone() },
SettingsApi { ctx: ctx.clone() },
HealthApi,
BotCommandApi { ctx: ctx.clone() },
wizard::WizardApi { ctx: ctx.clone() },
BotConfigApi { ctx: ctx.clone() },
@@ -239,7 +234,6 @@ pub fn build_openapi_service(ctx: Arc<AppContext>) -> (ApiService, ApiService) {
ChatApi { ctx: ctx.clone() },
AgentsApi { ctx: ctx.clone() },
SettingsApi { ctx: ctx.clone() },
HealthApi,
BotCommandApi { ctx: ctx.clone() },
wizard::WizardApi { ctx: ctx.clone() },
BotConfigApi { ctx },
+13 -3
View File
@@ -221,10 +221,20 @@ pub async fn fetch_pipeline_status_for_project(
.map_err(|e| format!("invalid upstream response: {e}"))
}
/// Check health of a single project URL.
/// Check health of a single project URL via the read-RPC `health.check` method.
///
/// Sends an RPC request to the project's `/mcp` endpoint. A successful
/// response (HTTP 2xx) indicates the project container is reachable and
/// serving requests.
pub async fn check_project_health(client: &Client, base_url: &str) -> Result<bool, String> {
let health_url = format!("{}/health", base_url.trim_end_matches('/'));
match client.get(&health_url).send().await {
let mcp_url = format!("{}/mcp", base_url.trim_end_matches('/'));
let rpc_body = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
});
match client.post(&mcp_url).json(&rpc_body).send().await {
Ok(resp) => Ok(resp.status().is_success()),
Err(e) => Err(format!("unreachable: {e}")),
}
-26
View File
@@ -404,32 +404,6 @@ pub async fn init_project(
Ok(registered_name)
}
/// Fetch aggregated health status across all projects.
pub async fn health_check_all(state: &GatewayState) -> (bool, BTreeMap<String, &'static str>) {
let mut all_healthy = true;
let mut statuses = BTreeMap::new();
let project_entries: Vec<(String, String)> = state
.projects
.read()
.await
.iter()
.map(|(n, e)| (n.clone(), e.url.clone()))
.collect();
for (name, url) in &project_entries {
let healthy = io::check_project_health(&state.client, url)
.await
.unwrap_or(false);
if !healthy {
all_healthy = false;
}
statuses.insert(name.clone(), if healthy { "ok" } else { "error" });
}
(all_healthy, statuses)
}
/// Broadcast a status event received from a project node to all local subscribers.
///
/// Returns the number of active receivers that received the event.
-38
View File
@@ -1,38 +0,0 @@
//! Pure health-check logic — no side effects.
use poem_openapi::Object;
use serde::Serialize;
/// The JSON payload returned by the health check endpoint.
#[derive(Serialize, Object)]
pub struct HealthStatus {
/// Human-readable status string, always `"ok"` when the server is healthy.
pub status: String,
}
/// Return a healthy status response.
pub fn ok() -> HealthStatus {
HealthStatus {
status: "ok".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ok_returns_status_ok() {
let s = ok();
assert_eq!(s.status, "ok");
}
#[test]
fn health_status_serializes() {
let s = HealthStatus {
status: "ok".to_string(),
};
let json = serde_json::to_value(&s).unwrap();
assert_eq!(json["status"], "ok");
}
}
-4
View File
@@ -1,4 +0,0 @@
//! Health I/O wrappers.
//!
//! Health has no side effects; this file exists to satisfy the
//! service-module convention (`docs/architecture/service-modules.md`).
-39
View File
@@ -1,39 +0,0 @@
//! Health service — public API for the health domain.
//!
//! Exposes a single `check()` function that returns a [`HealthStatus`].
//! HTTP handlers call this instead of constructing the response inline.
//!
//! Conventions: `docs/architecture/service-modules.md`
pub mod check;
pub(super) mod io;
pub use check::HealthStatus;
// ── Error type ────────────────────────────────────────────────────────────────
/// Typed errors returned by `service::health` functions.
///
/// Health checks are currently infallible; this enum satisfies the module
/// convention and accommodates future error cases (e.g. dependency checks).
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error {
/// An internal error occurred during the health check.
Internal(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Internal(msg) => write!(f, "Health error: {msg}"),
}
}
}
// ── Public API ────────────────────────────────────────────────────────────────
/// Perform a health check and return the status.
pub fn check() -> HealthStatus {
check::ok()
}
-1
View File
@@ -14,7 +14,6 @@ pub mod events;
pub mod file_io;
pub mod gateway;
pub mod git_ops;
pub mod health;
pub mod merge;
pub mod notifications;
pub mod oauth;