fabro/crates/arc-workflows/src/handler/wait_timer.rs
Bryan Helmkamp 55d44f7488 Add lifecycle hook system replacing legacy tool_hooks
Introduce a configurable hook system that triggers user-defined actions
at workflow lifecycle points (RunStart, StageStart, StageComplete,
StageFailed, EdgeSelected, CheckpointSaved, etc). Hooks can block
execution, skip nodes, or override edge routing via JSON decisions.

- New `hook/` module: types, config, executor (command), runner
- Engine instrumented at 8 lifecycle points with HookRunner calls
- TOML config: `[[hooks]]` in server.toml and run config files
- Config cascade: server hooks + run hooks merge, name collisions
  resolved by run config winning
- Remove legacy tool_hooks.pre/post from codergen handler (breaking)
- 30 e2e integration tests covering all hook events and behaviors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 01:42:19 -05:00

92 lines
2.7 KiB
Rust

use std::path::Path;
use async_trait::async_trait;
use crate::context::Context;
use crate::error::ArcError;
use crate::graph::{AttrValue, Graph, Node};
use crate::outcome::Outcome;
use super::{EngineServices, Handler};
/// Sleeps for a configured duration before proceeding.
pub struct WaitTimerHandler;
#[async_trait]
impl Handler for WaitTimerHandler {
async fn execute(
&self,
node: &Node,
_context: &Context,
_graph: &Graph,
_logs_root: &Path,
_services: &EngineServices,
) -> Result<Outcome, ArcError> {
let duration = node
.attrs
.get("duration")
.and_then(AttrValue::as_duration)
.ok_or_else(|| {
ArcError::Validation(format!(
"wait.timer node {:?} is missing a valid `duration` attribute",
node.id
))
})?;
tokio::time::sleep(duration).await;
Ok(Outcome::success())
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::event::EventEmitter;
use crate::handler::HandlerRegistry;
fn make_services() -> EngineServices {
EngineServices {
registry: std::sync::Arc::new(HandlerRegistry::new(Box::new(
crate::handler::start::StartHandler,
))),
emitter: std::sync::Arc::new(EventEmitter::new()),
sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new(
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
)),
git_state: std::sync::RwLock::new(None),
hook_runner: None,
}
}
#[tokio::test]
async fn wait_timer_success_with_short_duration() {
let handler = WaitTimerHandler;
let mut node = Node::new("wait60");
node.attrs.insert(
"duration".to_string(),
AttrValue::Duration(Duration::from_millis(1)),
);
let context = Context::new();
let graph = Graph::new("test");
let logs_root = Path::new("/tmp/test");
let outcome = handler
.execute(&node, &context, &graph, logs_root, &make_services())
.await
.unwrap();
assert_eq!(outcome.status, crate::outcome::StageStatus::Success);
}
#[tokio::test]
async fn wait_timer_errors_without_duration() {
let handler = WaitTimerHandler;
let node = Node::new("wait_no_dur");
let context = Context::new();
let graph = Graph::new("test");
let logs_root = Path::new("/tmp/test");
let result = handler
.execute(&node, &context, &graph, logs_root, &make_services())
.await;
assert!(result.is_err());
}
}