1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#![allow(
clippy::missing_docs_in_private_items,
reason = "test helpers and fixtures do not need doc comments"
)]
//! Split out of `service_tests.rs` to keep that file under the repo's 700-line
//! pre-push line-count gate (`.githooks/pre-push`).
use super::*;
use crate::routines::new_store;
use std::sync::Mutex;
/// Point `MOADIM_HOME_OVERRIDE` at a fresh, empty temp home for the duration of a test, removing the
/// env var and the temp dir on drop. This keeps `svc_create`/`svc_update`/`write_routine` and the
/// other disk-touching paths off the developer's real `~/.moadim`, so a panicking assertion can never
/// leak test routines into the real home. Tests in this crate run single-threaded
/// (`RUST_TEST_THREADS=1`), so the global env mutation is safe.
struct TempHome(std::path::PathBuf);
impl TempHome {
fn set() -> Self {
let dir = std::env::temp_dir().join(format!("moadim-svctest-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&dir).expect("create temp home");
// SAFETY: single-threaded test execution.
unsafe {
std::env::set_var("MOADIM_HOME_OVERRIDE", &dir);
}
Self(dir)
}
}
impl Drop for TempHome {
fn drop(&mut self) {
// SAFETY: single-threaded test execution.
unsafe {
std::env::remove_var("MOADIM_HOME_OVERRIDE");
}
let _ = std::fs::remove_dir_all(&self.0);
}
}
/// Build a no-op update request (every field `None`); callers set one field.
fn empty_update_request() -> UpdateRoutineRequest {
UpdateRoutineRequest {
disabled_reason: None,
model: None,
goal: None,
schedule: None,
schedules: None,
title: None,
agent: None,
prompt: 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(),
}
}
/// Serializes the tests that clear `PATH`, so concurrent service tests never see
/// a stripped environment. The poisoned-lock case is recovered into the guard.
static PATH_GUARD: Mutex<()> = Mutex::new(());
/// Run `body` with an empty `PATH`, restoring the original value afterwards.
///
/// Clearing `PATH` makes the `crontab` and `sh` lookups inside the crontab sync
/// and the trigger spawn fail to launch, exercising their warning branches.
fn with_empty_path(body: impl FnOnce()) {
let guard = PATH_GUARD
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let saved = std::env::var_os("PATH");
std::env::set_var("PATH", "");
body();
match saved {
Some(value) => std::env::set_var("PATH", value),
None => std::env::remove_var("PATH"),
}
drop(guard);
}
#[test]
fn svc_update_not_found_when_no_schedule_and_id_missing() {
let _home = TempHome::set();
// Covers L359 error arm: `lock.get(id).ok_or(AppError::NotFound)?` fires when the
// routine does not exist in the store and no new schedule is provided.
let store = new_store(); // empty store
with_empty_path(|| {
let result = svc_update(
&store,
"nonexistent-id",
UpdateRoutineRequest {
disabled_reason: None,
model: None,
goal: None,
schedule: None,
schedules: None,
..empty_update_request()
},
);
assert!(matches!(result, Err(AppError::NotFound)));
});
}
#[test]
fn svc_update_not_found_when_schedule_provided_and_id_missing() {
let _home = TempHome::set();
// Covers L371 error arm: `lock.get_mut(id).ok_or(AppError::NotFound)?` fires when
// the routine does not exist in the store and a new schedule is provided.
let store = new_store(); // empty store
with_empty_path(|| {
let result = svc_update(
&store,
"nonexistent-id",
UpdateRoutineRequest {
disabled_reason: None,
model: None,
goal: None,
schedule: Some("@daily".into()),
schedules: None,
..empty_update_request()
},
);
assert!(matches!(result, Err(AppError::NotFound)));
});
}