mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 15:40:18 +00:00
fix(web): recompute cron next_fire_at when re-enabling routines (#1080)
This commit is contained in:
@@ -10,6 +10,7 @@ use axum::{
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::agent::routine::{Trigger, next_cron_fire};
|
||||
use crate::channels::web::server::GatewayState;
|
||||
use crate::channels::web::types::*;
|
||||
use crate::error::RoutineError;
|
||||
@@ -182,12 +183,21 @@ pub async fn routines_toggle_handler(
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Routine not found".to_string()))?;
|
||||
|
||||
let was_enabled = routine.enabled;
|
||||
// If a specific value was provided, use it; otherwise toggle.
|
||||
routine.enabled = match body {
|
||||
Some(Json(req)) => req.enabled.unwrap_or(!routine.enabled),
|
||||
None => !routine.enabled,
|
||||
};
|
||||
|
||||
if routine.enabled
|
||||
&& !was_enabled
|
||||
&& let Trigger::Cron { schedule, timezone } = &routine.trigger
|
||||
{
|
||||
routine.next_fire_at = next_cron_fire(schedule, timezone.as_deref())
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
}
|
||||
|
||||
store
|
||||
.update_routine(&routine)
|
||||
.await
|
||||
|
||||
@@ -26,6 +26,7 @@ use tower_http::set_header::SetResponseHeaderLayer;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::agent::SessionManager;
|
||||
use crate::agent::routine::{Trigger, next_cron_fire};
|
||||
use crate::bootstrap::ironclaw_base_dir;
|
||||
use crate::channels::IncomingMessage;
|
||||
use crate::channels::relay::DEFAULT_RELAY_NAME;
|
||||
@@ -2416,12 +2417,21 @@ async fn routines_toggle_handler(
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Routine not found".to_string()))?;
|
||||
|
||||
let was_enabled = routine.enabled;
|
||||
// If a specific value was provided, use it; otherwise toggle.
|
||||
routine.enabled = match body {
|
||||
Some(Json(req)) => req.enabled.unwrap_or(!routine.enabled),
|
||||
None => !routine.enabled,
|
||||
};
|
||||
|
||||
if routine.enabled
|
||||
&& !was_enabled
|
||||
&& let Trigger::Cron { schedule, timezone } = &routine.trigger
|
||||
{
|
||||
routine.next_fire_at = next_cron_fire(schedule, timezone.as_deref())
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
}
|
||||
|
||||
store
|
||||
.update_routine(&routine)
|
||||
.await
|
||||
|
||||
@@ -13,6 +13,8 @@ mod support;
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::support::gateway_workflow_harness::GatewayWorkflowHarness;
|
||||
use crate::support::mock_openai_server::{
|
||||
MockOpenAiResponse, MockOpenAiRule, MockOpenAiServerBuilder, MockToolCall,
|
||||
@@ -147,4 +149,115 @@ mod tests {
|
||||
harness.shutdown().await;
|
||||
mock.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn routines_toggle_reenable_cron_recomputes_next_fire_at() {
|
||||
let mock = MockOpenAiServerBuilder::new()
|
||||
.with_rule(MockOpenAiRule::on_user_contains(
|
||||
"create cron routine",
|
||||
MockOpenAiResponse::ToolCalls(vec![MockToolCall::new(
|
||||
"call_create_cron_1",
|
||||
"routine_create",
|
||||
serde_json::json!({
|
||||
"name": "wf-cron-toggle-reenable",
|
||||
"description": "Cron toggle regression test",
|
||||
"trigger_type": "cron",
|
||||
"schedule": "0 */5 * * * *",
|
||||
"timezone": "UTC",
|
||||
"action_type": "lightweight",
|
||||
"prompt": "noop"
|
||||
}),
|
||||
)]),
|
||||
))
|
||||
.with_default_response(MockOpenAiResponse::Text("ack".to_string()))
|
||||
.start()
|
||||
.await;
|
||||
|
||||
let harness =
|
||||
GatewayWorkflowHarness::start_openai_compatible(&mock.openai_base_url(), "mock-model")
|
||||
.await;
|
||||
|
||||
let thread_id = harness.create_thread().await;
|
||||
harness.send_chat(&thread_id, "create cron routine").await;
|
||||
harness
|
||||
.wait_for_turns(&thread_id, 1, Duration::from_secs(10))
|
||||
.await;
|
||||
|
||||
let routine = harness
|
||||
.routine_by_name("wf-cron-toggle-reenable")
|
||||
.await
|
||||
.expect("routine should exist");
|
||||
let routine_id = routine
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.expect("routine id missing");
|
||||
|
||||
let routine_uuid = Uuid::parse_str(routine_id).expect("valid routine uuid");
|
||||
|
||||
// Disable through the web toggle endpoint.
|
||||
harness
|
||||
.client
|
||||
.post(format!(
|
||||
"{}/api/routines/{routine_id}/toggle",
|
||||
harness.base_url()
|
||||
))
|
||||
.bearer_auth(&harness.auth_token)
|
||||
.json(&serde_json::json!({ "enabled": false }))
|
||||
.send()
|
||||
.await
|
||||
.expect("disable toggle request failed")
|
||||
.error_for_status()
|
||||
.expect("disable toggle non-2xx");
|
||||
|
||||
// Simulate an unscheduled disabled cron routine (next_fire_at missing).
|
||||
let mut stored = harness
|
||||
.db
|
||||
.get_routine(routine_uuid)
|
||||
.await
|
||||
.expect("db get_routine")
|
||||
.expect("routine should still exist");
|
||||
stored.next_fire_at = None;
|
||||
harness
|
||||
.db
|
||||
.update_routine(&stored)
|
||||
.await
|
||||
.expect("db update_routine");
|
||||
|
||||
// Re-enable through the web toggle endpoint.
|
||||
harness
|
||||
.client
|
||||
.post(format!(
|
||||
"{}/api/routines/{routine_id}/toggle",
|
||||
harness.base_url()
|
||||
))
|
||||
.bearer_auth(&harness.auth_token)
|
||||
.json(&serde_json::json!({ "enabled": true }))
|
||||
.send()
|
||||
.await
|
||||
.expect("enable toggle request failed")
|
||||
.error_for_status()
|
||||
.expect("enable toggle non-2xx");
|
||||
|
||||
let detail = harness
|
||||
.client
|
||||
.get(format!("{}/api/routines/{routine_id}", harness.base_url()))
|
||||
.bearer_auth(&harness.auth_token)
|
||||
.send()
|
||||
.await
|
||||
.expect("detail request failed")
|
||||
.error_for_status()
|
||||
.expect("detail non-2xx")
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.expect("invalid detail response");
|
||||
|
||||
assert_eq!(detail["enabled"].as_bool(), Some(true));
|
||||
assert!(
|
||||
detail["next_fire_at"].as_str().is_some(),
|
||||
"expected next_fire_at to be recomputed when re-enabling cron routine, got {detail}"
|
||||
);
|
||||
|
||||
harness.shutdown().await;
|
||||
mock.shutdown().await;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user