systemg 0.57.1

An agent-friendly general process composer.
Documentation
//! Integration tests covering PID/state tracking after supervisor restarts

#[path = "common/mod.rs"]
mod common;

use std::{
    fs,
    sync::{Arc, Mutex},
};

use assert_cmd::cargo::cargo_bin_cmd;
use common::{HomeEnvGuard, is_process_alive, wait_for_pid};
use systemg::{
    config::Config,
    daemon::{Daemon, PidFile, ServiceLifecycleStatus, ServiceStateFile},
    diag::SgCode,
    state_store::StateStore,
};
use tempfile::tempdir;

/// Builds an in-process daemon backed by the configured project's state store.
fn build_daemon(config: Config) -> Daemon {
    let store = StateStore::for_project(&config.project.id);
    let pid_file = Arc::new(Mutex::new(PidFile::load(store.clone()).unwrap_or_default()));
    let state_file = Arc::new(Mutex::new(
        ServiceStateFile::load(store).unwrap_or_default(),
    ));
    Daemon::new(config, pid_file, state_file, false)
}

#[test]
/// Verifies live PID data repairs a stale recorded exit during status rendering.
fn status_recovers_from_stale_exit_state() {
    let temp = tempdir().expect("failed to create temp dir");
    let home = temp.path().join("home");
    fs::create_dir_all(&home).expect("failed to create home dir");
    let _home_guard = HomeEnvGuard::set(&home);

    let config_path = home.join("sysg.yaml");
    let config_contents = r#"
version: "2"
services:
  steady:
    command: "sleep 30"
    restart_policy: always
    backoff: 1s
"#;
    fs::write(&config_path, config_contents).expect("write config yaml");

    let config = systemg::config::load_config(Some(config_path.to_str().unwrap()))
        .expect("load config");
    let daemon = build_daemon(config);
    daemon.start_services().expect("failed to start services");

    let pid = wait_for_pid("steady");
    assert!(is_process_alive(pid));

    let config_arc = daemon.config();
    let service_hash = config_arc.state_key("steady");
    let mut state_file = ServiceStateFile::load(daemon.store()).expect("load state file");
    state_file
        .set(
            &service_hash,
            ServiceLifecycleStatus::ExitedWithError,
            None,
            Some(1),
            None,
        )
        .expect("write stale state");

    let output = cargo_bin_cmd!("sysg")
        .arg("status")
        .arg("-c")
        .arg(config_path.to_str().unwrap())
        .arg("-s")
        .arg("steady")
        .env("HOME", &home)
        .output()
        .expect("run sysg status");

    assert!(
        !output.status.success(),
        "offline status should report an unsupervised process"
    );
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains(SgCode::SupervisorOffline.as_str()),
        "offline status should report SG0206: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.contains("Running"),
        "Expected status output to treat the alive process as running, got: {stdout}"
    );

    let refreshed_state =
        ServiceStateFile::load(daemon.store()).expect("reload state file");
    let entry = refreshed_state
        .get(&service_hash)
        .expect("state entry present after status call");
    assert_eq!(entry.status, ServiceLifecycleStatus::Running);
    assert_eq!(entry.pid, Some(pid));

    daemon.stop_services().ok();
    daemon.shutdown_monitor();
}