import { describe, expect, it, vi } from "vitest"; import { LaunchdNativeServiceProbe, SpawnProbeCommandRunner, SystemdNativeServiceProbe, launchdProbePlist, nativeServicePrerequisiteShellCheck, systemdRunArguments, type LaunchdProbeFileSystem, type ProbeCommandResult, type ProbeCommandRunner, } from "./serviceProbe.js"; import type { NativeServiceProbeRequest } from "./servicePlan.js"; function request(kind: "systemd" | "launchd" = "systemd"): NativeServiceProbeRequest { return { purpose: "plan-validation", backend: { kind, label: kind }, shell: { name: "zsh", executable: "/bin/zsh", source: "detected", detectedExecutable: "/bin/zsh", }, environment: { PI_WEB_CONFIG: "/home/user/config with space.json" }, workingDirectory: "/checkout with space", prerequisites: [{ id: "sessiond.command.npm", kind: "command-available", command: "npm", description: "npm is available", }], }; } function completed(status = 0, stdout = "", stderr = ""): ProbeCommandResult { return { kind: "completed", status, stdout, stderr }; } function marker(id: string, status: "satisfied" | "unsatisfied"): string { return `PI_WEB_PROBE_fixed\t${Buffer.from(id).toString("base64")}\t${status}\n`; } function queuedRunner(results: ProbeCommandResult[]): ProbeCommandRunner & { calls: { command: string; args: readonly string[]; timeoutMs: number }[] } { const calls: { command: string; args: readonly string[]; timeoutMs: number }[] = []; return { calls, run: (command, args, timeoutMs) => { calls.push({ command, args, timeoutMs }); const result = results.shift(); if (result === undefined) throw new Error(`Unexpected command: ${command} ${args.join(" ")}`); return Promise.resolve(result); }, }; } describe("systemd authoritative native-service probe", () => { it("runs the exact shell, environment, and cwd in a transient user service", async () => { const runner = queuedRunner([completed(0, `login banner\n${marker("sessiond.command.npm", "satisfied")}`)]); const probe = new SystemdNativeServiceProbe({ commandRunner: runner, createUniqueId: () => "fixed", commandTimeoutMs: 3210, }); await expect(probe.run(request())).resolves.toEqual({ kind: "completed", outcomes: [{ prerequisiteId: "sessiond.command.npm", status: "satisfied", detail: null }], }); expect(runner.calls).toHaveLength(1); expect(runner.calls[0]).toMatchObject({ command: "systemd-run", timeoutMs: 3210 }); expect(runner.calls[0]?.args).toEqual([ "--user", "--wait", "--collect", "--pipe", "--quiet", "--unit=pi-web-authoritative-probe-fixed.service", "--property=RuntimeMaxSec=15s", "--property=TimeoutStopSec=5s", "--setenv=PI_WEB_CONFIG=/home/user/config with space.json", "--working-directory=/checkout with space", "/usr/bin/env", "/bin/zsh", "-lc", expect.stringContaining("command -v 'npm'"), ]); }); it("reports requirement failures as completed and malformed output as infrastructure", async () => { const unsatisfiedRunner = queuedRunner([completed(0, marker("sessiond.command.npm", "unsatisfied"))]); const dependencies = { commandRunner: unsatisfiedRunner, createUniqueId: () => "fixed", commandTimeoutMs: 100 }; await expect(new SystemdNativeServiceProbe(dependencies).run(request())).resolves.toEqual({ kind: "completed", outcomes: [{ prerequisiteId: "sessiond.command.npm", status: "unsatisfied", detail: "npm did not resolve to an external executable in the native service environment.", }], }); const malformedRunner = queuedRunner([completed(0, "no marker here")]); await expect(new SystemdNativeServiceProbe({ ...dependencies, commandRunner: malformedRunner }).run(request())).resolves.toMatchObject({ kind: "infrastructure-failure", reason: "malformed-output", }); }); it("bounds a hung unit, cleans it up, and reports the timeout", async () => { const runner = queuedRunner([ { kind: "timeout", stdout: "", stderr: "" }, completed(0), completed(0, "not-found\n"), ]); const probe = new SystemdNativeServiceProbe({ commandRunner: runner, createUniqueId: () => "fixed", commandTimeoutMs: 100, }); await expect(probe.run(request())).resolves.toMatchObject({ kind: "infrastructure-failure", reason: "timeout", }); expect(runner.calls.map(({ command }) => command)).toEqual(["systemd-run", "systemctl", "systemctl"]); }); it("bounds a hung unit and distinguishes cleanup failure", async () => { const runner = queuedRunner([ { kind: "timeout", stdout: "", stderr: "" }, completed(0), completed(0, "loaded\n", "unit still loaded"), ]); const probe = new SystemdNativeServiceProbe({ commandRunner: runner, createUniqueId: () => "fixed", commandTimeoutMs: 100, }); const result = await probe.run(request()); expect(result).toMatchObject({ kind: "infrastructure-failure", reason: "cleanup" }); expect(result.kind === "infrastructure-failure" && result.message).toContain("unit still loaded"); expect(runner.calls.map(({ command, args }) => [command, ...args.slice(0, 3)])).toEqual([ ["systemd-run", "--user", "--wait", "--collect"], ["systemctl", "--user", "stop", "pi-web-authoritative-probe-fixed.service"], ["systemctl", "--user", "show", "pi-web-authoritative-probe-fixed.service"], ]); }); }); describe("spawn probe command runner", () => { it("bounds captured command output", async () => { const runner = new SpawnProbeCommandRunner(); const result = await runner.run( process.execPath, ["-e", "process.stdout.write('x'.repeat(2 * 1024 * 1024))"], 5_000, ); expect(result).toMatchObject({ kind: "output-limit" }); expect(result.stdout.length).toBeLessThanOrEqual(1024 * 1024); }); it("settles a timeout without waiting for inherited pipes to close", async () => { const runner = new SpawnProbeCommandRunner(); const childScript = [ "const { spawn } = require('node:child_process');", "const child = spawn(process.execPath, ['-e', 'setTimeout(() => {}, 1000)'], { stdio: ['ignore', 'inherit', 'inherit'] });", "child.unref();", ].join(" "); const startedAt = performance.now(); await expect(runner.run(process.execPath, ["-e", childScript], 20)).resolves.toMatchObject({ kind: "timeout" }); expect(performance.now() - startedAt).toBeLessThan(500); }); }); describe("launchd authoritative native-service probe", () => { it("bootstraps a uniquely labelled one-shot agent in gui/ and always cleans it up", async () => { const runner = queuedRunner([completed(0), completed(0)]); const fileSystem = launchdFileSystem({ "/tmp/probe/result.log": marker("sessiond.command.npm", "satisfied"), }); let now = 0; const probe = new LaunchdNativeServiceProbe({ commandRunner: runner, fileSystem, uid: 501, createUniqueId: () => "fixed", now: () => now, sleep: (milliseconds) => { now += milliseconds; return Promise.resolve(); }, probeTimeoutMs: 500, pollIntervalMs: 10, commandTimeoutMs: 100, }); await expect(probe.run(request("launchd"))).resolves.toMatchObject({ kind: "completed" }); expect(runner.calls.map(({ command, args }) => [command, ...args])).toEqual([ ["launchctl", "bootstrap", "gui/501", "/tmp/probe/probe.plist"], ["launchctl", "bootout", "gui/501/com.pi-web.authoritative-probe.501.fixed"], ]); expect(fileSystem.writeFile).toHaveBeenCalledWith( "/tmp/probe/probe.plist", expect.stringContaining("/bin/zsh"), 0o600, ); expect(fileSystem.writeFile).toHaveBeenCalledWith( "/tmp/probe/probe.plist", expect.stringContaining("WorkingDirectory\n /checkout with space"), 0o600, ); expect(fileSystem.writeFile).toHaveBeenCalledWith( "/tmp/probe/probe.plist", expect.stringContaining("/bin/mv '/tmp/probe/result.pending' '/tmp/probe/result.log'"), 0o600, ); expect(fileSystem.removeDirectory).toHaveBeenCalledWith("/tmp/probe"); }); it("times out deterministically, boots out the agent, and removes temporary files", async () => { const runner = queuedRunner([completed(0), completed(0)]); const fileSystem = launchdFileSystem({}); let now = 0; const probe = new LaunchdNativeServiceProbe({ commandRunner: runner, fileSystem, uid: 502, createUniqueId: () => "fixed", now: () => now, sleep: (milliseconds) => { now += milliseconds; return Promise.resolve(); }, probeTimeoutMs: 20, pollIntervalMs: 10, commandTimeoutMs: 100, }); await expect(probe.run(request("launchd"))).resolves.toMatchObject({ kind: "infrastructure-failure", reason: "timeout", }); expect(runner.calls.at(-1)).toMatchObject({ command: "launchctl", args: ["bootout", "gui/502/com.pi-web.authoritative-probe.502.fixed"], }); expect(fileSystem.removeDirectory).toHaveBeenCalledWith("/tmp/probe"); }); it("bounds a stalled result-file read before cleaning up", async () => { const runner = queuedRunner([completed(0), completed(0)]); const fileSystem = launchdFileSystem({}); fileSystem.readOptionalFile.mockReturnValueOnce(new Promise(() => undefined)); const probe = new LaunchdNativeServiceProbe({ commandRunner: runner, fileSystem, uid: 502, createUniqueId: () => "fixed", now: () => 0, sleep: () => Promise.resolve(), probeTimeoutMs: 10, pollIntervalMs: 1, commandTimeoutMs: 100, }); await expect(probe.run(request("launchd"))).resolves.toMatchObject({ kind: "infrastructure-failure", reason: "timeout", }); expect(runner.calls.map(({ args }) => args[0])).toEqual(["bootstrap", "bootout"]); expect(fileSystem.removeDirectory).toHaveBeenCalledWith("/tmp/probe"); }); it("surfaces cleanup failure instead of returning an otherwise successful probe", async () => { const runner = queuedRunner([completed(0), completed(1, "", "bootout denied")]); const fileSystem = launchdFileSystem({ "/tmp/probe/result.log": marker("sessiond.command.npm", "satisfied"), }); const probe = new LaunchdNativeServiceProbe({ commandRunner: runner, fileSystem, uid: 503, createUniqueId: () => "fixed", now: () => 0, sleep: () => Promise.resolve(), probeTimeoutMs: 20, pollIntervalMs: 10, commandTimeoutMs: 100, }); const result = await probe.run(request("launchd")); expect(result).toMatchObject({ kind: "infrastructure-failure", reason: "cleanup" }); expect(result.kind === "infrastructure-failure" && result.message).toContain("bootout denied"); expect(fileSystem.removeDirectory).toHaveBeenCalledWith("/tmp/probe"); }); it("boots out a label when bootstrap itself times out", async () => { const runner = queuedRunner([ { kind: "timeout", stdout: "", stderr: "" }, completed(0), ]); const fileSystem = launchdFileSystem({}); const probe = new LaunchdNativeServiceProbe({ commandRunner: runner, fileSystem, uid: 504, createUniqueId: () => "fixed", now: () => 0, sleep: () => Promise.resolve(), probeTimeoutMs: 20, pollIntervalMs: 10, commandTimeoutMs: 100, }); await expect(probe.run(request("launchd"))).resolves.toMatchObject({ kind: "infrastructure-failure", reason: "timeout", }); expect(runner.calls.map(({ args }) => args[0])).toEqual(["bootstrap", "bootout"]); expect(fileSystem.removeDirectory).toHaveBeenCalledWith("/tmp/probe"); }); it("treats an explicit not-loaded bootout response as successful cleanup after bootstrap fails", async () => { const runner = queuedRunner([ completed(1, "", "bootstrap denied"), completed(3, "", "Could not find service in domain"), ]); const fileSystem = launchdFileSystem({}); const probe = new LaunchdNativeServiceProbe({ commandRunner: runner, fileSystem, uid: 504, createUniqueId: () => "fixed", now: () => 0, sleep: () => Promise.resolve(), probeTimeoutMs: 20, pollIntervalMs: 10, commandTimeoutMs: 100, }); const result = await probe.run(request("launchd")); expect(result).toMatchObject({ kind: "infrastructure-failure", reason: "manager" }); expect(result.kind === "infrastructure-failure" && result.message).toContain("bootstrap denied"); expect(runner.calls.map(({ args }) => args[0])).toEqual(["bootstrap", "bootout"]); expect(fileSystem.removeDirectory).toHaveBeenCalledWith("/tmp/probe"); }); it("cleans a loaded label after malformed private result output", async () => { const runner = queuedRunner([completed(0), completed(0)]); const fileSystem = launchdFileSystem({ "/tmp/probe/result.log": "malformed result" }); const probe = new LaunchdNativeServiceProbe({ commandRunner: runner, fileSystem, uid: 505, createUniqueId: () => "fixed", now: () => 0, sleep: () => Promise.resolve(), probeTimeoutMs: 20, pollIntervalMs: 10, commandTimeoutMs: 100, }); await expect(probe.run(request("launchd"))).resolves.toMatchObject({ kind: "infrastructure-failure", reason: "malformed-output", }); expect(runner.calls.at(-1)?.args[0]).toBe("bootout"); expect(fileSystem.removeDirectory).toHaveBeenCalledWith("/tmp/probe"); }); it("cleans a loaded label when the private result cannot be read", async () => { const runner = queuedRunner([completed(0), completed(0)]); const fileSystem = launchdFileSystem({}); fileSystem.readOptionalFile.mockRejectedValueOnce(new Error("read denied")); const probe = new LaunchdNativeServiceProbe({ commandRunner: runner, fileSystem, uid: 506, createUniqueId: () => "fixed", now: () => 0, sleep: () => Promise.resolve(), probeTimeoutMs: 20, pollIntervalMs: 10, commandTimeoutMs: 100, }); const result = await probe.run(request("launchd")); expect(result).toMatchObject({ kind: "infrastructure-failure", reason: "manager" }); expect(result.kind === "infrastructure-failure" && result.message).toContain("Could not read launchd probe result"); expect(runner.calls.at(-1)?.args[0]).toBe("bootout"); expect(fileSystem.removeDirectory).toHaveBeenCalledWith("/tmp/probe"); }); it("reports temporary-file cleanup failures", async () => { const runner = queuedRunner([ completed(1, "", "bootstrap denied"), completed(3, "", "Could not find service in domain"), ]); const fileSystem = launchdFileSystem({}); fileSystem.removeDirectory.mockRejectedValueOnce(new Error("rm denied")); const probe = new LaunchdNativeServiceProbe({ commandRunner: runner, fileSystem, uid: 506, createUniqueId: () => "fixed", now: () => 0, sleep: () => Promise.resolve(), probeTimeoutMs: 20, pollIntervalMs: 10, commandTimeoutMs: 100, }); const result = await probe.run(request("launchd")); expect(result).toMatchObject({ kind: "infrastructure-failure", reason: "cleanup" }); expect(result.kind === "infrastructure-failure" && result.message).toContain("rm denied"); }); }); describe("probe service definitions", () => { it("requires external executables instead of accepting shell functions or aliases", () => { const commandRequirement = request().prerequisites[0]; if (commandRequirement === undefined) throw new Error("Expected a command prerequisite"); const bashCheck = nativeServicePrerequisiteShellCheck("bash", commandRequirement); expect(bashCheck).toContain("case \"$pi_web_probe_executable\" in */*)"); expect(bashCheck).toContain("test -f \"$pi_web_probe_executable\""); expect(bashCheck).toContain("test -x \"$pi_web_probe_executable\""); const fishCheck = nativeServicePrerequisiteShellCheck("fish", commandRequirement); expect(fishCheck).toContain("string match -q '*/*'"); expect(fishCheck).toContain("test -f $pi_web_probe_executable[1]"); expect(fishCheck).toContain("test -x $pi_web_probe_executable[1]"); }); it("invokes the resolved external Node executable for version checks", () => { const check = nativeServicePrerequisiteShellCheck("zsh", { id: "sessiond.node", kind: "node-version", command: "node", minimumMajor: 22, description: "node >= 22", }); expect(check).toContain("\"$pi_web_probe_executable\" '-e'"); expect(check).not.toContain("&& node -e"); }); it("requires bundled entrypoints to be readable regular files", () => { const check = nativeServicePrerequisiteShellCheck("bash", { id: "sessiond.entrypoint", kind: "readable-file", path: "/package/server.js", description: "entrypoint", }); expect(check).toBe("test -f '/package/server.js' && test -r '/package/server.js'"); }); it("renders backend inputs without inheriting the caller PATH", () => { const probeRequest = request(); const systemdArguments = systemdRunArguments(probeRequest, "probe.service", "echo ok"); expect(systemdArguments.some((argument) => argument.includes("PATH="))).toBe(false); const plist = launchdProbePlist(probeRequest, "com.example.probe", "echo ok", "/tmp/out", "/tmp/err"); expect(plist).not.toContain("PATH"); expect(plist).toContain("PI_WEB_CONFIG"); expect(plist).toContain("HardResourceLimits"); }); it("escapes manager-side substitutions in the systemd probe payload", () => { const args = systemdRunArguments(request(), "probe.service", "test -r '/tmp/$HOME/%h'"); expect(args.at(-1)).toBe("test -r '/tmp/$$HOME/%h'"); }); }); function launchdFileSystem(contents: Record): LaunchdProbeFileSystem & { writeFile: ReturnType>; readOptionalFile: ReturnType>; removeDirectory: ReturnType>; } { const writeFileMock = vi.fn(() => Promise.resolve()); const readOptionalFileMock = vi.fn((path) => Promise.resolve(contents[path] ?? null)); const removeDirectoryMock = vi.fn(() => Promise.resolve()); return { createTemporaryDirectory: () => Promise.resolve("/tmp/probe"), writeFile: writeFileMock, readOptionalFile: readOptionalFileMock, removeDirectory: removeDirectoryMock, }; }