Fix slow unit tests (>5s) by removing unnecessary production delays

- doctor_no_color test: add --dry-run flag to skip live network probes (11s -> <1s)
- retry tests: set retry_policy=linear for 500ms delay instead of 5s default backoff
- cli_backend tests: make poll_interval configurable on AgentCliBackend, use 10ms in tests
- doctor TOML fixtures: remove status.code=1 now that dry-run doctor exits 0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-13 17:59:43 -04:00
parent 1b3d3d900b
commit cb21dc8096
6 changed files with 49 additions and 20 deletions

View file

@ -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()

View file

@ -1,6 +1,5 @@
bin.name = "fabro"
args = ["doctor", "--dry-run"]
status.code = 1
[env]
inherit = false

View file

@ -1,6 +1,5 @@
bin.name = "fabro"
args = ["doctor"]
status.code = 1
[env]
inherit = false

View file

@ -1,6 +1,5 @@
bin.name = "fabro"
args = ["doctor", "-v"]
status.code = 1
[env]
inherit = false

View file

@ -364,6 +364,7 @@ pub struct AgentCliBackend {
model: String,
provider: Provider,
env: HashMap<String, String>,
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<dyn Sandbox>) -> Vec<String> {
// 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

View file

@ -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<dyn fabro_agent::Sandbox> = 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<dyn fabro_agent::Sandbox> =
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<dyn fabro_agent::Sandbox> = 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<dyn fabro_agent::Sandbox> = 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<dyn fabro_agent::Sandbox> = 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<dyn fabro_agent::Sandbox> = 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<dyn fabro_agent::Sandbox> = 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<dyn fabro_agent::Sandbox> = 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<dyn fabro_agent::Sandbox> = 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<dyn fabro_agent::Sandbox> = 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",