diff --git a/lib/crates/fabro-cli/tests/cli.rs b/lib/crates/fabro-cli/tests/cli.rs index 568c7f029..17a38df55 100644 --- a/lib/crates/fabro-cli/tests/cli.rs +++ b/lib/crates/fabro-cli/tests/cli.rs @@ -352,7 +352,7 @@ fn serve_help() { #[test] fn doctor_no_color_when_no_color_set() { arc() - .args(["doctor"]) + .args(["doctor", "--dry-run"]) .env_clear() .env("NO_COLOR", "1") .assert() diff --git a/lib/crates/fabro-cli/tests/cmd/doctor/dry-run-flag.toml b/lib/crates/fabro-cli/tests/cmd/doctor/dry-run-flag.toml index edff2ec3d..9d644266a 100644 --- a/lib/crates/fabro-cli/tests/cmd/doctor/dry-run-flag.toml +++ b/lib/crates/fabro-cli/tests/cmd/doctor/dry-run-flag.toml @@ -1,6 +1,5 @@ bin.name = "fabro" args = ["doctor", "--dry-run"] -status.code = 1 [env] inherit = false diff --git a/lib/crates/fabro-cli/tests/cmd/doctor/header.toml b/lib/crates/fabro-cli/tests/cmd/doctor/header.toml index bcb8838c1..933a2987d 100644 --- a/lib/crates/fabro-cli/tests/cmd/doctor/header.toml +++ b/lib/crates/fabro-cli/tests/cmd/doctor/header.toml @@ -1,6 +1,5 @@ bin.name = "fabro" args = ["doctor"] -status.code = 1 [env] inherit = false diff --git a/lib/crates/fabro-cli/tests/cmd/doctor/verbose.toml b/lib/crates/fabro-cli/tests/cmd/doctor/verbose.toml index 53980937f..9612357a7 100644 --- a/lib/crates/fabro-cli/tests/cmd/doctor/verbose.toml +++ b/lib/crates/fabro-cli/tests/cmd/doctor/verbose.toml @@ -1,6 +1,5 @@ bin.name = "fabro" args = ["doctor", "-v"] -status.code = 1 [env] inherit = false diff --git a/lib/crates/fabro-workflows/src/cli/cli_backend.rs b/lib/crates/fabro-workflows/src/cli/cli_backend.rs index d3afa43b9..2c63f5f03 100644 --- a/lib/crates/fabro-workflows/src/cli/cli_backend.rs +++ b/lib/crates/fabro-workflows/src/cli/cli_backend.rs @@ -364,6 +364,7 @@ pub struct AgentCliBackend { model: String, provider: Provider, env: HashMap, + poll_interval: std::time::Duration, } impl AgentCliBackend { @@ -373,6 +374,7 @@ impl AgentCliBackend { model, provider, env: HashMap::new(), + poll_interval: std::time::Duration::from_secs(5), } } @@ -382,6 +384,12 @@ impl AgentCliBackend { self } + #[must_use] + pub fn with_poll_interval(mut self, interval: std::time::Duration) -> Self { + self.poll_interval = interval; + self + } + /// Detect changed files by comparing git state before and after the CLI run. async fn detect_changed_files(&self, sandbox: &Arc) -> Vec { // Get unstaged changes @@ -568,7 +576,7 @@ impl CodergenBackend for AgentCliBackend { // 3c. Poll for completion let poll_command = format!("[ -f {exit_code_path} ] && cat {exit_code_path} || echo running"); - let poll_interval = std::time::Duration::from_secs(5); + let poll_interval = self.poll_interval; let exit_code: i32 = loop { tokio::time::sleep(poll_interval).await; emitter.touch(); // keep the stall watchdog alive while polling diff --git a/lib/crates/fabro-workflows/tests/integration.rs b/lib/crates/fabro-workflows/tests/integration.rs index dc55fca51..1cf91dcec 100644 --- a/lib/crates/fabro-workflows/tests/integration.rs +++ b/lib/crates/fabro-workflows/tests/integration.rs @@ -1,6 +1,7 @@ use std::collections::VecDeque; use std::path::Path; use std::sync::Arc; +use std::time::Duration; use fabro_llm::provider::Provider; use fabro_workflows::checkpoint::Checkpoint; @@ -1002,6 +1003,10 @@ async fn retry_on_failure_then_succeed() { retry_node .attrs .insert("max_retries".to_string(), AttrValue::Integer(3)); + retry_node.attrs.insert( + "retry_policy".to_string(), + AttrValue::String("linear".to_string()), + ); graph.nodes.insert("work".to_string(), retry_node); graph.edges.push(Edge::new("start", "work")); @@ -2537,6 +2542,10 @@ async fn scenario_node_retries_on_retry_status() { flaky .attrs .insert("max_retries".to_string(), AttrValue::Integer(2)); + flaky.attrs.insert( + "retry_policy".to_string(), + AttrValue::String("linear".to_string()), + ); graph.nodes.insert("flaky".to_string(), flaky); graph.edges.push(Edge::new("start", "flaky")); graph.edges.push(Edge::new("flaky", "exit")); @@ -9557,7 +9566,8 @@ async fn cli_backend_run_writes_prompt_and_calls_exec() { let claude_output = r#"{"type":"result","result":"I fixed the bug.","usage":{"input_tokens":500,"output_tokens":200}}"#; let test_env = Arc::new(CliTestEnv::new(claude_output)); let env: Arc = test_env.clone(); - let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); + let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic) + .with_poll_interval(Duration::from_millis(10)); let node = Node::new("fix_code"); let context = Context::new(); @@ -9630,7 +9640,8 @@ async fn cli_backend_run_detects_changed_files() { let claude_output = r#"{"type":"result","result":"Created new file.","usage":{"input_tokens":100,"output_tokens":50}}"#; let env: Arc = Arc::new(CliTestEnv::new(claude_output).with_git_diff_after("src/main.rs\nsrc/lib.rs\n")); - let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); + let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic) + .with_poll_interval(Duration::from_millis(10)); let node = Node::new("implement"); let context = Context::new(); @@ -9664,7 +9675,8 @@ async fn cli_backend_run_with_codex_provider() { let codex_output = "{\"type\":\"item.completed\",\"item\":{\"id\":\"item_0\",\"type\":\"agent_message\",\"text\":\"Implemented the feature.\"}}\n{\"type\":\"turn.completed\",\"usage\":{\"input_tokens\":300,\"output_tokens\":150}}"; let test_env = Arc::new(CliTestEnv::new(codex_output)); let env: Arc = test_env.clone(); - let backend = AgentCliBackend::new("gpt-5.3-codex".into(), Provider::OpenAi); + let backend = AgentCliBackend::new("gpt-5.3-codex".into(), Provider::OpenAi) + .with_poll_interval(Duration::from_millis(10)); let node = Node::new("implement"); let context = Context::new(); @@ -9830,7 +9842,8 @@ async fn cli_backend_run_fails_on_nonzero_exit() { } let failing_env: Arc = Arc::new(FailingCliEnv); - let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); + let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic) + .with_poll_interval(Duration::from_millis(10)); let node = Node::new("step"); let context = Context::new(); let emitter = Arc::new(EventEmitter::new()); @@ -9869,7 +9882,8 @@ async fn cli_backend_run_fails_on_nonzero_exit() { #[tokio::test] async fn cli_backend_run_fails_on_unparseable_output() { let env: Arc = Arc::new(CliTestEnv::new("this is not json at all")); - let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); + let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic) + .with_poll_interval(Duration::from_millis(10)); let node = Node::new("step"); let context = Context::new(); @@ -9906,7 +9920,8 @@ async fn cli_backend_run_uses_node_model_override() { r#"{"type":"result","result":"ok","usage":{"input_tokens":10,"output_tokens":5}}"#; let test_env = Arc::new(CliTestEnv::new(claude_output)); let env: Arc = test_env.clone(); - let backend = AgentCliBackend::new("default-model".into(), Provider::Anthropic); + let backend = AgentCliBackend::new("default-model".into(), Provider::Anthropic) + .with_poll_interval(Duration::from_millis(10)); let mut node = Node::new("step"); node.attrs.insert( @@ -9952,7 +9967,8 @@ async fn cli_backend_run_uses_node_provider_override() { let codex_output = "{\"type\":\"item.completed\",\"item\":{\"id\":\"item_0\",\"type\":\"agent_message\",\"text\":\"ok\"}}\n{\"type\":\"turn.completed\",\"usage\":{\"input_tokens\":10,\"output_tokens\":5}}"; let test_env = Arc::new(CliTestEnv::new(codex_output)); let env: Arc = test_env.clone(); - let backend = AgentCliBackend::new("default-model".into(), Provider::Anthropic); + let backend = AgentCliBackend::new("default-model".into(), Provider::Anthropic) + .with_poll_interval(Duration::from_millis(10)); let mut node = Node::new("step"); node.attrs.insert( @@ -9995,7 +10011,8 @@ async fn cli_backend_run_writes_provider_used_json() { let claude_output = r#"{"type":"result","result":"done","usage":{"input_tokens":10,"output_tokens":5}}"#; let env: Arc = Arc::new(CliTestEnv::new(claude_output)); - let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); + let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic) + .with_poll_interval(Duration::from_millis(10)); let node = Node::new("step"); let context = Context::new(); @@ -10037,7 +10054,8 @@ async fn backend_router_delegates_to_cli_for_cli_node() { let env: Arc = Arc::new(CliTestEnv::new(claude_output)); let api_backend = Box::new(MockCodergenBackend); // would return "Response for ..." - let cli = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); + let cli = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic) + .with_poll_interval(Duration::from_millis(10)); let router = BackendRouter::new(api_backend, cli); let mut node = Node::new("cli_step"); @@ -10082,7 +10100,8 @@ async fn backend_router_delegates_to_api_for_normal_node() { let env = local_env(); let api_backend = Box::new(MockCodergenBackend); - let cli = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); + let cli = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic) + .with_poll_interval(Duration::from_millis(10)); let router = BackendRouter::new(api_backend, cli); let mut node = Node::new("api_step"); @@ -10126,7 +10145,8 @@ async fn backend_router_delegates_to_cli_for_backend_attr() { let env: Arc = Arc::new(CliTestEnv::new(codex_output)); let api_backend = Box::new(MockCodergenBackend); - let cli = AgentCliBackend::new("gpt-5.3-codex".into(), Provider::OpenAi); + let cli = AgentCliBackend::new("gpt-5.3-codex".into(), Provider::OpenAi) + .with_poll_interval(Duration::from_millis(10)); let router = BackendRouter::new(api_backend, cli); let mut node = Node::new("codex_step"); @@ -10220,7 +10240,8 @@ async fn full_pipeline_with_cli_backend_node() { // Build engine with BackendRouter let api = MockCodergenBackend; - let cli = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); + let cli = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic) + .with_poll_interval(Duration::from_millis(10)); let router = BackendRouter::new(Box::new(api), cli); let codergen_handler = AgentHandler::new(Some(Box::new(router))); @@ -10232,7 +10253,8 @@ async fn full_pipeline_with_cli_backend_node() { Box::new(AgentHandler::new(Some(Box::new({ // Second BackendRouter for the "agent" handler let api2 = MockCodergenBackend; - let cli2 = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); + let cli2 = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic) + .with_poll_interval(Duration::from_millis(10)); BackendRouter::new(Box::new(api2), cli2) })))), ); @@ -10358,14 +10380,16 @@ async fn stylesheet_backend_property_routes_to_cli() { // Run the pipeline let api = MockCodergenBackend; - let cli = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); + let cli = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic) + .with_poll_interval(Duration::from_millis(10)); let router = BackendRouter::new(Box::new(api), cli); let mut registry = HandlerRegistry::new(Box::new(AgentHandler::new(Some(Box::new(router))))); registry.register("start", Box::new(StartHandler)); registry.register("exit", Box::new(ExitHandler)); let api2 = MockCodergenBackend; - let cli2 = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); + let cli2 = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic) + .with_poll_interval(Duration::from_millis(10)); let router2 = BackendRouter::new(Box::new(api2), cli2); registry.register( "agent",