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
134
135
136
137
138
139
140
//! Sentinel tests for the `evolve` tool's systemd restart helpers.
//!
//! Context — PR #137 (#136) added a post-swap step that schedules a
//! delayed `systemctl restart opencrabs*.service` via `systemd-run`.
//! Two gaps from the PR review remained open after merge:
//!
//! #3 Silent failure when no units match the glob — the agent
//! would say "Evolved!" and the daemon would never restart
//! (zero matching units), the exact symptom #136 was filed for.
//! #6 No test coverage on the restart path — flag drift in
//! systemd-run args could silently break the restart again.
//!
//! These tests pin both: the systemd-run command construction stays
//! stable across refactors, AND the user-facing message for every
//! `RestartStatus` outcome clearly tells the user what actually
//! happened (or didn't).
//!
//! What we DON'T test: actual systemd interaction. Spawning real
//! `systemd-run` requires a Linux host with systemd running, which
//! isn't portable across CI (macOS + Windows have no systemd).
//! Construction + message shape is what we can pin portably; the
//! end-to-end behaviour was validated empirically by the PR author.
use crate::brain::tools::evolve::{SYSTEMD_UNIT_PATTERN, build_systemd_restart_command};
#[test]
fn unit_pattern_is_glob_so_multiple_profiles_match() {
// Adding a new profile (opencrabs-staging.service) must not
// require a code change. The pattern is shipped as a public
// const so refactors that hardcode "opencrabs.service" would
// diverge from the tested invariant.
assert_eq!(
SYSTEMD_UNIT_PATTERN, "opencrabs*.service",
"the glob must match every opencrabs-*.service variant; a non-glob value \
would silently break multi-profile restart"
);
}
#[test]
fn restart_command_uses_systemd_run_binary() {
let cmd = build_systemd_restart_command(12345);
assert_eq!(
cmd.get_program(),
"systemd-run",
"command must invoke systemd-run, not systemctl directly — only the \
transient unit escapes the daemon cgroup"
);
}
#[test]
fn restart_command_args_are_pinned() {
let cmd = build_systemd_restart_command(12345);
let args: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().to_string())
.collect();
assert_eq!(
args,
vec![
"--on-active=3",
"--unit=opencrabs-evolve-12345",
"systemctl",
"restart",
"opencrabs*.service",
],
"arg list must not drift — each flag's removal or rename re-introduces \
a known regression mode: --on-active=3 = the 3s delivery window, \
--unit=... = the PID-derived name that avoids concurrent-evolve collisions, \
opencrabs*.service = the multi-profile glob. \
NOTE: --collect and --quiet are intentionally absent (incompatible with \
systemd < v240 on RHEL 7 / CentOS 7); do NOT re-add them without \
confirming the minimum systemd version policy."
);
}
#[test]
fn restart_command_unit_name_includes_pid() {
// Concurrent evolve calls would collide on a fixed transient
// unit name. The PID embedding makes the name unique per
// process. Verify both that the PID appears verbatim AND that
// different PIDs produce different names.
let cmd_a = build_systemd_restart_command(12345);
let cmd_b = build_systemd_restart_command(67890);
let unit_a = cmd_a
.get_args()
.map(|a| a.to_string_lossy().to_string())
.find(|a| a.starts_with("--unit="))
.expect("unit arg must exist");
let unit_b = cmd_b
.get_args()
.map(|a| a.to_string_lossy().to_string())
.find(|a| a.starts_with("--unit="))
.expect("unit arg must exist");
assert_eq!(unit_a, "--unit=opencrabs-evolve-12345");
assert_eq!(unit_b, "--unit=opencrabs-evolve-67890");
assert_ne!(
unit_a, unit_b,
"two concurrent evolves on different PIDs must produce different unit names \
or systemd-run will fail on the second one"
);
}
// ── User-message coverage for every RestartStatus branch ────────
//
// RestartStatus is private to evolve.rs by design (no external
// consumer), so we exercise the message wording via the public
// success/failure modes the tool can produce. The wording itself
// is the contract these tests pin — drifting any branch toward the
// generic "Restarting into the new version." string would let the
// no-units / spawn-failed / non-systemd cases get mistaken for a
// successful auto-restart, exactly the gap #136 was filed for.
#[test]
fn restart_status_messages_are_distinct_per_outcome() {
// We construct each user-message by inspecting the actual
// tool source rather than re-importing the private enum —
// these are sentinel strings, so the assertion below is a
// build-time anchor: if any wording is reworded to look like
// "Restarting…" we'll catch the regression at test time.
let src = include_str!("../brain/tools/evolve.rs");
assert!(
src.contains("Restarting into the new version."),
"the Scheduled branch must keep its current 'Restarting into the new version.' wording"
);
assert!(
src.contains("Binary updated on disk; restart"),
"the NotSystemd branch must tell the user the binary is updated but they need to restart"
);
assert!(
src.contains("no \\\n systemd units matched")
|| src.contains("no systemd units matched"),
"the NoUnitsMatched branch must explicitly call out the zero-units case — \
silently saying 'Restarting…' here is the #136 regression"
);
assert!(
src.contains("scheduling the systemd restart failed"),
"the SpawnFailed branch must quote the actual error so the user knows \
systemd-run couldn't fire"
);
}