mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-06 08:18:58 +00:00
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>
92 lines
2.7 KiB
Rust
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());
|
|
}
|
|
}
|