moadim 3.2.8

Loop engine for AI agents — routines over REST, MCP, and a built-in web UI
#![allow(
    clippy::missing_docs_in_private_items,
    reason = "test helpers and fixtures do not need doc comments"
)]

use super::*;
use crate::routines::{new_store, slugify, Routine};

fn make_routine(id: &str, title: &str, agent: &str) -> Routine {
    Routine {
        model: None,
        id: id.to_string(),
        schedule: "30 9 * * 1-5".to_string(),
        schedules: vec![],
        title: title.to_string(),
        agent: agent.to_string(),
        prompt: "p".to_string(),
        goal: None,
        repositories: vec![],
        machines: vec![crate::machine::current_machine()],
        enabled: true,
        disabled_reason: None,
        source: "managed".to_string(),
        created_at: 0,
        updated_at: 0,
        last_manual_trigger_at: None,
        last_scheduled_trigger_at: None,
        snoozed_until: None,
        skip_runs: None,
        power_saving: false,
        power_saving_exempt: false,
        tags: vec![],
        ttl_secs: None,
        max_runtime_secs: None,
        env: std::collections::HashMap::new(),
        auto_disabled_reason: None,
        consecutive_failures: 0,
        failure_threshold: None,
        notifications: Default::default(),
    }
}

#[test]
fn build_block_reads_multiple_crons_from_schedule_sidecar_and_unions_redundant_entries() {
    let agent_name = "test-sync-agent-multi-cron-block";
    let title = "Multi Cron Sync Routine";
    let slug = slugify(title);
    std::fs::create_dir_all(crate::paths::agents_dir()).unwrap();
    let cfg = crate::paths::agent_toml_path(agent_name);
    std::fs::write(&cfg, "command = \"claude\"\nargs = []\n").unwrap();
    let routine_dir = crate::paths::routine_dir(&slug);
    std::fs::create_dir_all(&routine_dir).unwrap();
    std::fs::write(
        routine_dir.join("schedule.cron"),
        "# human-edited extra fires\n*/10 * * * *\n*/20 * * * *\n5 9 * * 1-5\n",
    )
    .unwrap();
    std::fs::write(routine_dir.join(".compailed.cron"), "stale compiled cron\n").unwrap();

    let store = new_store();
    store.lock().unwrap().insert(
        "multi-cron".into(),
        make_routine("multi-cron", title, agent_name),
    );

    let block = build_block(&store);

    assert_eq!(
        block.matches("# moadim-routine:multi-cron").count(),
        2,
        "{block}"
    );
    assert!(block.contains("*/10 * * * * "), "{block}");
    assert!(block.contains("5 9 * * 1-5 "), "{block}");
    assert!(
        !block.contains("*/20 * * * * "),
        "cron-union should remove redundant subset: {block}"
    );
    assert_eq!(
        std::fs::read_to_string(crate::paths::routine_compailed_cron_path(&slug)).unwrap(),
        "*/10 * * * *\n5 9 * * 1-5\n"
    );
    assert!(
        !routine_dir.join(".compailed.cron").exists(),
        "legacy hidden cron-union sidecar should be removed"
    );
    assert_eq!(
        std::fs::read_to_string(routine_dir.join("schedule.cron")).unwrap(),
        "# human-edited extra fires\n*/10 * * * *\n*/20 * * * *\n5 9 * * 1-5\n"
    );

    std::fs::remove_file(&cfg).unwrap();
    let _ = std::fs::remove_dir_all(routine_dir);
}

#[test]
fn build_block_skips_invalid_multi_cron_entries_and_falls_back_if_none_valid() {
    let agent_name = "test-sync-agent-invalid-multi-cron-block";
    std::fs::create_dir_all(crate::paths::agents_dir()).unwrap();
    let cfg = crate::paths::agent_toml_path(agent_name);
    std::fs::write(&cfg, "command = \"claude\"\nargs = []\n").unwrap();

    let valid_title = "Invalid Multi Cron Valid Fallback Routine";
    let valid_slug = slugify(valid_title);
    let valid_dir = crate::paths::routine_dir(&valid_slug);
    std::fs::create_dir_all(&valid_dir).unwrap();
    std::fs::write(valid_dir.join("schedule.cron"), "not a cron\n5 9 * * 1-5\n").unwrap();

    let invalid_title = "Invalid Multi Cron All Invalid Routine";
    let invalid_slug = slugify(invalid_title);
    let invalid_dir = crate::paths::routine_dir(&invalid_slug);
    std::fs::create_dir_all(&invalid_dir).unwrap();
    std::fs::write(
        invalid_dir.join("schedule.cron"),
        "not a cron\n99 99 99 99 99\n",
    )
    .unwrap();

    let mut fallback = make_routine("all-invalid", invalid_title, agent_name);
    fallback.schedule = "15 10 * * *".to_string();

    let store = new_store();
    store.lock().unwrap().insert(
        "some-valid".into(),
        make_routine("some-valid", valid_title, agent_name),
    );
    store.lock().unwrap().insert("all-invalid".into(), fallback);

    let block = build_block(&store);

    assert!(block.contains("5 9 * * 1-5 "), "{block}");
    assert!(block.contains("15 10 * * * "), "{block}");
    assert!(!block.contains("not a cron"), "{block}");
    assert!(!block.contains("99 99 99 99 99"), "{block}");
    assert_eq!(
        std::fs::read_to_string(crate::paths::routine_compailed_cron_path(&valid_slug)).unwrap(),
        "5 9 * * 1-5\n"
    );
    assert_eq!(
        std::fs::read_to_string(crate::paths::routine_compailed_cron_path(&invalid_slug)).unwrap(),
        "15 10 * * *\n"
    );

    std::fs::remove_file(&cfg).unwrap();
    let _ = std::fs::remove_dir_all(valid_dir);
    let _ = std::fs::remove_dir_all(invalid_dir);
}
include!("build_block_keeps_valid_schedules_when_cron_union_cannot_simplify_them_tests.rs");