diff --git a/server/src/gateway.rs b/server/src/gateway.rs index 18550947..f0051033 100644 --- a/server/src/gateway.rs +++ b/server/src/gateway.rs @@ -1634,50 +1634,12 @@ pub async fn gateway_bot_config_page_handler() -> Response { // ── Gateway server startup ─────────────────────────────────────────── -/// Start the gateway HTTP server. This is the entry point when `--gateway` is used. -pub async fn run(config_path: &Path, port: u16) -> Result<(), std::io::Error> { - // Locate the gateway config directory (parent of `projects.toml`). - let config_dir = config_path - .parent() - .unwrap_or(std::path::Path::new(".")) - .to_path_buf(); - - let config = GatewayConfig::load(config_path).map_err(std::io::Error::other)?; - let state = - GatewayState::new(config, config_dir.clone(), port).map_err(std::io::Error::other)?; - let state_arc = Arc::new(state); - - let active = state_arc.active_project.read().await.clone(); - crate::slog!("[gateway] Starting gateway on port {port}, active project: {active}"); - crate::slog!( - "[gateway] Registered projects: {}", - state_arc - .projects - .read() - .await - .keys() - .cloned() - .collect::>() - .join(", ") - ); - - // Write `.mcp.json` so that the gateway's Matrix bot's Claude Code CLI - // connects to this gateway's MCP endpoint (which proxies to the active project). - if let Err(e) = write_gateway_mcp_json(&config_dir, port) { - crate::slog!("[gateway] Warning: could not write .mcp.json: {e}"); - } - - // Spawn the Matrix bot if `.huskies/bot.toml` exists in the config directory. - let gateway_projects: Vec = state_arc.projects.read().await.keys().cloned().collect(); - let bot_abort = spawn_gateway_bot( - &config_dir, - Arc::clone(&state_arc.active_project), - gateway_projects, - port, - ); - *state_arc.bot_handle.lock().await = bot_abort; - - let route = poem::Route::new() +/// Build the complete gateway route tree. +/// +/// Extracted from `run` so that tests can construct the full route tree and +/// catch duplicate-route panics before they reach production. +pub fn build_gateway_route(state_arc: Arc) -> impl poem::Endpoint { + poem::Route::new() .at("/bot-config", poem::get(gateway_bot_config_page_handler)) .at("/api/gateway", poem::get(gateway_api_handler)) .at("/api/gateway/switch", poem::post(gateway_switch_handler)) @@ -1732,7 +1694,53 @@ pub async fn run(config_path: &Path, port: u16) -> Result<(), std::io::Error> { ) .at("/*path", poem::get(crate::http::assets::embedded_file)) .at("/", poem::get(crate::http::assets::embedded_index)) - .data(state_arc); + .data(state_arc) +} + +/// Start the gateway HTTP server. This is the entry point when `--gateway` is used. +pub async fn run(config_path: &Path, port: u16) -> Result<(), std::io::Error> { + // Locate the gateway config directory (parent of `projects.toml`). + let config_dir = config_path + .parent() + .unwrap_or(std::path::Path::new(".")) + .to_path_buf(); + + let config = GatewayConfig::load(config_path).map_err(std::io::Error::other)?; + let state = + GatewayState::new(config, config_dir.clone(), port).map_err(std::io::Error::other)?; + let state_arc = Arc::new(state); + + let active = state_arc.active_project.read().await.clone(); + crate::slog!("[gateway] Starting gateway on port {port}, active project: {active}"); + crate::slog!( + "[gateway] Registered projects: {}", + state_arc + .projects + .read() + .await + .keys() + .cloned() + .collect::>() + .join(", ") + ); + + // Write `.mcp.json` so that the gateway's Matrix bot's Claude Code CLI + // connects to this gateway's MCP endpoint (which proxies to the active project). + if let Err(e) = write_gateway_mcp_json(&config_dir, port) { + crate::slog!("[gateway] Warning: could not write .mcp.json: {e}"); + } + + // Spawn the Matrix bot if `.huskies/bot.toml` exists in the config directory. + let gateway_projects: Vec = state_arc.projects.read().await.keys().cloned().collect(); + let bot_abort = spawn_gateway_bot( + &config_dir, + Arc::clone(&state_arc.active_project), + gateway_projects, + port, + ); + *state_arc.bot_handle.lock().await = bot_abort; + + let route = build_gateway_route(state_arc); let host = std::env::var("HUSKIES_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); let addr = format!("{host}:{port}"); @@ -2260,4 +2268,16 @@ enabled = false .await; assert_eq!(resp.0.status(), StatusCode::NOT_FOUND); } + + /// Build the full gateway route tree and verify it does not panic. + /// + /// Poem panics at construction time when duplicate routes are registered. + /// This test catches any regression where a duplicate route is re-introduced + /// (e.g. the `/` vs `/*path` duplicate fixed in commit 0969fb5d). + #[test] + fn gateway_route_tree_builds_without_panic() { + let state = make_test_state(); + // build_gateway_route will panic if any route is registered more than once. + let _route = build_gateway_route(state); + } }