moadim 3.2.4

Loop engine for AI agents — routines over REST, MCP, and a built-in web UI

#[test]
fn svc_create_update_delete_lifecycle() {
    let store = new_store();
    let suffix = uuid::Uuid::new_v4().to_string();
    let created_title = format!("Cov Routine {suffix}");
    let renamed_title = format!("Renamed {suffix}");
    let created = svc_create(
        &store,
        CreateRoutineRequest {
        disabled_reason: None,
            model: None,
            schedule: "@daily".into(),
            schedules: vec![],
            title: created_title.clone(),
            agent: "claude".into(),
            prompt: "p".into(),
            goal: None,
            repositories: vec![],
            machines: vec![crate::machine::current_machine()],
            enabled: true,
            ttl_secs: None,
            max_runtime_secs: None,
            power_saving_exempt: false,
            tags: vec![],
            env: std::collections::HashMap::new(),
            failure_threshold: None,
        notifications: Default::default(),
        },
    )
    .unwrap();
    let id = created.routine.id;
    // folder is slug of the title, not the UUID
    let created_slug = slugify(&created_title);
    assert!(crate::paths::routine_toml_path(&created_slug).exists());
    assert!(crate::paths::routine_compiled_prompt_path(&created_slug).exists());

    let updated = svc_update(
        &store,
        &id,
        UpdateRoutineRequest {
        disabled_reason: None,
            model: None,
            schedule: Some("@weekly".into()),
            schedules: None,
            title: Some(renamed_title.clone()),
            agent: Some("codex".into()),
            prompt: Some("p2".into()),
            goal: None,
            repositories: Some(vec![Repository {
                repository: "r".into(),
                branch: None,
                auto_pull: true,
            }]),
            machines: None,
            enabled: Some(false),
            ttl_secs: None,
            max_runtime_secs: None,
            power_saving_exempt: None,
            tags: None,
            env: None,
            failure_threshold: None,
        notifications: Default::default(),
        },
    )
    .unwrap();
    assert_eq!(updated.routine.schedule, "@weekly");
    assert_eq!(updated.routine.title, renamed_title);
    assert_eq!(updated.routine.agent, "codex");
    assert!(!updated.routine.enabled);

    svc_delete(&store, &id).unwrap();
    // after rename to "Renamed" and delete, the slug dir is gone
    assert!(!crate::paths::routine_dir(&slugify(&format!("Renamed {suffix}"))).exists());
}

#[test]
fn svc_update_not_found() {
    let req = UpdateRoutineRequest {
        disabled_reason: None,
        schedule: None,
        schedules: None,
        title: Some("x".into()),
        agent: None,
        model: None,
        prompt: None,
        goal: None,
        repositories: None,
        machines: None,
        enabled: None,
        ttl_secs: None,
        max_runtime_secs: None,
        power_saving_exempt: None,
        tags: None,
        env: None,
        failure_threshold: None,
        notifications: Default::default(),
    };
    assert!(svc_update(&new_store(), "missing", req).is_err());
}

#[test]
fn svc_update_invalid_cron_rejected() {
    let store = new_store();
    store
        .lock()
        .unwrap()
        .insert("id".into(), make_routine("id"));
    let req = UpdateRoutineRequest {
        disabled_reason: None,
        schedule: Some("bad".into()),
        schedules: None,
        title: None,
        agent: None,
        model: None,
        prompt: None,
        goal: None,
        repositories: None,
        machines: None,
        enabled: None,
        ttl_secs: None,
        max_runtime_secs: None,
        power_saving_exempt: None,
        tags: None,
        env: None,
        failure_threshold: None,
        notifications: Default::default(),
    };
    assert!(svc_update(&store, "id", req).is_err());
}

#[test]
fn svc_delete_not_found() {
    assert!(svc_delete(&new_store(), "missing").is_err());
}

#[allow(
    clippy::missing_docs_in_private_items,
    reason = "split-out module keeps the file under the linecheck limit"
)]
#[path = "svc_trigger_not_found_tests.rs"]
mod svc_trigger_not_found_tests;