From 0472ed3ab5fea89104700dbdbb542770c5d172dd Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 10 Oct 2026 21:27:23 +0200 Subject: [PATCH] hive-agent: test the mark-done handler's 500 through an injected dial post_mark_todos_done's body moves into mark_ids_done, which takes the per-id dial as a closure, so tests cover the 500 and 200 responses without reaching the live in-agent socket. --- hive-agent/src/web_ui/actions.rs | 64 ++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/hive-agent/src/web_ui/actions.rs b/hive-agent/src/web_ui/actions.rs index f3fc004b..21d2243f 100644 --- a/hive-agent/src/web_ui/actions.rs +++ b/hive-agent/src/web_ui/actions.rs @@ -8,6 +8,7 @@ use axum::{ response::{IntoResponse, Response}, }; use serde::{Deserialize, Serialize}; +use std::future::Future; use super::{AppState, SigintOutcome, error_response}; @@ -252,10 +253,23 @@ pub(super) async fn post_mark_todos_done(Form(form): Form) -> if ids.is_empty() { return error_response(StatusCode::BAD_REQUEST, "mark-done: no todo ids selected"); } + mark_ids_done(ids, |id| async move { + crate::todo_server::dial(&hive_agent_sock::Request::MarkTodoDone { id }).await + }) + .await +} + +/// The handler body behind `dial`, which answers one id's `MarkTodoDone` +/// (`None` when the store doesn't answer). Tests inject it so they never +/// reach the live in-agent socket. +async fn mark_ids_done(ids: Vec, dial: F) -> Response +where + F: Fn(i64) -> Fut, + Fut: Future>, +{ let mut answers = Vec::with_capacity(ids.len()); for id in ids { - answers - .push(crate::todo_server::dial(&hive_agent_sock::Request::MarkTodoDone { id }).await); + answers.push(dial(id).await); } match settle_mark_done(answers) { Ok(acked) => axum::Json(MarkTodosDoneBody { acked }).into_response(), @@ -303,7 +317,51 @@ struct MarkTodosDoneBody { #[cfg(test)] mod tests { - use super::{accept_or_refuse, settle_mark_done}; + use super::{accept_or_refuse, mark_ids_done, settle_mark_done}; + + async fn body_text(response: axum::response::Response) -> String { + let bytes = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + String::from_utf8(bytes.to_vec()).unwrap() + } + + #[tokio::test] + async fn mark_done_handler_answers_500_with_the_message_when_the_store_does_not_answer() { + let response = mark_ids_done(vec![1, 2], |_| async { None }).await; + assert_eq!( + response.status(), + axum::http::StatusCode::INTERNAL_SERVER_ERROR + ); + let body = body_text(response).await; + assert!(body.contains("2 of 2 failed"), "{body}"); + assert!(body.contains("did not answer"), "{body}"); + } + + #[tokio::test] + async fn mark_done_handler_answers_500_with_the_store_error() { + let response = mark_ids_done(vec![7], |_| async { + Some(hive_agent_sock::Response::Err { + message: "database is locked".to_owned(), + }) + }) + .await; + assert_eq!( + response.status(), + axum::http::StatusCode::INTERNAL_SERVER_ERROR + ); + assert!(body_text(response).await.contains("database is locked")); + } + + #[tokio::test] + async fn mark_done_handler_answers_200_with_the_acked_total() { + let response = mark_ids_done(vec![1, 2], |_| async { + Some(hive_agent_sock::Response::Acked { count: 1 }) + }) + .await; + assert_eq!(response.status(), axum::http::StatusCode::OK); + assert_eq!(body_text(response).await, r#"{"acked":2}"#); + } #[test] fn mark_done_with_no_answer_is_an_error() {