folk-plugin-process 0.2.0

Auxiliary process supervisor plugin for Folk — starts, monitors, and restarts sidecar processes
Documentation
use std::sync::Arc;
use std::time::Duration;

use folk_plugin_process::config::{ProcessDef, RestartPolicy};
use folk_plugin_process::supervisor::{ProcessStatus, ProcessSupervisor};
use tokio::sync::watch;

#[tokio::test]
async fn supervisor_restarts_on_failure() {
    // "false" exits immediately with code 1 -> should restart
    let def = ProcessDef {
        name: "test".into(),
        command: "false".into(),
        restart: RestartPolicy::OnFailure,
        max_restarts: 2,
        restart_delay: Duration::from_millis(10),
    };

    let (_sd_tx, sd_rx) = watch::channel(false);
    let sup = Arc::new(ProcessSupervisor::new(def));
    let sup_clone = sup.clone();

    tokio::spawn(async move {
        sup_clone.run(sd_rx).await;
    });
    tokio::time::sleep(Duration::from_millis(200)).await;

    // After max_restarts, should be Failed
    let status = sup.status().await;
    assert!(
        matches!(status, ProcessStatus::Failed { .. }),
        "expected Failed, got {:?}",
        status
    );
}

#[tokio::test]
async fn supervisor_stops_on_shutdown() {
    let def = ProcessDef {
        name: "sleeper".into(),
        command: "sleep 60".into(),
        restart: RestartPolicy::Always,
        max_restarts: 5,
        restart_delay: Duration::from_millis(10),
    };

    let (sd_tx, sd_rx) = watch::channel(false);
    let sup = Arc::new(ProcessSupervisor::new(def));
    let sup_clone = sup.clone();

    let handle = tokio::spawn(async move {
        sup_clone.run(sd_rx).await;
    });

    // Give it time to start
    tokio::time::sleep(Duration::from_millis(100)).await;

    // Signal shutdown
    sd_tx.send(true).unwrap();
    let _ = tokio::time::timeout(Duration::from_secs(10), handle).await;

    let status = sup.status().await;
    assert_eq!(status, ProcessStatus::Stopped);
}

#[tokio::test]
async fn supervisor_never_restart_exits_cleanly() {
    let def = ProcessDef {
        name: "once".into(),
        command: "true".into(),
        restart: RestartPolicy::Never,
        max_restarts: 5,
        restart_delay: Duration::from_millis(10),
    };

    let (_sd_tx, sd_rx) = watch::channel(false);
    let sup = Arc::new(ProcessSupervisor::new(def));
    let sup_clone = sup.clone();

    let handle = tokio::spawn(async move {
        sup_clone.run(sd_rx).await;
    });

    let _ = tokio::time::timeout(Duration::from_secs(5), handle).await;

    let status = sup.status().await;
    // "true" exits with 0 + Never policy = should not restart, ends as Failed
    assert!(matches!(status, ProcessStatus::Failed { .. }));
}