/**
* Unit tests for macOS launchd service management.
*
* Mocks filesystem and child_process to test logic without touching launchd.
* Covers: plist generation, path resolution, status parsing, install/uninstall
* flows, restart/stop commands, readInstalledPlist parsing, and error paths.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// ── Mocks ──────────────────────────────────────────────────────────────────
const mockExistsSync = vi.fn<(path: string) => boolean>();
const mockMkdirSync = vi.fn();
const mockWriteFileSync = vi.fn();
const mockReadFileSync = vi.fn<(path: string, encoding: string) => string>();
const mockRealpathSync = vi.fn<(path: string) => string>();
const mockUnlinkSync = vi.fn();
const mockExecSync = vi.fn<(cmd: string, opts?: object) => string>();
const mockHomedir = vi.fn(() => "/Users/testuser");
vi.mock("node:fs", () => ({
existsSync: (...args: unknown[]) => mockExistsSync(args[0] as string),
mkdirSync: (...args: unknown[]) => mockMkdirSync(...args),
writeFileSync: (...args: unknown[]) => mockWriteFileSync(...args),
readFileSync: (...args: unknown[]) => mockReadFileSync(args[0] as string, args[1] as string),
realpathSync: (...args: unknown[]) => mockRealpathSync(args[0] as string),
unlinkSync: (...args: unknown[]) => mockUnlinkSync(...args),
}));
vi.mock("node:child_process", () => ({
execSync: (...args: unknown[]) => mockExecSync(args[0] as string, args[1] as object | undefined),
}));
vi.mock("node:os", () => ({
homedir: () => mockHomedir(),
}));
// Stub process.getuid to return a fake uid on all platforms
const originalGetuid = process.getuid;
beforeEach(() => {
process.getuid = () => 501;
mockUnlinkSync.mockImplementation(() => undefined);
mockRealpathSync.mockImplementation((path: string) => path);
mockReadFileSync.mockImplementation((path: string) => {
if (path.endsWith("package.json")) {
return JSON.stringify({ engines: { node: ">=23.6.0" } });
}
return "";
});
mockExecSync.mockImplementation((cmd: string) => {
if (cmd.includes("--version")) {
return "v25.0.0";
}
return "";
});
});
afterEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
process.getuid = originalGetuid;
});
// Import after mocks are in place
import {
getServiceStatus,
installService,
readInstalledPlist,
restartService,
stopService,
uninstallService,
} from "../src/launchd.js";
// ── Plist path ─────────────────────────────────────────────────────────────
describe("plist path resolution", () => {
it("derives plist path from homedir", () => {
mockExistsSync.mockReturnValue(false);
const status = getServiceStatus();
expect(status.plistPath).toBe(
"/Users/testuser/Library/LaunchAgents/dev.chaosdonkey.oppi.plist",
);
expect(status.label).toBe("dev.chaosdonkey.oppi");
});
});
// ── Plist XML generation (via installService) ──────────────────────────────
describe("plist XML generation", () => {
function captureWrittenPlist(dataDir?: string): string {
mockExistsSync.mockImplementation((p: string) => {
if (p === "/opt/homebrew/bin/node") return true;
if (p === "/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js") return true;
if (p.endsWith(".plist")) return false;
return false;
});
installService(dataDir);
const writeCall = mockWriteFileSync.mock.calls[0];
return writeCall[1] as string;
}
it("generates valid plist XML with correct structure", () => {
const xml = captureWrittenPlist("/tmp/test-oppi");
expect(xml).toContain('');
expect(xml).toContain("');
expect(xml).toContain("Label");
expect(xml).toContain("dev.chaosdonkey.oppi");
});
it("sets ProgramArguments with runtime, CLI, serve, and data-dir", () => {
const xml = captureWrittenPlist("/tmp/test-oppi");
expect(xml).toContain("ProgramArguments");
expect(xml).toContain("/opt/homebrew/bin/node");
expect(xml).toContain(
"/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js",
);
expect(xml).toContain("serve");
expect(xml).toContain("--data-dir");
expect(xml).toContain("/tmp/test-oppi");
});
it("includes KeepAlive with SuccessfulExit=false", () => {
const xml = captureWrittenPlist("/tmp/test-oppi");
expect(xml).toContain("KeepAlive");
expect(xml).toContain("SuccessfulExit");
expect(xml).toContain("");
});
it("includes RunAtLoad", () => {
const xml = captureWrittenPlist("/tmp/test-oppi");
expect(xml).toContain("RunAtLoad");
expect(xml).toContain("");
});
it("sets PATH and OPPI_DATA_DIR without a second runtime owner", () => {
const xml = captureWrittenPlist("/tmp/test-oppi");
expect(xml).toContain("EnvironmentVariables");
expect(xml).toContain("PATH");
expect(xml).toContain("/opt/homebrew/bin");
expect(xml).toContain("OPPI_DATA_DIR");
expect(xml).toContain("/tmp/test-oppi");
expect(xml).not.toContain("OPPI_RUNTIME_BIN");
});
it("sets log paths to dataDir/server.log", () => {
const xml = captureWrittenPlist("/tmp/test-oppi");
expect(xml).toContain("StandardOutPath");
expect(xml).toContain("StandardErrorPath");
expect(xml).toContain("/tmp/test-oppi/server.log");
});
it("includes ThrottleInterval and ProcessType", () => {
const xml = captureWrittenPlist("/tmp/test-oppi");
expect(xml).toContain("ThrottleInterval");
expect(xml).toContain("5");
expect(xml).toContain("ProcessType");
expect(xml).toContain("Standard");
});
it("sets WorkingDirectory to homedir", () => {
const xml = captureWrittenPlist("/tmp/test-oppi");
expect(xml).toContain("WorkingDirectory");
expect(xml).toContain("/Users/testuser");
});
it("defaults dataDir to ~/.config/oppi when not provided", () => {
const xml = captureWrittenPlist(undefined);
expect(xml).toContain("OPPI_DATA_DIR");
expect(xml).toContain("/Users/testuser/.config/oppi");
});
});
// ── Runtime resolution ─────────────────────────────────────────────────────
describe("runtime resolution", () => {
it("prefers Homebrew node", () => {
mockExistsSync.mockImplementation((p: string) => {
if (p === "/opt/homebrew/bin/node") return true;
if (p === "/usr/local/bin/node") return true;
if (p === "/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js") return true;
if (p.endsWith(".plist")) return false;
return false;
});
const result = installService("/tmp/data");
expect(result.runtimePath).toBe("/opt/homebrew/bin/node");
});
it("falls back to /usr/local/bin/node when Homebrew node is unavailable", () => {
mockExistsSync.mockImplementation((p: string) => {
if (p === "/opt/homebrew/bin/node") return false;
if (p === "/usr/local/bin/node") return true;
if (p === "/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js") return true;
if (p.endsWith(".plist")) return false;
return false;
});
const result = installService("/tmp/data");
expect(result.runtimePath).toBe("/usr/local/bin/node");
});
it("rejects nodes older than the server minimum", () => {
mockExistsSync.mockImplementation((p: string) => {
if (p === "/opt/homebrew/bin/node") return true;
if (p === "/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js") return true;
if (p.endsWith(".plist")) return false;
return false;
});
mockExecSync.mockImplementation((cmd: string) => {
if (cmd.includes("--version")) {
return "v20.11.1";
}
return "";
});
const result = installService("/tmp/data");
expect(result.ok).toBe(false);
expect(result.message).toContain("Node.js 20.11.1 found");
expect(result.message).toContain("23.6.0 or newer");
});
it("returns error when no runtime found", () => {
mockExistsSync.mockImplementation((p: string) => {
if (p === "/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js") return true;
return false;
});
const result = installService("/tmp/data");
expect(result.ok).toBe(false);
expect(result.message).toContain("Node.js 23.6.0 or newer not found");
});
});
// ── CLI resolution ─────────────────────────────────────────────────────────
describe("CLI resolution", () => {
it("returns error when CLI not found", () => {
// Runtime exists, but no CLI
mockExistsSync.mockImplementation((p: string) => {
if (p === "/opt/homebrew/bin/node") return true;
return false;
});
const result = installService("/tmp/data");
expect(result.ok).toBe(false);
expect(result.message).toContain("Oppi CLI not found");
});
it("resolves the globally installed npm CLI", () => {
mockExistsSync.mockImplementation((p: string) => {
if (p === "/opt/homebrew/bin/node") return true;
if (p === "/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js") return true;
if (p.endsWith(".plist")) return false;
return false;
});
const result = installService("/tmp/data");
expect(result.ok).toBe(true);
expect(result.cliPath).toBe("/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js");
});
it("resolves the current npm CLI symlink before fallback locations", () => {
const originalArgv1 = process.argv[1];
process.argv[1] = "/tmp/npm-prefix/bin/oppi";
mockRealpathSync.mockImplementation((p: string) => {
if (p === "/tmp/npm-prefix/bin/oppi") {
return "/tmp/npm-prefix/lib/node_modules/oppi-server/dist/src/cli.js";
}
return p;
});
mockExistsSync.mockImplementation((p: string) => {
if (p === "/opt/homebrew/bin/node") return true;
if (p === "/tmp/npm-prefix/bin/oppi") return true;
if (p === "/tmp/npm-prefix/lib/node_modules/oppi-server/dist/src/cli.js") return true;
if (p === "/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js") return true;
if (p.endsWith(".plist")) return false;
return false;
});
try {
const result = installService("/tmp/data");
expect(result.ok).toBe(true);
expect(result.cliPath).toBe("/tmp/npm-prefix/bin/oppi");
} finally {
process.argv[1] = originalArgv1;
}
});
});
// ── installService flow ────────────────────────────────────────────────────
describe("installService", () => {
function setupValidInstall() {
mockExistsSync.mockImplementation((p: string) => {
if (p === "/opt/homebrew/bin/node") return true;
if (p === "/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js") return true;
if (p.endsWith(".plist")) return false;
return false;
});
}
it("creates LaunchAgents directory", () => {
setupValidInstall();
installService("/tmp/data");
expect(mockMkdirSync).toHaveBeenCalledWith("/Users/testuser/Library/LaunchAgents", {
recursive: true,
});
});
it("writes plist with mode 0o644", () => {
setupValidInstall();
installService("/tmp/data");
expect(mockWriteFileSync).toHaveBeenCalledWith(
"/Users/testuser/Library/LaunchAgents/dev.chaosdonkey.oppi.plist",
expect.any(String),
{ mode: 0o644 },
);
});
it("calls launchctl bootstrap after writing plist", () => {
setupValidInstall();
installService("/tmp/data");
const bootstrapCall = mockExecSync.mock.calls.find(([cmd]) =>
(cmd as string).includes("bootstrap"),
);
expect(bootstrapCall).toBeDefined();
expect(bootstrapCall![0]).toBe(
"launchctl bootstrap gui/501 /Users/testuser/Library/LaunchAgents/dev.chaosdonkey.oppi.plist",
);
});
it("bootouts existing plist before installing", () => {
mockExistsSync.mockImplementation((p: string) => {
if (p === "/opt/homebrew/bin/node") return true;
if (p === "/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js") return true;
if (p === "/Users/testuser/Library/LaunchAgents/dev.chaosdonkey.oppi.plist") return true;
return false;
});
installService("/tmp/data");
expect(mockExecSync).toHaveBeenCalledWith(
"launchctl bootout gui/501 /Users/testuser/Library/LaunchAgents/dev.chaosdonkey.oppi.plist 2>/dev/null",
{ stdio: "pipe" },
);
});
it("disables legacy dev.chenda LaunchAgent before installing", () => {
mockExistsSync.mockImplementation((p: string) => {
if (p === "/opt/homebrew/bin/node") return true;
if (p === "/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js") return true;
if (p === "/Users/testuser/Library/LaunchAgents/dev.chenda.oppi.plist") return true;
if (p === "/Users/testuser/Library/LaunchAgents/dev.chaosdonkey.oppi.plist") return false;
return false;
});
const result = installService("/tmp/data");
expect(result.ok).toBe(true);
expect(result.legacyRemoved).toEqual([
"/Users/testuser/Library/LaunchAgents/dev.chenda.oppi.plist",
]);
expect(mockExecSync).toHaveBeenCalledWith(
"launchctl bootout gui/501/dev.chenda.oppi 2>/dev/null",
{ stdio: "pipe" },
);
expect(mockExecSync).toHaveBeenCalledWith(
"launchctl bootout gui/501 /Users/testuser/Library/LaunchAgents/dev.chenda.oppi.plist 2>/dev/null",
{ stdio: "pipe" },
);
expect(mockUnlinkSync).toHaveBeenCalledWith(
"/Users/testuser/Library/LaunchAgents/dev.chenda.oppi.plist",
);
});
it("fails install when legacy LaunchAgent plist cannot be removed", () => {
mockExistsSync.mockImplementation((p: string) => {
if (p === "/opt/homebrew/bin/node") return true;
if (p === "/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js") return true;
if (p === "/Users/testuser/Library/LaunchAgents/dev.chenda.oppi.plist") return true;
return false;
});
mockUnlinkSync.mockImplementation(() => {
throw new Error("EACCES: permission denied");
});
const result = installService("/tmp/data");
expect(result.ok).toBe(false);
expect(result.message).toContain("Failed to disable legacy LaunchAgent");
expect(result.message).toContain("EACCES");
});
it("handles error 37 (already loaded) by kickstarting", () => {
setupValidInstall();
mockExecSync.mockImplementation((cmd: string) => {
if (cmd.includes("--version")) {
return "v25.0.0";
}
if (cmd.includes("bootstrap")) {
throw new Error("37: Service is already loaded");
}
return "";
});
const result = installService("/tmp/data");
expect(result.ok).toBe(true);
const kickstartCall = mockExecSync.mock.calls.find(([cmd]) =>
(cmd as string).includes("kickstart"),
);
expect(kickstartCall).toBeDefined();
expect(kickstartCall![0]).toBe("launchctl kickstart -k gui/501/dev.chaosdonkey.oppi");
});
it("returns error on non-37 bootstrap failure", () => {
setupValidInstall();
mockExecSync.mockImplementation((cmd: string) => {
if (cmd.includes("--version")) {
return "v25.0.0";
}
if (cmd.includes("bootstrap")) {
throw new Error("5: Some other error");
}
return "";
});
const result = installService("/tmp/data");
expect(result.ok).toBe(false);
expect(result.message).toContain("Failed to load LaunchAgent");
expect(result.message).toContain("5: Some other error");
});
it("returns ok with paths on success", () => {
setupValidInstall();
const result = installService("/tmp/data");
expect(result.ok).toBe(true);
expect(result.message).toContain("installed and started");
expect(result.runtimePath).toBe("/opt/homebrew/bin/node");
expect(result.cliPath).toBe("/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js");
});
});
// ── uninstallService ───────────────────────────────────────────────────────
describe("uninstallService", () => {
it("returns ok when plist not installed", () => {
mockExistsSync.mockReturnValue(false);
const result = uninstallService();
expect(result.ok).toBe(true);
expect(result.message).toContain("not installed");
});
it("bootouts and removes plist when installed", () => {
mockExistsSync.mockReturnValue(true);
mockExecSync.mockReturnValue("");
const result = uninstallService();
expect(result.ok).toBe(true);
expect(result.message).toContain("uninstalled");
const bootoutCall = mockExecSync.mock.calls.find(([cmd]) =>
(cmd as string).includes("bootout"),
);
expect(bootoutCall).toBeDefined();
expect(mockUnlinkSync).toHaveBeenCalledWith(
"/Users/testuser/Library/LaunchAgents/dev.chaosdonkey.oppi.plist",
);
});
it("still removes plist if bootout fails", () => {
mockExistsSync.mockReturnValue(true);
mockExecSync.mockImplementation(() => {
throw new Error("already unloaded");
});
const result = uninstallService();
expect(result.ok).toBe(true);
expect(mockUnlinkSync).toHaveBeenCalled();
});
it("returns error if plist removal fails", () => {
mockExistsSync.mockReturnValue(true);
mockExecSync.mockReturnValue("");
mockUnlinkSync.mockImplementation(() => {
throw new Error("EACCES: permission denied");
});
const result = uninstallService();
expect(result.ok).toBe(false);
expect(result.message).toContain("Failed to remove plist");
expect(result.message).toContain("EACCES");
});
});
// ── restartService ─────────────────────────────────────────────────────────
describe("restartService", () => {
it("returns error when plist not installed", () => {
mockExistsSync.mockReturnValue(false);
const result = restartService();
expect(result.ok).toBe(false);
expect(result.message).toContain("not installed");
});
it("calls kickstart -k with correct label", () => {
mockExistsSync.mockReturnValue(true);
mockExecSync.mockReturnValue("");
const result = restartService();
expect(result.ok).toBe(true);
expect(result.message).toContain("restarted");
expect(mockExecSync).toHaveBeenCalledWith(
"launchctl kickstart -k gui/501/dev.chaosdonkey.oppi",
{ stdio: "pipe" },
);
});
it("returns error on kickstart failure", () => {
mockExistsSync.mockReturnValue(true);
mockExecSync.mockImplementation(() => {
throw new Error("kickstart: no such process");
});
const result = restartService();
expect(result.ok).toBe(false);
expect(result.message).toContain("Restart failed");
});
});
// ── stopService ────────────────────────────────────────────────────────────
describe("stopService", () => {
it("returns error when plist not installed", () => {
mockExistsSync.mockReturnValue(false);
const result = stopService();
expect(result.ok).toBe(false);
expect(result.message).toContain("not installed");
});
it("bootouts by label on stop", () => {
mockExistsSync.mockReturnValue(true);
mockExecSync.mockReturnValue("");
const result = stopService();
expect(result.ok).toBe(true);
expect(result.message).toContain("stopped");
expect(mockExecSync).toHaveBeenCalledWith("launchctl bootout gui/501/dev.chaosdonkey.oppi", {
stdio: "pipe",
});
});
it("treats 'No such process' as success (already stopped)", () => {
mockExistsSync.mockReturnValue(true);
mockExecSync.mockImplementation(() => {
throw new Error("No such process");
});
const result = stopService();
expect(result.ok).toBe(true);
expect(result.message).toContain("not running");
});
it("treats 'Could not find' as success (already stopped)", () => {
mockExistsSync.mockReturnValue(true);
mockExecSync.mockImplementation(() => {
throw new Error("Could not find service");
});
const result = stopService();
expect(result.ok).toBe(true);
expect(result.message).toContain("not running");
});
it("returns error on unexpected stop failure", () => {
mockExistsSync.mockReturnValue(true);
mockExecSync.mockImplementation(() => {
throw new Error("unexpected launchctl error");
});
const result = stopService();
expect(result.ok).toBe(false);
expect(result.message).toContain("Stop failed");
});
});
// ── getServiceStatus ───────────────────────────────────────────────────────
describe("getServiceStatus", () => {
it("returns not installed when plist does not exist", () => {
mockExistsSync.mockReturnValue(false);
const status = getServiceStatus();
expect(status.installed).toBe(false);
expect(status.running).toBe(false);
expect(status.pid).toBeNull();
});
it("parses PID from launchctl print output", () => {
mockExistsSync.mockReturnValue(true);
mockExecSync.mockReturnValue(
[
"dev.chaosdonkey.oppi = {",
" active count = 1",
" pid = 12345",
" state = running",
"}",
].join("\n"),
);
const status = getServiceStatus();
expect(status.installed).toBe(true);
expect(status.running).toBe(true);
expect(status.pid).toBe(12345);
});
it("detects running state even without PID line", () => {
mockExistsSync.mockReturnValue(true);
mockExecSync.mockReturnValue(
["dev.chaosdonkey.oppi = {", " active count = 1", " state = running", "}"].join("\n"),
);
const status = getServiceStatus();
expect(status.installed).toBe(true);
expect(status.running).toBe(true);
expect(status.pid).toBeNull();
});
it("treats pid = 0 as not running", () => {
mockExistsSync.mockReturnValue(true);
mockExecSync.mockReturnValue(
["dev.chaosdonkey.oppi = {", " pid = 0", " state = waiting", "}"].join("\n"),
);
const status = getServiceStatus();
expect(status.installed).toBe(true);
expect(status.running).toBe(false);
expect(status.pid).toBe(0);
});
it("handles launchctl print failure (service not loaded)", () => {
mockExistsSync.mockReturnValue(true);
mockExecSync.mockImplementation(() => {
throw new Error("Could not find service");
});
const status = getServiceStatus();
expect(status.installed).toBe(true);
expect(status.running).toBe(false);
expect(status.pid).toBeNull();
});
it("always includes label and plist path", () => {
mockExistsSync.mockReturnValue(false);
const status = getServiceStatus();
expect(status.label).toBe("dev.chaosdonkey.oppi");
expect(status.plistPath).toBe(
"/Users/testuser/Library/LaunchAgents/dev.chaosdonkey.oppi.plist",
);
});
});
// ── readInstalledPlist ─────────────────────────────────────────────────────
describe("readInstalledPlist", () => {
it("returns null when plist does not exist", () => {
mockExistsSync.mockReturnValue(false);
expect(readInstalledPlist()).toBeNull();
});
it("parses runtime, CLI, and data-dir from plist XML", () => {
const plistXml = `
ProgramArguments
/opt/homebrew/bin/node
/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js
serve
--data-dir
/Users/testuser/.config/oppi
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(plistXml);
const parsed = readInstalledPlist();
expect(parsed).not.toBeNull();
expect(parsed!.runtimePath).toBe("/opt/homebrew/bin/node");
expect(parsed!.cliPath).toBe("/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js");
expect(parsed!.dataDir).toBe("/Users/testuser/.config/oppi");
});
it("returns null when ProgramArguments has fewer than 5 entries", () => {
const plistXml = `
ProgramArguments
/opt/homebrew/bin/node
serve
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(plistXml);
expect(readInstalledPlist()).toBeNull();
});
it("returns null when readFileSync throws", () => {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockImplementation(() => {
throw new Error("EACCES: permission denied");
});
expect(readInstalledPlist()).toBeNull();
});
it("returns null when plist has no ProgramArguments key", () => {
const plistXml = `
Label
dev.chaosdonkey.oppi
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(plistXml);
expect(readInstalledPlist()).toBeNull();
});
});
// ── uid() edge case ────────────────────────────────────────────────────────
describe("uid unavailable", () => {
it("installService returns error when getuid is not available", () => {
// Remove getuid to simulate non-macOS
process.getuid = undefined as unknown as () => number;
mockExistsSync.mockImplementation((p: string) => {
if (p === "/opt/homebrew/bin/node") return true;
if (p === "/opt/homebrew/lib/node_modules/oppi-server/dist/src/cli.js") return true;
// No existing plist, so bootout path is skipped — uid() is hit at bootstrap
if (p.endsWith(".plist")) return false;
return false;
});
const result = installService("/tmp/data");
expect(result.ok).toBe(false);
expect(result.message).toContain("uid() not available");
});
});