Files
mesh-catalog/modules/systemd/test/client.test.ts
T
jochen a80af7a97f systemd owns its package; systemd-networkd configures networkd and claims none
The service manager's package was declared by the networking module, so the
module that is systemd could not own it and had to leave it out. networkd is a
component of systemd: its module configures it. Removing the package resource
from systemd-networkd uninstalls nothing — the host never removes a package
that is not declared absent.
2026-10-04 12:17:08 +02:00

168 lines
9.7 KiB
TypeScript

// The service manager's verbs over a fake runner (novox/hq ADR 0177, to-be 41 WP4): which manager
// a call reaches and how, acts on the system manager escalated, failures named rather than read as
// empty answers, and whether the mesh declares a unit.
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { MESH_UNIT_HEADER, ServiceManager, escalated, sessionEnv, unitArg, type Ran, type Runner } from "../client.ts";
interface Call {
cmd: string;
args: string[];
env?: NodeJS.ProcessEnv;
}
function fake(answer: (c: Call) => Partial<Ran>, calls: Call[] = []): Runner {
return async (cmd, args, env) => {
const c = { cmd, args, env };
calls.push(c);
return { stdout: "", stderr: "", status: 0, ...answer(c) };
};
}
const LIST = "sshd.service loaded active running OpenSSH Daemon\nbroken.service loaded failed failed A broken thing\n";
const SHOW_MESH = "LoadState=loaded\nActiveState=active\nSubState=running\nUnitFileState=enabled\nMainPID=42\nExecMainStatus=0\nDescription=showcase, a mesh daemon\nFragmentPath=/etc/systemd/system/showcase.service\n";
const SHOW_PACKAGE = "LoadState=loaded\nActiveState=active\nSubState=running\nUnitFileState=enabled\nMainPID=7\nExecMainStatus=0\nDescription=OpenSSH Daemon\nFragmentPath=/usr/lib/systemd/system/sshd.service\n";
const files: Record<string, string> = {
"/etc/systemd/system/showcase.service": `${MESH_UNIT_HEADER} Do not edit — this file is replaced whenever the\n[Unit]\n`,
"/usr/lib/systemd/system/sshd.service": "[Unit]\nDescription=OpenSSH Daemon\n",
};
const read = async (p: string) => {
if (p in files) return files[p];
throw new Error("ENOENT");
};
function manager(run: Runner, o: { uid?: number; user?: string } = {}): ServiceManager {
return new ServiceManager({ account: "operator", uid: o.uid ?? 1000, user: o.user ?? "operator", run, read });
}
test("an act on the system manager goes through sudo without a prompt unless this is root; reads never do", () => {
assert.deepEqual(escalated("systemctl", ["restart", "sshd.service"], "system", 1000), ["sudo", ["-n", "systemctl", "restart", "sshd.service"]]);
for (const verb of ["start", "stop", "enable", "disable"]) {
assert.equal(escalated("systemctl", [verb, "x.service"], "system", 1000)[0], "sudo");
}
assert.deepEqual(escalated("systemctl", ["restart", "sshd.service"], "system", 0), ["systemctl", ["restart", "sshd.service"]]);
assert.deepEqual(escalated("systemctl", ["show", "sshd.service"], "system", 1000), ["systemctl", ["show", "sshd.service"]]);
assert.deepEqual(escalated("systemctl", ["list-units", "restart"], "system", 1000)[0], "systemctl");
assert.deepEqual(escalated("systemctl", ["--user", "restart", "x.service"], "user", 1000)[0], "systemctl");
assert.deepEqual(escalated("journalctl", ["-u", "x"], "system", 1000)[0], "journalctl");
});
test("the user scope is plain --user, with the account's runtime directory and bus named", async () => {
const calls: Call[] = [];
const m = manager(fake(() => ({ stdout: LIST }), calls), { uid: 1234 });
await m.units("user");
assert.equal(calls[0].cmd, "systemctl");
assert.equal(calls[0].args[0], "--user");
assert.ok(!calls[0].args.some((a) => a.startsWith("--machine")));
assert.equal(calls[0].env?.XDG_RUNTIME_DIR, "/run/user/1234");
assert.equal(calls[0].env?.DBUS_SESSION_BUS_ADDRESS, "unix:path=/run/user/1234/bus");
await m.journal("user", "watcher.service", 10);
assert.deepEqual(calls[1].args.slice(0, 1), ["--user"]);
assert.equal(calls[1].cmd, "journalctl");
assert.equal(calls[1].env?.XDG_RUNTIME_DIR, "/run/user/1234");
assert.deepEqual(sessionEnv(5, { HOME: "/h" }), { HOME: "/h", XDG_RUNTIME_DIR: "/run/user/5", DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/5/bus" });
});
test("the system scope is given no session words", async () => {
const calls: Call[] = [];
await manager(fake(() => ({ stdout: LIST }), calls)).units("system");
assert.equal(calls[0].env, undefined);
assert.ok(!calls[0].args.includes("--user"));
});
test("the user scope from a process that is not the account is refused, not answered from the wrong manager", async () => {
const m = manager(fake(() => ({ stdout: LIST })), { user: "root", uid: 0 });
await assert.rejects(() => m.units("user"), /operator's service manager, and this runs as root/);
});
test("a system act escalates, and answers with the state after", async () => {
const calls: Call[] = [];
const m = manager(fake((c) => (c.args.includes("show") ? { stdout: SHOW_PACKAGE } : {}), calls));
const r = await m.act("system", "restart", "sshd.service");
assert.deepEqual([calls[0].cmd, ...calls[0].args], ["sudo", "-n", "systemctl", "restart", "sshd.service"]);
assert.equal(r.ok, true);
assert.equal(r.active, "active");
assert.equal(r.mesh_declared, false);
assert.equal(r.note, undefined, "no restore note on a unit the mesh did not write");
});
test("the restore note is attached only to a unit the mesh declares", async () => {
const m = manager(fake((c) => (c.args.includes("show") ? { stdout: SHOW_MESH } : {})));
const r = await m.act("system", "stop", "showcase.service");
assert.equal(r.mesh_declared, true);
assert.match(String(r.note), /host restores its declared state/);
});
test("status says whether the mesh declares the unit, from the unit file systemd loaded", async () => {
const mesh = await manager(fake(() => ({ stdout: SHOW_MESH }))).status("system", "showcase.service");
assert.equal(mesh.mesh_declared, true);
assert.equal(mesh.MainPID, "42");
const pkg = await manager(fake(() => ({ stdout: SHOW_PACKAGE }))).status("system", "sshd.service");
assert.equal(pkg.mesh_declared, false);
const none = await manager(fake(() => ({ stdout: "LoadState=not-found\nFragmentPath=\n" }))).status("system", "nope.service");
assert.equal(none.mesh_declared, false);
});
test("the header recognised is the one the host writes", () => {
// mesh-host internal/apply/process.go, unitFor: the first line of every unit the host writes.
assert.equal(MESH_UNIT_HEADER, "# Generated by the mesh.");
});
test("sudo refusing is named as a refusal; sudo missing is named as missing", async () => {
const refused = manager(fake(() => ({ status: 1, stderr: "sudo: a password is required\n" })));
await assert.rejects(() => refused.act("system", "start", "x.service"), /needs root for this and the runtime's account may not run it without a prompt: sudo: a password is required/);
const missing = manager(fake(() => ({ status: 127, error: "ENOENT" })));
await assert.rejects(() => missing.act("system", "start", "x.service"), /sudo is not installed here/);
});
test("polkit refusing is named", async () => {
const m = manager(fake(() => ({ status: 1, stderr: "Failed to stop x.service: Access denied as the requested operation requires interactive authentication.\n" })), { uid: 0, user: "root" });
await assert.rejects(() => m.act("system", "stop", "x.service"), /the service manager refused the runtime's account/);
});
test("a failed systemctl is an error, not an empty list", async () => {
const m = manager(fake(() => ({ status: 1, stderr: "Failed to list units: Connection timed out\n" })));
await assert.rejects(() => m.units("system"), /systemctl failed \(1\): Failed to list units: Connection timed out/);
});
test("an unreachable user manager is said, even when systemctl exits 0", async () => {
const said = "Failed to connect to user scope bus via local transport: No such file or directory\n";
const m = manager(fake(() => ({ status: 0, stderr: said })), { uid: 1000 });
await assert.rejects(() => m.units("user"), /operator's own service manager does not answer at \/run\/user\/1000/);
const nonzero = manager(fake(() => ({ status: 1, stderr: said })));
await assert.rejects(() => nonzero.status("user", "x.service"), /does not answer/);
});
test("failed reports each manager's failed units, and a manager that does not answer by its error", async () => {
const m = manager(fake((c) =>
c.args[0] === "--user" ? { status: 1, stderr: "Failed to connect to user scope bus via local transport: No such file or directory\n" } : { stdout: LIST }));
const r = await m.failed();
assert.deepEqual(r.system, [{ unit: "broken.service", load: "loaded", active: "failed", sub: "failed", description: "A broken thing" }]);
assert.ok(!Array.isArray(r.user));
assert.match((r.user as { error: string }).error, /does not answer/);
});
test("a unit's name is never an option", async () => {
assert.throws(() => unitArg("--host=elsewhere"), /is not a unit's name/);
assert.throws(() => unitArg("a b"), /is not a unit's name/);
assert.equal(unitArg("sshd.service"), "sshd.service");
const calls: Call[] = [];
const m = manager(fake(() => ({ stdout: LIST }), calls));
await assert.rejects(() => m.act("system", "stop", "-H"), /is not a unit's name/);
assert.equal(calls.length, 0, "nothing ran");
await m.units("system", "-x*");
assert.deepEqual(calls[0].args.slice(-2), ["--", "-x*"]);
});
test("the manifest owns the systemd package — the service manager's own, never a component module's", () => {
const m = JSON.parse(readFileSync(new URL("../module.json", import.meta.url), "utf8"));
assert.ok((m.resources ?? []).some((r: { type: string; package?: string }) => r.type === "package" && r.package === "systemd"));
assert.ok(m.capabilities.includes("package-manager"));
// networkd is a component of systemd and configures it; it never claims the package.
const networkd = JSON.parse(readFileSync(new URL("../../systemd-networkd/module.json", import.meta.url), "utf8"));
assert.ok(!(networkd.resources ?? []).some((r: { type: string; package?: string }) => r.type === "package" && r.package === "systemd"));
assert.deepEqual(m.claims[0].serves, ["units", "status", "start", "stop", "restart", "enable", "disable", "journal"]);
});