import { describe, expect, it } from 'bun:test';
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
getReceiverUnitPath,
installReceiverDaemon,
planReceiverInstall,
renderReceiverLaunchdPlist,
renderReceiverSystemdUnit,
uninstallReceiverDaemon,
} from './receiver-daemon';
const SECRET = 'a1b2c3d4e5f6secret';
// resolveCeliloPath existence-checks explicit overrides, so plan tests need
// a path that exists — a fake binary in a temp dir.
const FAKE_CELOLILO = join(mkdtempSync(join(tmpdir(), 'celilo-fake-bin-')), 'celilo');
writeFileSync(FAKE_CELOLILO, '#!/bin/sh\n', { mode: 0o755 });
describe('renderReceiverSystemdUnit', () => {
it('runs `subscribers serve` with --no-dispatch and the secret as an Environment line', () => {
const out = renderReceiverSystemdUnit({
celiloPath: FAKE_CELOLILO,
port: 8123,
secret: SECRET,
scope: 'user',
dispatch: false,
});
expect(out).toContain('subscribers serve --port 8123 --no-dispatch');
expect(out).toContain(`Environment=CELILO_BUS_SECRET=${SECRET}`);
expect(out).toContain('Restart=on-failure');
expect(out).toContain('WantedBy=default.target');
expect(out).not.toContain('User=');
});
it('honors the port and a --dispatch render for boxes without the events daemon', () => {
const out = renderReceiverSystemdUnit({
celiloPath: '/c',
port: 9999,
secret: SECRET,
scope: 'user',
dispatch: true,
});
expect(out).toContain('ExecStart=/c subscribers serve --port 9999');
expect(out).not.toContain('--no-dispatch');
});
it('system scope sets an explicit User= and multi-user.target', () => {
const out = renderReceiverSystemdUnit({
celiloPath: FAKE_CELOLILO,
port: 8123,
secret: SECRET,
scope: 'system',
runAsUser: 'celilo',
dispatch: false,
});
expect(out).toContain('User=celilo');
expect(out).toContain('WantedBy=multi-user.target');
expect(out).toContain('journalctl -u celilo-build-bus-receiver.service');
expect(out).not.toContain('journalctl --user');
});
});
describe('renderReceiverLaunchdPlist', () => {
it('carries the secret under EnvironmentVariables and --no-dispatch by default', () => {
const out = renderReceiverLaunchdPlist({
celiloPath: FAKE_CELOLILO,
port: 8123,
secret: SECRET,
scope: 'system',
home: '/root',
dispatch: false,
});
expect(out).toContain('Label');
expect(out).toContain('com.celilo.build-bus-receiver');
expect(out).toContain('CELILO_BUS_SECRET');
expect(out).toContain(`${SECRET}`);
expect(out).toContain('--no-dispatch');
expect(out).toContain('subscribers');
});
});
describe('getReceiverUnitPath', () => {
it('names the unit distinctly from the dispatcher unit in every platform/scope', () => {
expect(getReceiverUnitPath('linux', '/home/op', 'user')).toBe(
join('/home/op/.config/systemd/user', 'celilo-build-bus-receiver.service'),
);
expect(getReceiverUnitPath('linux', '/root', 'system')).toBe(
'/etc/systemd/system/celilo-build-bus-receiver.service',
);
expect(getReceiverUnitPath('darwin', '/root', 'system')).toBe(
'/Library/LaunchDaemons/com.celilo.build-bus-receiver.plist',
);
expect(getReceiverUnitPath('darwin', '/home/op', 'user')).toBe(
'/home/op/Library/LaunchAgents/com.celilo.build-bus-receiver.plist',
);
});
});
describe('planReceiverInstall', () => {
it('requires a secret and refuses to invent one', () => {
expect(() => planReceiverInstall({ secret: undefined, home: '/h' })).toThrow(/secret/i);
expect(() => planReceiverInstall({ secret: '', home: '/h' })).toThrow(/secret/i);
});
it('plans a user-scope unit that runs the receiver receiver-only by default', () => {
const plan = planReceiverInstall({
secret: SECRET,
home: '/home/op',
platform: 'linux',
celiloPath: FAKE_CELOLILO,
});
expect(plan.platform).toBe('linux');
expect(plan.scope).toBe('user');
expect(plan.unitContent).toContain('--no-dispatch');
expect(plan.unitContent).toContain(`CELILO_BUS_SECRET=${SECRET}`);
});
it('plans a system-scope unit with an explicit run-as user', () => {
const plan = planReceiverInstall({
secret: SECRET,
platform: 'linux',
scope: 'system',
home: '/root',
runAsUser: 'celilo',
celiloPath: FAKE_CELOLILO,
});
expect(plan.unitContent).toContain('User=celilo');
});
it('system scope falls back to the bus-dir owner when no run-as user is given', () => {
// events-daemon's resolveRunAsUser is the shared seam; here we only pin
// that the receiver unit passes a runAsUser through rather than dropping it.
const plan = planReceiverInstall({
secret: SECRET,
platform: 'linux',
scope: 'system',
home: '/root',
runAsUser: 'celilo',
celiloPath: FAKE_CELOLILO,
});
expect(plan.runAsUser).toBe('celilo');
});
});
describe('installReceiverDaemon / uninstallReceiverDaemon', () => {
const home = mkdtempSync(join(tmpdir(), 'celilo-receiver-daemon-'));
it('writes the unit file mode 0600 (it carries a secret) and idempotently rewrites', () => {
// celiloPath is passed explicitly: the unit under test must not
// consult the ambient PATH (resolveCeliloPath throws where no global
// celilo is installed — forge runners). Every planReceiverInstall test
// above injects FAKE_CELOLILO; the install path resolves the same way.
const first = installReceiverDaemon({
secret: SECRET,
platform: 'linux',
scope: 'user',
home,
celiloPath: FAKE_CELOLILO,
});
expect(existsSync(first.unitPath)).toBe(true);
expect(statSync(first.unitPath).mode & 0o777).toBe(0o600);
const content = readFileSync(first.unitPath, 'utf-8');
expect(content).toContain('--no-dispatch');
expect(content).toContain(`CELILO_BUS_SECRET=${SECRET}`);
// Idempotent second install.
const second = installReceiverDaemon({
secret: SECRET,
platform: 'linux',
scope: 'user',
home,
celiloPath: FAKE_CELOLILO,
});
expect(second.unitPath).toBe(first.unitPath);
const removed = uninstallReceiverDaemon({ platform: 'linux', scope: 'user', home });
expect(removed.removed).toBe(true);
expect(existsSync(first.unitPath)).toBe(false);
});
it('uninstall of a missing unit reports removed=false', () => {
const empty = mkdtempSync(join(tmpdir(), 'celilo-receiver-daemon-'));
const result = uninstallReceiverDaemon({ platform: 'linux', scope: 'user', home: empty });
expect(result.removed).toBe(false);
rmSync(empty, { recursive: true, force: true });
});
});