import { createServer, type Server } from 'node:net'; import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { applyBrokerSupervisor, checkBrokerSupervisorHealth, isBrokerSupervisorHealthy, resolveBrokerSupervisorPaths, resolveLeaseBrokerSocketPath, type BrokerSupervisorPaths, } from './broker-supervisor.js'; const REAL_FRAMEWORK_ROOT = new URL('../../framework/', import.meta.url).pathname; const cleanupDirs: string[] = []; const cleanupServers: Server[] = []; async function tempDir(prefix: string): Promise { const dir = await mkdtemp(join(tmpdir(), prefix)); cleanupDirs.push(dir); return dir; } afterEach(async () => { for (const server of cleanupServers.splice(0)) { await new Promise((resolve) => server.close(() => resolve())); } for (const dir of cleanupDirs.splice(0)) { await rm(dir, { recursive: true, force: true }); } }); describe('resolveLeaseBrokerSocketPath', () => { it('honors an explicit MOSAIC_LEASE_BROKER_SOCKET override', () => { expect(resolveLeaseBrokerSocketPath({ MOSAIC_LEASE_BROKER_SOCKET: '/tmp/explicit.sock' })).toBe( '/tmp/explicit.sock', ); }); it('falls back to $XDG_RUNTIME_DIR/mosaic-lease/broker.sock', () => { expect(resolveLeaseBrokerSocketPath({ XDG_RUNTIME_DIR: '/run/user/1000' })).toBe( join('/run/user/1000', 'mosaic-lease', 'broker.sock'), ); }); it('falls back to /run/user//mosaic-lease/broker.sock as a last resort', () => { expect(resolveLeaseBrokerSocketPath({}, 4242)).toBe( join('/run/user', '4242', 'mosaic-lease', 'broker.sock'), ); }); it('prefers the explicit override over XDG_RUNTIME_DIR', () => { expect( resolveLeaseBrokerSocketPath({ MOSAIC_LEASE_BROKER_SOCKET: '/explicit.sock', XDG_RUNTIME_DIR: '/run/user/1000', }), ).toBe('/explicit.sock'); }); }); describe('resolveBrokerSupervisorPaths', () => { it('colocates the state file next to the resolved socket', () => { const paths = resolveBrokerSupervisorPaths({ mosaicHome: '/home/x/.config/mosaic', frameworkRoot: '/repo/framework', env: { XDG_RUNTIME_DIR: '/run/user/1000' }, }); expect(paths.socketPath).toBe(join('/run/user/1000', 'mosaic-lease', 'broker.sock')); expect(paths.statePath).toBe(join('/run/user/1000', 'mosaic-lease', 'state.json')); }); it('targets the systemd --user dir under the given home, not mosaicHome', () => { const paths = resolveBrokerSupervisorPaths({ mosaicHome: '/somewhere-else/.config/mosaic', frameworkRoot: '/repo/framework', homeDir: '/home/canary', env: {}, uid: 0, }); expect(paths.systemdUserDir).toBe(join('/home/canary', '.config', 'systemd', 'user')); expect(paths.unitTargetPath).toBe( join('/home/canary', '.config', 'systemd', 'user', 'mosaic-lease-broker.service'), ); }); it('is a pure function: identical options resolve to identical paths', () => { const options = { mosaicHome: '/h/.config/mosaic', frameworkRoot: '/repo/framework', env: { XDG_RUNTIME_DIR: '/run/user/1000' }, }; expect(resolveBrokerSupervisorPaths(options)).toEqual(resolveBrokerSupervisorPaths(options)); }); }); describe('applyBrokerSupervisor', () => { async function fakePaths(): Promise { const home = await tempDir('mosaic-broker-supervisor-home-'); const mosaicHome = join(home, '.config', 'mosaic'); const runtimeDir = await tempDir('mosaic-broker-supervisor-runtime-'); return resolveBrokerSupervisorPaths({ mosaicHome, frameworkRoot: REAL_FRAMEWORK_ROOT, homeDir: home, env: { XDG_RUNTIME_DIR: runtimeDir }, }); } it('renders a unit that references the installed wrapper script and hardens the runtime dir', async () => { const paths = await fakePaths(); const unitSource = await readFile(paths.unitSourcePath, 'utf8'); expect(unitSource).toContain('ExecStart='); expect(unitSource).toContain('%h/.config/mosaic/tools/lease-broker/start-lease-broker.sh'); expect(unitSource).toContain('RuntimeDirectory=mosaic-lease'); expect(unitSource).toContain('RuntimeDirectoryMode=0700'); expect(unitSource).toContain('Restart=on-failure'); expect(unitSource).toContain('WantedBy=default.target'); // No ambient environment file preload, matching the other fleet units' // strict-parsing convention. expect(unitSource).not.toMatch(/^Environment(File)?=/m); }); it('materializes the unit, wrapper script, and daemon sources on first apply', async () => { const paths = await fakePaths(); const result = await applyBrokerSupervisor(paths); expect(result.installedFiles).toContain(paths.unitTargetPath); expect(result.installedFiles).toContain(paths.wrapperTargetPath); for (const target of paths.daemonTargetPaths) { expect(result.installedFiles).toContain(target); } const unitTargetContent = await readFile(paths.unitTargetPath, 'utf8'); const unitSourceContent = await readFile(paths.unitSourcePath, 'utf8'); expect(unitTargetContent).toBe(unitSourceContent); const wrapperMode = (await stat(paths.wrapperTargetPath)).mode & 0o777; expect(wrapperMode).toBe(0o755); for (const target of paths.daemonTargetPaths) { await expect(stat(target)).resolves.toBeDefined(); } }); it('is idempotent: applying twice reproduces identical files with no error', async () => { const paths = await fakePaths(); await applyBrokerSupervisor(paths); const firstUnit = await readFile(paths.unitTargetPath, 'utf8'); const firstWrapper = await readFile(paths.wrapperTargetPath, 'utf8'); const firstWrapperMode = (await stat(paths.wrapperTargetPath)).mode & 0o777; await expect(applyBrokerSupervisor(paths)).resolves.toBeDefined(); const secondUnit = await readFile(paths.unitTargetPath, 'utf8'); const secondWrapper = await readFile(paths.wrapperTargetPath, 'utf8'); const secondWrapperMode = (await stat(paths.wrapperTargetPath)).mode & 0o777; expect(secondUnit).toBe(firstUnit); expect(secondWrapper).toBe(firstWrapper); expect(secondWrapperMode).toBe(firstWrapperMode); }); it('never touches the real host: only writes under the supplied temp dirs', async () => { const paths = await fakePaths(); await applyBrokerSupervisor(paths); expect(paths.systemdUserDir.startsWith(tmpdir())).toBe(true); expect(paths.mosaicHome.startsWith(tmpdir())).toBe(true); }); }); describe('checkBrokerSupervisorHealth / isBrokerSupervisorHealthy', () => { async function fakeHealthPaths(): Promise< Pick > { const runtimeDir = await tempDir('mosaic-broker-supervisor-health-'); await mkdir(join(runtimeDir, 'systemd-user'), { recursive: true }); return { unitTargetPath: join(runtimeDir, 'systemd-user', 'mosaic-lease-broker.service'), socketPath: join(runtimeDir, 'broker.sock'), }; } it('reports unhealthy when neither the unit nor the socket exist', async () => { const paths = await fakeHealthPaths(); const health = await checkBrokerSupervisorHealth(paths); expect(health).toEqual({ unitInstalled: false, socketPresent: false, healthy: false }); expect(await isBrokerSupervisorHealthy(paths)).toBe(false); }); it('reports unhealthy when the unit is installed but no socket is listening', async () => { const paths = await fakeHealthPaths(); await writeFile(paths.unitTargetPath, '[Unit]\n'); const health = await checkBrokerSupervisorHealth(paths); expect(health.unitInstalled).toBe(true); expect(health.socketPresent).toBe(false); expect(health.healthy).toBe(false); }); it('reports healthy=true once a real Unix socket exists at the resolved path, and false again once removed', async () => { const paths = await fakeHealthPaths(); const server = createServer(); cleanupServers.push(server); await new Promise((resolve, reject) => { server.once('error', reject); server.listen(paths.socketPath, resolve); }); expect(await isBrokerSupervisorHealthy(paths)).toBe(true); const health = await checkBrokerSupervisorHealth(paths); expect(health.socketPresent).toBe(true); expect(health.healthy).toBe(true); await new Promise((resolve) => server.close(() => resolve())); await rm(paths.socketPath, { force: true }); expect(await isBrokerSupervisorHealthy(paths)).toBe(false); }); it('does not confuse a stale regular file at the socket path with a live socket', async () => { const paths = await fakeHealthPaths(); await writeFile(paths.socketPath, 'not actually a socket'); expect(await isBrokerSupervisorHealthy(paths)).toBe(false); }); });