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 }); }); });