hive-agent: warn once on a model-list mismatch; test the ACP set-model/effort branch
filter_offered_models logged its "no match" warning on every pickers() call; pickers() runs on every /api/state poll (every 4s per open dashboard tab) and on every post_set_model/post_set_effort call. Guard it with a process-lifetime AtomicBool, threaded in explicitly so tests can use their own instead of racing each other on a shared static. Extract post_set_model/post_set_effort's ACP accept/refuse decision into a pure accept_or_refuse() helper and add tests for it: an offered value is accepted, an unoffered one is refused with 400, and an empty offered list (before the first turn, or effort while a model switch is pending) refuses with 400 too. Not fixed here: a session refusing session/set_config_option leaves the picker showing the refused value as pending indefinitely. Fixing it needs hive-runtime to expose a real refused/pending distinction that Choice and SessionChoices don't carry today, threaded through to hive-agent's model_pending computation, plus a fake-agent test fixture that can simulate an RPC refusal — a cross-crate change, not a small one.
This commit is contained in:
parent
8a8da5ec8a
commit
9de5ba4279
2 changed files with 97 additions and 15 deletions
|
|
@ -148,8 +148,8 @@ pub(super) async fn post_set_model(
|
|||
}
|
||||
let text = if state.bus.session_choices().is_some() {
|
||||
let offered = super::state::pickers(&state.bus).available_models;
|
||||
if !offered.iter().any(|m| m == name) {
|
||||
return not_offered("model", &offered);
|
||||
if let Some(response) = accept_or_refuse("model", name, &offered) {
|
||||
return response;
|
||||
}
|
||||
format!("operator: /model — model set to '{name}' from the next turn")
|
||||
} else {
|
||||
|
|
@ -180,8 +180,8 @@ pub(super) async fn post_set_effort(
|
|||
let level = form.effort.trim();
|
||||
let text = if state.bus.session_choices().is_some() {
|
||||
let offered = super::state::pickers(&state.bus).available_efforts;
|
||||
if !offered.iter().any(|e| e == level) {
|
||||
return not_offered("effort", &offered);
|
||||
if let Some(response) = accept_or_refuse("effort", level, &offered) {
|
||||
return response;
|
||||
}
|
||||
format!("operator: /effort — effort set to '{level}' from the next turn")
|
||||
} else if crate::harness_state::is_valid_effort(level) {
|
||||
|
|
@ -201,6 +201,18 @@ pub(super) async fn post_set_effort(
|
|||
(axum::http::StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
||||
/// The ACP branch of `post_set_model`/`post_set_effort`: `None` when `name`
|
||||
/// is one `offered` (the session's current pickers list) names, else the
|
||||
/// [`not_offered`] refusal to return. Before a session has attached,
|
||||
/// `offered` is empty and every name is refused.
|
||||
fn accept_or_refuse(what: &str, name: &str, offered: &[String]) -> Option<Response> {
|
||||
if offered.iter().any(|o| o == name) {
|
||||
None
|
||||
} else {
|
||||
Some(not_offered(what, offered))
|
||||
}
|
||||
}
|
||||
|
||||
/// Refuses a pick the ACP session does not offer.
|
||||
fn not_offered(what: &str, offered: &[String]) -> Response {
|
||||
let message = if offered.is_empty() {
|
||||
|
|
@ -253,3 +265,31 @@ pub(super) async fn post_mark_todos_done(Form(form): Form<MarkTodosDoneForm>) ->
|
|||
struct MarkTodosDoneBody {
|
||||
acked: u64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::accept_or_refuse;
|
||||
|
||||
#[test]
|
||||
fn an_offered_value_is_accepted() {
|
||||
let offered = vec!["m/think".to_owned(), "m/plain".to_owned()];
|
||||
assert!(accept_or_refuse("model", "m/plain", &offered).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_value_the_session_does_not_offer_is_refused_with_400() {
|
||||
let offered = vec!["m/think".to_owned()];
|
||||
let response = accept_or_refuse("model", "m/plain", &offered).unwrap();
|
||||
assert_eq!(response.status(), axum::http::StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_offered_yet_refuses_with_400() {
|
||||
// Before a session has attached (or on the effort picker while a
|
||||
// model switch is pending), `pickers()` returns an empty list —
|
||||
// every name is refused, not just ones the session once offered
|
||||
// and later dropped.
|
||||
let response = accept_or_refuse("effort", "high", &[]).unwrap();
|
||||
assert_eq!(response.status(), axum::http::StatusCode::BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue