src/service.rs
//! **INSTALL-TREE**, the root's view (lane B4): every item of
//! [`skidbladnir_systemd::service`] at its old path, plus what is not a systemd
//! host's concern: the Kubernetes renderer ([`crate::k8s::KubernetesRenderer`]),
//! the Windows SCM renderer ([`WindowsServiceRenderer`], over [`crate::msi`]), the
//! signed-bundle refresh ([`crate::service_backend::ServiceRefresh`]) and the
//! git watch ([`LifecycleWatch`]).
pub use skidbladnir_systemd::service::*;
pub use crate::service_backend::ServiceRefresh;
pub use crate::service_windows::WindowsServiceRenderer;
/// Resolve a [`Backend`] to its renderer: every backend this crate renders,
/// Kubernetes and the Windows SCM included. `None` only for a declared but
/// unrendered backend, never a fake success.
pub fn renderer_for(backend: Backend) -> Option<Box<dyn ServiceRenderer>> {
match backend {
Backend::Kubernetes => Some(Box::new(crate::k8s::KubernetesRenderer)),
Backend::WindowsService => Some(Box::new(WindowsServiceRenderer)),
other => skidbladnir_systemd::service::renderer_for(other),
}
}
/// **WATCH-REFRESH — poll git `HEAD`; refresh on change, no-op when unchanged.**
/// The one tick behind `korp-server --watch-refresh` / `dwarves-full
/// --watch-refresh`: read the current commit (gix, zero-shell), diff it against the
/// persisted [`crate::watch::WatchStamp`], and only on a MOVE drive the refresh
/// engine through the injected [`crate::watch::RefreshTrigger`], advancing the stamp
/// on success. Delegates to [`crate::watch::watch_refresh_once`].
pub trait LifecycleWatch {
fn watch_refresh(&self, req: &crate::watch::WatchRequest<'_>) -> anyhow::Result<crate::watch::WatchOutcome>;
}
impl LifecycleWatch for Lifecycle {
fn watch_refresh(&self, req: &crate::watch::WatchRequest<'_>) -> anyhow::Result<crate::watch::WatchOutcome> {
crate::watch::watch_refresh_once(req)
}
}
#[cfg(test)]
mod tests {
use super::*;
use skidbladnir_systemd::systemd::RestartPolicy;
fn holger() -> ServiceSpec {
ServiceSpec::new("holger", "Holger artifact repository", "/opt/holger/bin/holger")
.arg("serve")
.arg("--config")
.arg("/etc/holger/holger.toml")
.user("_holger")
.working_dir("/var/lib/holger")
.env("HOLGER_DATA", "/var/lib/holger")
.after("nornir-server")
.restart(RestartPolicy::OnFailure)
.socket(Socket::tcp(7070))
}
/// **Kubernetes has the real thing on both sides — render both.**
///
/// `resources.limits.memory` IS the hard ceiling (the kubelet writes the container
/// cgroup's `memory.max`; exceed it and the container is OOM-killed and restarted —
/// the `MemoryMax=` + `Restart=` pair). `resources.requests.memory` carries the
/// soft ceiling and is a near-miss, not a twin: it is a SCHEDULING guarantee, not a
/// throttle. It is the only soft-side number the pod API takes, and naming the
/// difference beats dropping the declaration.
///
/// RED direction: delete the `resources:` block from `KubernetesRenderer` and a pod
/// that declared a ceiling gets scheduled with none — the cluster-shaped version of
/// 2026-08-20.
#[test]
fn the_kubernetes_renderer_renders_limits_and_requests() {
let spec = ServiceSpec::new("dwarves-server", "d", "/usr/local/bin/dwarves-server")
.image("dwarves:1.0")
.memory(MemoryBound::gib(16, 24).unwrap());
let manifests: String = crate::k8s::KubernetesRenderer
.render(&spec).unwrap()
.files
.iter()
.map(|f| f.contents.clone())
.collect();
assert!(
manifests.contains(" limits:\n memory: \"24Gi\"\n"),
"the HARD ceiling is resources.limits.memory:\n{manifests}"
);
assert!(
manifests.contains(" requests:\n memory: \"16Gi\"\n"),
"the SOFT ceiling goes to requests — a near-miss, named as one:\n{manifests}"
);
// Nothing declared ⇒ no resources block, so every existing manifest is
// byte-identical (these are exact-bytes contract manifests).
let plain: String = crate::k8s::KubernetesRenderer
.render(&ServiceSpec::new("s", "d", "/bin/s").image("s:1")).unwrap()
.files
.iter()
.map(|f| f.contents.clone())
.collect();
assert!(
!plain.contains("resources:\n limits"),
"no declaration ⇒ no container resources block:\n{plain}"
);
}
/// Windows honest edges: `RestartPolicy::No` ⇒ NO crash-recovery block; a no-env
/// spec carries NO env `ServiceConfig`; no deps ⇒ NO `depend=` in the create verb;
/// and an unset user defaults the account to the built-in `LocalSystem`.
#[test]
fn windows_service_no_restart_no_env_and_default_account() {
let spec = ServiceSpec::new("svc", "d", "/bin/svc").restart(RestartPolicy::No);
let wv = WindowsServiceRenderer.render(&spec).unwrap();
let wxs = wv.file_ending("svc.wxs").expect("a .wxs fragment");
// No restart ⇒ no recovery block.
assert!(
!wxs.contains("FirstFailureActionType"),
"No ⇒ no crash-recovery:\n{wxs}"
);
// No env ⇒ no env ServiceConfig.
assert!(!wxs.contains("<!-- env:"), "no env ⇒ no env block:\n{wxs}");
// Enable-at-boot is still auto-start.
assert!(wxs.contains("Start=\"auto\""));
// Unset user → the built-in LocalSystem account, in BOTH the XML and the verb.
assert!(
wxs.contains("Account=\"LocalSystem\""),
"default account:\n{wxs}"
);
assert!(wv.enable.contains(&"LocalSystem".to_string()));
assert_eq!(
wv.enable
.iter()
.position(|s| s == "obj=")
.map(|i| &wv.enable[i + 1]),
Some(&"LocalSystem".to_string())
);
// No deps ⇒ no depend= flag.
assert!(
!wv.enable.contains(&"depend=".to_string()),
"no deps ⇒ no depend=: {:?}",
wv.enable
);
}
// The cross-backend registry tests (lane B4): they span the Kubernetes and the
// Windows renderers, so they run against the root's `renderer_for`.
/// `AgentMode` is pure classification metadata: a Fat spec and an otherwise
/// identical Thin spec render BYTE-IDENTICAL units on every backend (the mode
/// classifies the node; it does not change the unit), and it round-trips.
#[test]
fn agent_mode_is_metadata_only_same_rendered_output() {
let base = || {
ServiceSpec::new("nornir", "nornir server", "/usr/local/bin/nornir-server")
.user("nornir")
.socket(Socket::tcp(7878))
};
let fat = base().agent(AgentMode::Fat);
let thin = base().agent(AgentMode::Thin);
assert_ne!(fat.agent, thin.agent, "the specs really differ in mode");
for backend in [
Backend::SystemdSystem,
Backend::OpenRc,
Backend::SysV,
Backend::Runit,
Backend::S6Rc,
Backend::Dinit,
Backend::Kubernetes,
Backend::Oci,
Backend::Quadlet,
Backend::WindowsService,
] {
let rf = renderer_for(backend).unwrap().render(&fat).unwrap();
let rt = renderer_for(backend).unwrap().render(&thin).unwrap();
assert_eq!(
rf.files, rt.files,
"{backend:?}: agent mode must not change files"
);
assert_eq!(
rf.enable, rt.enable,
"{backend:?}: agent mode must not change verbs"
);
}
// And the mode survives a JSON round-trip.
let back: ServiceSpec =
serde_json::from_str(&serde_json::to_string(&thin).unwrap()).unwrap();
assert_eq!(back.agent, AgentMode::Thin);
}
/// Full backend-coverage matrix: EVERY backend now resolves to a renderer whose
/// `backend()` matches the key it was looked up under (11/11 — no honest `None`
/// remains). Guards against a renderer being wired to the wrong `Backend` (a
/// fake-success class of bug) and against the count silently regressing.
#[test]
fn backend_coverage_matrix_is_honest_and_self_consistent() {
let implemented = [
Backend::SystemdSystem,
Backend::SystemdUser,
Backend::OpenRc,
Backend::SysV,
Backend::Runit,
Backend::S6Rc,
Backend::Dinit,
Backend::Kubernetes,
Backend::Oci,
Backend::Quadlet,
Backend::WindowsService,
];
let mut resolved = 0;
for b in Backend::all() {
match renderer_for(*b) {
Some(r) => {
assert!(
implemented.contains(b),
"{b:?} resolved but is not in the implemented set"
);
assert_eq!(
r.backend(),
*b,
"{b:?}: renderer reports a DIFFERENT backend"
);
resolved += 1;
}
None => assert!(
!implemented.contains(b),
"{b:?} is implemented but resolved to None"
),
}
}
assert_eq!(
resolved,
implemented.len(),
"exactly the implemented backends resolve"
);
}
/// Stinky #20: config files ship WITH the spec. Every host backend renders them at
/// their own path and mode; the container backends also bind-mount them read-only
/// at the same path; Windows refuses rather than drop them.
#[test]
fn config_files_render_on_every_host_backend() {
let spec = ServiceSpec::new("bifrost", "Bifrost", "/usr/local/bin/bifrost")
.config_file("/etc/bifrost/m.yml", "graph: lake\n");
let mut spec600 = spec.clone();
spec600.config_files[0].mode = 0o640;
for b in [
Backend::SystemdSystem,
Backend::OpenRc,
Backend::SysV,
Backend::Runit,
Backend::S6Rc,
Backend::Dinit,
Backend::Oci,
Backend::Quadlet,
] {
let r = renderer_for(b).unwrap().render(&spec600).unwrap();
let f = r
.files
.iter()
.find(|f| f.path == "/etc/bifrost/m.yml")
.unwrap_or_else(|| panic!("{b:?} dropped the config file: {:?}", r.files));
assert_eq!(f.contents, "graph: lake\n", "{b:?}");
assert_eq!(f.mode, 0o640, "{b:?}");
}
let oci = renderer_for(Backend::Oci).unwrap().render(&spec).unwrap();
assert!(
oci.start.windows(2).any(|w| w[0] == "--volume" && w[1] == "/etc/bifrost/m.yml:/etc/bifrost/m.yml:ro"),
"{:?}",
oci.start
);
let q = renderer_for(Backend::Quadlet).unwrap().render(&spec).unwrap();
assert!(q.files[0].contents.contains("\nVolume=/etc/bifrost/m.yml:/etc/bifrost/m.yml:ro\n"));
let colon = ServiceSpec::new("bifrost", "B", "/b").config_file("/etc/a:b", "x");
assert!(renderer_for(Backend::Oci).unwrap().render(&colon).is_err());
assert!(renderer_for(Backend::SystemdSystem).unwrap().render(&colon).is_ok());
let w = renderer_for(Backend::WindowsService).unwrap().render(&spec);
assert!(w.unwrap_err().to_string().contains("config file"));
assert_eq!(to_systemd_spec(&spec).config_files, spec.config_files);
}
#[test]
fn every_backend_carries_secrets_privately_or_refuses() {
const TOKEN: &str = "tok-do-not-leak-123";
let spec = holger()
.image("registry.example/holger:1")
.secret_env("HOLGER_TOKEN", TOKEN);
for &b in Backend::all() {
let r = renderer_for(b).expect("every backend renders").render(&spec);
let Ok(r) = r else {
assert_eq!(b, Backend::WindowsService, "only Windows may refuse: {r:?}");
continue;
};
let holders: Vec<_> = r.files.iter().filter(|f| f.contents.contains(TOKEN)).collect();
assert!(!holders.is_empty(), "{b:?} dropped the secret: {:#?}", r.files);
for f in holders {
assert_eq!(f.mode, 0o600, "{b:?}: secret in non-private {}", f.path);
}
}
}
/// Stinky #2: EVERY backend either carries a secret in a private (0600) file it
/// renders, or REFUSES. Before, only k8s and `to_systemd_spec` read `secret_env`;
/// every other renderer dropped it and the service started without its token.
/// Stinky #7/#17: the neutral spec reached every renderer unchecked — a service
/// name of `../x` became `/etc/init.d/../x`, a newline in a description or user
/// injected lines into a unit/script, and an env key with a space or `=` produced
/// an assignment no backend can express. Every backend now refuses such a spec.
#[test]
fn every_backend_refuses_an_unsafe_spec() {
let base = || holger().image("registry.example/holger:1");
let bad: Vec<(&str, ServiceSpec)> = vec![
("name traversal", { let mut s = base(); s.name = "../x".into(); s }),
("name slash", { let mut s = base(); s.name = "a/b".into(); s }),
("description newline", { let mut s = base(); s.description = "a\nb".into(); s }),
("user newline", base().user("svc\nroot")),
("working dir newline", base().working_dir("/srv\n/x")),
("env key space", base().env("A B", "v")),
("secret key =", base().secret_env("T=U", "v")),
("operator key digit", base().operator_env("1X", "v")),
];
for (what, spec) in &bad {
for &b in Backend::all() {
let r = renderer_for(b).expect("registered").render(spec);
assert!(r.is_err(), "{b:?} rendered a spec with {what}");
}
}
// A multi-line VALUE is valid: every backend whose env format can carry it
// renders it. The literal NAME=VALUE files (dinit, podman) cannot and refuse
// it by name (stinky #10), and Windows refuses env it would store in the clear.
for &b in Backend::all() {
let r = renderer_for(b)
.unwrap()
.render(&base().env("K", "multi\nline value"));
match b {
Backend::WindowsService => {}
Backend::Dinit | Backend::Oci => {
assert!(r.is_err(), "{b:?} wrote a newline into a literal env file")
}
_ => {
r.unwrap_or_else(|e| panic!("{b:?} refused a valid spec: {e}"));
}
}
}
}
/// **The five backends with NO cgroup route must SAY so, naming the numbers.**
///
/// runit's `chpst -m`, s6's `s6-softlimit -m`, dinit's `rlimit-data` and SysV's
/// `ulimit -v` all *look* like the answer and are a different mechanism: an rlimit
/// caps one process's address space, is accounted per-process, never reclaims, has
/// no soft ceiling, and when it bites the allocation fails inside the program
/// instead of the cgroup being killed and restarted. Rendering one would claim a
/// bound that is not one and leave the box exactly as exposed as it was on
/// 2026-08-20. Windows' job objects are real but unreachable from `sc.exe` or a WiX
/// `<ServiceInstall>`.
///
/// So: an explicit `NOT HONOURED` note carrying BOTH numbers, in the artifact the
/// operator reads — the same discipline as OpenRC's supplementary-groups note.
///
/// RED direction: drop any one of the five `memory_not_honoured_note` calls and
/// that backend's assertion fails while its script still renders and installs —
/// precisely the silent drop this asserts against.
#[test]
fn every_backend_without_a_cgroup_route_names_the_ceiling_it_cannot_honour() {
let bound = MemoryBound::gib(16, 24).unwrap();
let spec = ServiceSpec::new(
"dwarves-server",
"the forge",
"/usr/local/bin/dwarves-server",
)
.user("dwarves")
.memory(bound);
let plain = ServiceSpec::new(
"dwarves-server",
"the forge",
"/usr/local/bin/dwarves-server",
)
.user("dwarves");
let all_files = |s: &ServiceSpec, b: Backend| -> String {
renderer_for(b)
.unwrap()
.render(s).unwrap()
.files
.iter()
.map(|f| f.contents.clone())
.collect()
};
for backend in [
Backend::Runit,
Backend::S6Rc,
Backend::Dinit,
Backend::SysV,
Backend::WindowsService,
] {
let text = all_files(&spec, backend);
assert!(
text.contains("NOT HONOURED"),
"{backend:?}: an unrenderable ceiling must be VISIBLE, not silent:\n{text}"
);
assert!(
text.contains("17179869184") && text.contains("25769803776"),
"{backend:?}: the note must name BOTH numbers or nobody can act on it:\n{text}"
);
// THE COUNTERWEIGHT: nothing declared ⇒ no note, so every existing script,
// service dir and .wxs is unchanged.
let untouched = all_files(&plain, backend);
assert!(
!untouched.contains("NOT HONOURED") && !untouched.contains("skidbladnir:"),
"{backend:?}: nothing declared, nothing to warn about:\n{untouched}"
);
}
// …and the backends that DO have a route must never emit the note: claiming a
// gap you do not have is the same dishonesty from the other side.
for backend in [
Backend::SystemdSystem,
Backend::SystemdUser,
Backend::OpenRc,
Backend::Oci,
Backend::Quadlet,
Backend::Kubernetes,
] {
let text = all_files(&spec, backend);
assert!(
!text.contains("NOT HONOURED"),
"{backend:?} has a real route and must render it, not excuse itself:\n{text}"
);
}
}
/// **Every backend that cannot react to an OOM kill must SAY so — and Quadlet,
/// which can, must render it.**
///
/// The ceiling's fan-out one axis over, with one deliberate difference in the
/// seam: `Memory=` goes on Quadlet's `[Container]` side (podman must bound the
/// container's cgroup), while `OOMPolicy=` goes on the `[Service]` side, because
/// the reaction is a decision the service MANAGER makes about a unit and podman
/// has no equivalent flag — the same reason `Restart=` is already a `[Service]`
/// key here instead of `--restart`.
///
/// OpenRC is asserted in the NOT-HONOURED set on purpose, and that is a judgement
/// rather than a limitation: `rc_cgroup_settings` really can write
/// `memory.oom.group 1`, which is exactly what `OOMPolicy=kill` is defined as, so
/// a third of this axis is reachable. Rendering only that third would make
/// `continue` and `stop` silently mean something else, so the note names the
/// by-hand route and claims nothing.
///
/// RED direction: drop any one `oom_not_honoured_note` call and that backend
/// renders a service whose declared reaction has vanished without a word; drop the
/// Quadlet block and a container service inherits the box's `DefaultOOMPolicy=`
/// while the spec says otherwise.
#[test]
fn every_backend_without_an_oom_route_names_the_reaction_it_cannot_honour() {
let spec = ServiceSpec::new(
"dwarves-server",
"the forge",
"/usr/local/bin/dwarves-server",
)
.user("dwarves")
.oom_policy(OomPolicy::Continue);
let plain = ServiceSpec::new(
"dwarves-server",
"the forge",
"/usr/local/bin/dwarves-server",
)
.user("dwarves");
let all_files = |s: &ServiceSpec, b: Backend| -> String {
renderer_for(b)
.unwrap()
.render(s).unwrap()
.files
.iter()
.map(|f| f.contents.clone())
.collect()
};
for backend in [
Backend::Runit,
Backend::S6Rc,
Backend::Dinit,
Backend::SysV,
Backend::WindowsService,
Backend::OpenRc,
] {
let text = all_files(&spec, backend);
assert!(
text.contains("NOT HONOURED") && text.contains("OOMPolicy=continue"),
"{backend:?}: an unrenderable reaction must be VISIBLE, and must quote \
what was declared:\n{text}"
);
assert!(
text.contains("KEEPS RUNNING"),
"{backend:?}: the note has to say what the reaction MEANS, or an operator \
cannot reproduce it by hand:\n{text}"
);
// THE COUNTERWEIGHT: nothing declared ⇒ not one word added.
let untouched = all_files(&plain, backend);
assert!(
!untouched.contains("NOT HONOURED") && !untouched.contains("skidbladnir:"),
"{backend:?}: nothing declared, nothing to warn about:\n{untouched}"
);
}
// Quadlet HAS the directive — through the `[Service]` pass-through, not on the
// container — so it must render it and must not excuse itself.
let quadlet = all_files(&spec, Backend::Quadlet);
assert!(
quadlet.contains("OOMPolicy=continue\n") && !quadlet.contains("NOT HONOURED"),
"quadlet passes [Service] keys straight through to the generated unit:\n{quadlet}"
);
assert!(
!all_files(&plain, Backend::Quadlet).contains("OOMPolicy"),
"…and nothing declared still renders a byte-identical .container file"
);
// Both systemd scopes render the real directive.
for backend in [Backend::SystemdSystem, Backend::SystemdUser] {
let text = all_files(&spec, backend);
assert!(
text.contains("OOMPolicy=continue\n"),
"{backend:?} has the directive natively:\n{text}"
);
}
}
/// **Every backend that cannot rate-limit restarts must SAY so — and Quadlet,
/// which can, must render it.**
///
/// The note carries the DERIVED OUTCOME ("give up after N attempts and STAY
/// DOWN"), not just the two numbers, because an operator granting this on another
/// init system needs the behaviour — reproducing `12` and `5` onto a supervisor
/// with different arithmetic gets the opposite result.
///
/// dinit and OpenRC are in the NOT-HONOURED set as a JUDGEMENT, not a limitation:
/// both are believed to have a real structural twin, and neither is rendered
/// because no openrc/dinit/runit/s6 binary or man page exists on the box this was
/// written on, and a mis-spelled variable in either is SILENTLY IGNORED — a limit
/// that looks granted and does nothing, which is worse than a note. Same ruling as
/// the OpenRC OOM partial.
///
/// RED direction: drop any one `start_limit_not_honoured_note` and that backend
/// renders a service whose declared give-up point has vanished.
#[test]
fn every_backend_without_a_rate_limiter_names_the_budget_it_cannot_honour() {
let spec = ServiceSpec::new("dwarves-server", "the forge", "/usr/local/bin/d")
.user("dwarves")
.restart_sec(2)
.start_limit(StartLimit::give_up_after(5, 2).unwrap());
let plain =
ServiceSpec::new("dwarves-server", "the forge", "/usr/local/bin/d").user("dwarves");
let all_files = |s: &ServiceSpec, b: Backend| -> String {
renderer_for(b)
.unwrap()
.render(s).unwrap()
.files
.iter()
.map(|f| f.contents.clone())
.collect()
};
for backend in [
Backend::Runit,
Backend::S6Rc,
Backend::Dinit,
Backend::SysV,
Backend::WindowsService,
Backend::OpenRc,
] {
let text = all_files(&spec, backend);
assert!(
text.contains("NOT HONOURED"),
"{backend:?}: an unrenderable budget must be VISIBLE:\n{text}"
);
assert!(
text.contains("StartLimitIntervalSec=12") && text.contains("StartLimitBurst=5"),
"{backend:?}: the note must quote what was declared:\n{text}"
);
assert!(
text.contains("give up after 5 attempts") && text.contains("STAY DOWN"),
"{backend:?}: …and the OUTCOME, not just the arithmetic:\n{text}"
);
// THE COUNTERWEIGHT.
let untouched = all_files(&plain, backend);
assert!(
!untouched.contains("NOT HONOURED") && !untouched.contains("skidbladnir:"),
"{backend:?}: nothing declared, nothing to warn about:\n{untouched}"
);
}
// Quadlet renders the real directive, on the [Unit] side this time.
let quadlet = all_files(&spec, Backend::Quadlet);
assert!(
quadlet.contains("StartLimitIntervalSec=12\n")
&& quadlet.contains("StartLimitBurst=5\n")
&& !quadlet.contains("NOT HONOURED"),
"quadlet passes [Unit] keys through to the generated unit:\n{quadlet}"
);
assert!(
!all_files(&plain, Backend::Quadlet).contains("StartLimit"),
"…and nothing declared still renders a byte-identical .container file"
);
// A `never_give_up` note must read as the OPPOSITE intent, not as a budget.
let forever = all_files(
&ServiceSpec::new("d", "d", "/bin/d").start_limit(StartLimit::never_give_up()),
Backend::Runit,
);
assert!(
forever.contains("RETRY FOREVER") && !forever.contains("give up after"),
"an explicit no-limit must not be described as a budget:\n{forever}"
);
}
/// The backend registry is honest: implemented backends resolve to a renderer,
/// declared-but-unrendered ones resolve to `None` (never a fake), and the slug
/// axis covers the whole tree for the coverage matrix.
#[test]
fn registry_resolves_only_implemented_backends() {
assert!(renderer_for(Backend::SystemdSystem).is_some());
assert!(renderer_for(Backend::SystemdUser).is_some());
assert!(renderer_for(Backend::OpenRc).is_some());
assert!(renderer_for(Backend::SysV).is_some());
assert!(renderer_for(Backend::Runit).is_some());
assert!(renderer_for(Backend::S6Rc).is_some());
assert!(renderer_for(Backend::Dinit).is_some());
assert!(renderer_for(Backend::Kubernetes).is_some());
assert!(renderer_for(Backend::Oci).is_some());
assert!(renderer_for(Backend::Quadlet).is_some());
assert!(renderer_for(Backend::WindowsService).is_some());
// Full coverage: EVERY declared backend now resolves to a renderer.
assert!(
Backend::all().iter().all(|b| renderer_for(*b).is_some()),
"11/11 render"
);
// Every backend has a stable, unique slug.
let slugs: Vec<_> = Backend::all().iter().map(|b| b.slug()).collect();
let uniq: std::collections::BTreeSet<_> = slugs.iter().collect();
assert_eq!(slugs.len(), uniq.len(), "slugs are unique");
assert_eq!(Backend::all().len(), 11);
}
/// **WHICH service the kernel picks — the axis that would have saved gunnar.**
///
/// The completion of the OOM story, and the only one of the four that is about the
/// NEIGHBOURS. The ceiling bounds one service and the reaction decides its fate
/// once reaped; neither says who gets reaped when the machine as a whole runs out.
/// On 2026-08-20 that was `gunnar-service`, taken alongside the forge that caused
/// the shortage, because every unit on the box sat at the same score.
///
/// podman HAS the exact flag (`--oom-score-adj int Tune the host's OOM
/// preferences (-1000 to 1000)`, read off `podman run --help` on this box), so the
/// OCI renderer honours it — the one OOM axis that argv backend can express.
///
/// RED direction: drop the `spec.oom_score_adjust` block from `systemd_unit_scoped`
/// and a declared bias vanishes while the unit still installs.
#[test]
fn the_kernels_choice_of_victim_is_declared_and_fans_out() {
let spec = ServiceSpec::new("dwarves-server", "the forge", "/usr/local/bin/d")
.user("dwarves")
.oom_score_adjust(OomScoreAdjust::sacrifice_first());
let plain =
ServiceSpec::new("dwarves-server", "the forge", "/usr/local/bin/d").user("dwarves");
// systemd renders the real directive, in [Service] beside the reaction.
assert!(spec.render_systemd_unit().contains("OOMScoreAdjust=500\n"));
assert!(
!plain.render_systemd_unit().contains("OOMScoreAdjust"),
"undeclared must stay byte-identical"
);
let all_files = |s: &ServiceSpec, b: Backend| -> String {
renderer_for(b)
.unwrap()
.render(s).unwrap()
.files
.iter()
.map(|f| f.contents.clone())
.collect()
};
// podman: the verified flag, in argv — not a note.
let oci = renderer_for(Backend::Oci).unwrap().render(&spec).unwrap();
assert!(
oci.start.contains(&"--oom-score-adj=500".to_string()),
"podman has the exact flag: {:?}",
oci.start
);
// Quadlet: the same flag through PodmanArgs, the container side of the seam.
assert!(all_files(&spec, Backend::Quadlet).contains("PodmanArgs=--oom-score-adj=500\n"));
// The five shell backends + Windows name it, and hand over the VERIFIED route.
for backend in [
Backend::Runit,
Backend::S6Rc,
Backend::Dinit,
Backend::SysV,
Backend::OpenRc,
Backend::WindowsService,
] {
let text = all_files(&spec, backend);
assert!(
text.contains("NOT HONOURED") && text.contains("OOMScoreAdjust=500"),
"{backend:?} must name the declared bias:\n{text}"
);
assert!(
text.contains("echo 500 > /proc/self/oom_score_adj"),
"{backend:?}: hand over the by-hand COMMAND, not a description:\n{text}"
);
assert!(
!all_files(&plain, backend).contains("skidbladnir:"),
"{backend:?}: nothing declared, nothing to warn about"
);
}
// The PROTECTIVE direction carries the measured privilege caution; the
// sacrificial one must NOT, or the caution stops being read.
let protected = ServiceSpec::new("gunnar-service", "g", "/bin/g")
.oom_score_adjust(OomScoreAdjust::new(-500).unwrap());
let note = all_files(&protected, Backend::Runit);
assert!(
note.contains("CAP_SYS_RESOURCE") && note.contains("-500 FAILED"),
"a negative bias needs the capability, and the note must say so:\n{note}"
);
assert!(
!all_files(&spec, Backend::Runit).contains("CAP_SYS_RESOURCE"),
"raising is unprivileged — do not cry wolf on the common direction"
);
// Out of range is refused by name, quoting the value and the bounds.
let refused = OomScoreAdjust::new(1001).expect_err("systemd rejects the whole unit");
assert!(refused.to_string().contains("1001") && refused.to_string().contains("-1000"));
assert!(OomScoreAdjust::new(-1000).is_ok() && OomScoreAdjust::new(1000).is_ok());
assert!(OomScoreAdjust::new(-1001).is_err());
assert!(OomScoreAdjust::sacrifice_first().value() > 0);
assert!(!OomScoreAdjust::sacrifice_first().needs_privilege());
assert!(OomScoreAdjust::new(-1).unwrap().needs_privilege());
}
/// Windows service renderer: ONE spec renders a real WiX `.wxs` service fragment
/// (through the SHARED `msi::render_service_install` seam) — `<ServiceInstall>`
/// with the account, delayed-auto start and a `restart` crash-recovery block, the
/// env carried in the `ServiceConfig` — plus the `sc.exe` create/start/query/stop
/// verbs (create carrying `binPath=`/`obj=`/`depend=`/`DisplayName=`). Every
/// assertion is RED-when-broken (a wrong attr/flag/order fails the build).
#[test]
fn windows_service_renders_a_wix_fragment_and_sc_verbs() {
let spec = holger();
let wv = renderer_for(Backend::WindowsService).unwrap().render(&spec).unwrap();
assert_eq!(wv.backend, Backend::WindowsService);
// File: a standalone WiX `.wxs` fragment via the reused MSI seam.
let wxs = wv
.file_ending("holger.wxs")
.expect("a .wxs service fragment");
assert!(wxs.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"));
assert!(wxs.contains("<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">"));
assert!(wxs.contains("<Fragment Id=\"holgerService\">"));
assert!(wxs.contains("<ServiceInstall"));
assert!(wxs.contains("Name=\"holger\""));
assert!(wxs.contains("DisplayName=\"Holger artifact repository\""));
assert!(
wxs.contains("Account=\"_holger\""),
"user → the SCM account:\n{wxs}"
);
assert!(wxs.contains("Start=\"auto\""));
assert!(wxs.contains("DelayedAutoStart=\"yes\""));
// RestartPolicy::OnFailure (restart_sec default 2) ⇒ a crash-recovery block —
// restart_sec IS expressible on Windows (unlike the OCI backend).
assert!(
wxs.contains("FirstFailureActionType=\"restart\""),
"restart recovery:\n{wxs}"
);
assert!(
wxs.contains("RestartServiceDelayInSeconds=\"2\""),
"restart_sec → recovery delay:\n{wxs}"
);
// env is carried in the fragment (sc.exe cannot set it).
assert!(
wxs.contains("<!-- env: HOLGER_DATA=/var/lib/holger -->"),
"env → ServiceConfig block:\n{wxs}"
);
assert!(wxs.contains("<ServiceControl"));
assert_eq!(wv.files[0].path, "C:\\ProgramData\\holger\\holger.wxs");
assert_eq!(wv.files[0].mode, 0o644);
// Verbs: sc.exe create IS the enable (binPath=/obj=/depend=/DisplayName=).
assert_eq!(
wv.enable,
vec![
"sc.exe",
"create",
"holger",
"binPath=",
"/opt/holger/bin/holger serve --config /etc/holger/holger.toml",
"start=",
"auto",
"obj=",
"_holger",
"depend=",
"nornir-server",
"DisplayName=",
"Holger artifact repository",
]
);
assert_eq!(wv.start, vec!["sc.exe", "start", "holger"]);
assert_eq!(wv.status, vec!["sc.exe", "query", "holger"]);
assert_eq!(wv.stop, vec!["sc.exe", "stop", "holger"]);
}
}