Part of #869 Mos (id-11) Gate-16 merge: independent APPROVE @75235ef8 (9/9, no live host mutation), author id2 != approver id11, CI green wp1971. Co-authored-by: jason.woltje <jason@diversecanvas.com> Co-committed-by: jason.woltje <jason@diversecanvas.com>
238 lines
8.8 KiB
TypeScript
238 lines
8.8 KiB
TypeScript
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<string> {
|
|
const dir = await mkdtemp(join(tmpdir(), prefix));
|
|
cleanupDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
afterEach(async () => {
|
|
for (const server of cleanupServers.splice(0)) {
|
|
await new Promise<void>((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/<uid>/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<BrokerSupervisorPaths> {
|
|
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<BrokerSupervisorPaths, 'unitTargetPath' | 'socketPath'>
|
|
> {
|
|
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<void>((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<void>((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);
|
|
});
|
|
});
|