ralph-agent-loop 0.4.0

A Rust CLI for managing AI agent loops with a structured JSON task queue
Documentation
use super::reset_dispatcher_for_tests;
use super::state::dispatcher_for_config_with_spawner;
use super::types::ThreadSpawner;
use crate::contracts::WebhookConfig;
use serial_test::serial;
use std::io;
use std::sync::atomic::{AtomicUsize, Ordering};

#[derive(Debug)]
struct FailingThreadSpawner;

impl ThreadSpawner for FailingThreadSpawner {
    fn spawn(&self, _name: String, _task: Box<dyn FnOnce() + Send + 'static>) -> io::Result<()> {
        Err(io::Error::new(
            io::ErrorKind::WouldBlock,
            "simulated thread exhaustion",
        ))
    }
}

#[derive(Debug, Default)]
struct SilentThreadSpawner;

impl ThreadSpawner for SilentThreadSpawner {
    fn spawn(&self, _name: String, _task: Box<dyn FnOnce() + Send + 'static>) -> io::Result<()> {
        Ok(())
    }
}

#[derive(Debug, Default)]
struct CountingSpawner {
    calls: AtomicUsize,
}

impl ThreadSpawner for CountingSpawner {
    fn spawn(&self, _name: String, _task: Box<dyn FnOnce() + Send + 'static>) -> io::Result<()> {
        self.calls.fetch_add(1, Ordering::SeqCst);
        Err(io::Error::other(
            "dispatcher should stay disabled after startup failure",
        ))
    }
}

#[test]
#[serial]
fn thread_spawn_failure_disables_webhooks_for_run_without_panic() {
    reset_dispatcher_for_tests();
    let config = WebhookConfig::default();

    let dispatcher = dispatcher_for_config_with_spawner(&config, &FailingThreadSpawner);
    assert!(dispatcher.is_none());

    let counting_spawner = CountingSpawner::default();
    let retry = dispatcher_for_config_with_spawner(&config, &counting_spawner);
    assert!(retry.is_none());
    assert_eq!(counting_spawner.calls.load(Ordering::SeqCst), 0);

    reset_dispatcher_for_tests();
}

#[test]
#[serial]
fn startup_handshake_timeout_disables_webhooks_without_panic() {
    reset_dispatcher_for_tests();
    let config = WebhookConfig::default();

    let dispatcher = dispatcher_for_config_with_spawner(&config, &SilentThreadSpawner);
    assert!(dispatcher.is_none());

    reset_dispatcher_for_tests();
}