Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 135 additions & 7 deletions packages/cli/src/__tests__/runtime-host-launch-agent-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { test } from 'node:test';
import { RUNTIME_HOST_SERVICE_LOG_MAX_BYTES } from '@maka/runtime-host/operator';
import {
createLaunchAgentRuntimeHostService,
renderLaunchAgentPlist,
renderLaunchAgentUpdatePlist,
resolveLaunchAgentPath,
resolveLaunchAgentUpdatePath,
} from '../runtime-host-launch-agent-service.js';
import type { RuntimeHostManagedServiceConfig } from '../runtime-host-service-manager.js';

Expand All @@ -34,6 +37,8 @@ const UID = 501;
const LABEL = `com.maka.runtime-host.${SERVICE_ID}`;
const DOMAIN = `gui/${String(UID)}`;
const TARGET = `${DOMAIN}/${LABEL}`;
const UPDATE_LABEL = `${LABEL}.update`;
const UPDATE_TARGET = `${DOMAIN}/${UPDATE_LABEL}`;

test('renders the canonical Runtime Host command as a private persistent LaunchAgent', () => {
const config = fixtureConfig('/tmp/node & tool', '/tmp/maka <cli>', '/tmp/state > root');
Expand All @@ -55,6 +60,111 @@ test('renders the canonical Runtime Host command as a private persistent LaunchA
assert.match(plist, /<string>workspace=\/tmp\/projects<\/string>/u);
});

test('renders managed update reconciliation as a periodic one-shot LaunchAgent', () => {
const config = {
...fixtureConfig('/tmp/node', '/tmp/maka', '/tmp/state'),
managedDeploymentRoot: '/tmp/managed deployment',
};
const plist = renderLaunchAgentUpdatePlist(config, {
label: UPDATE_LABEL,
stdoutPath: '/tmp/update.stdout.log',
stderrPath: '/tmp/update.stderr.log',
});

assert.match(plist, /<string>\/tmp\/managed deployment\/operator<\/string>/u);
assert.match(plist, /<string>reconcile-update<\/string>/u);
assert.match(plist, /<string>--framed<\/string>/u);
assert.match(plist, /<key>StartInterval<\/key>\n <integer>86400<\/integer>/u);
assert.doesNotMatch(plist, /<key>KeepAlive<\/key>/u);
});

test('installs and removes the update scheduler with a managed LaunchAgent', async () => {
await withFixture(async ({ homeDir, cliPath, launchctl }) => {
const backend = createLaunchAgentRuntimeHostService(SERVICE_ID, {
homeDir,
uid: UID,
runLaunchctl: launchctl.run,
isProcessAlive: () => false,
});
const config = {
...fixtureConfig(process.execPath, cliPath, join(homeDir, 'state')),
managedDeploymentRoot: join(homeDir, 'managed'),
};

await backend.install(config);
await backend.verifyDeployment(config);
const updatePath = resolveLaunchAgentUpdatePath(SERVICE_ID, homeDir);
assert.match(await readFile(updatePath, 'utf8'), /reconcile-update/u);
const logDirectory = join(homeDir, 'Library', 'Logs', 'Maka', 'runtime-host-services');
await Promise.all([
writeFile(join(logDirectory, `${LABEL}.stdout.log`), 'h'.repeat(64 * 1024)),
writeFile(join(logDirectory, `${LABEL}.stderr.log`), 'host stderr'),
writeFile(join(logDirectory, `${UPDATE_LABEL}.stdout.log`), 'update stdout'),
writeFile(
join(logDirectory, `${UPDATE_LABEL}.stderr.log`),
'scheduler reconciliation failed',
),
]);
const logs = await backend.logs();
assert.match(logs, /scheduler reconciliation failed/u);
assert.ok(Buffer.byteLength(logs) <= RUNTIME_HOST_SERVICE_LOG_MAX_BYTES);

const updateBootouts = () =>
launchctl.calls.filter(
([command, target]) => command === 'bootout' && target === UPDATE_TARGET,
).length;
const bootoutsBeforeReplace = updateBootouts();
await backend.replace(config);
assert.equal(updateBootouts(), bootoutsBeforeReplace);

launchctl.updateRunning = true;
launchctl.failNextBootstrap = true;
await assert.rejects(backend.replace(config), /Starting the Runtime Host LaunchAgent failed/u);
assert.equal(updateBootouts(), bootoutsBeforeReplace);
assert.equal(launchctl.updateRunning, true);
launchctl.updateRunning = false;

await writeFile(updatePath, '<plist>stale</plist>\n', { mode: 0o600 });
await assert.rejects(
backend.verifyReplacementPreconditions(config),
(error: unknown) =>
error instanceof Error && 'code' in error && error.code === 'target_mismatch',
);
await assert.rejects(
backend.verifyDeployment(config),
(error: unknown) =>
error instanceof Error && 'code' in error && error.code === 'target_mismatch',
);
await backend.install(config);
await backend.verifyDeployment(config);

await backend.stop();
assert.equal(launchctl.updateLoaded, false);
await backend.verifyDeployment(config);
await assert.rejects(
backend.verifyDeployment(config, { requireSchedulerReady: true }),
(error: unknown) =>
error instanceof Error && 'code' in error && error.code === 'target_mismatch',
);
await backend.replace(config);
assert.equal(launchctl.updateLoaded, true);
await backend.verifyDeployment(config, { requireSchedulerReady: true });

const { managedDeploymentRoot: _managedDeploymentRoot, ...unmanagedConfig } = config;
await backend.install(unmanagedConfig);
await backend.verifyDeployment(unmanagedConfig);
assert.equal(await fileExists(updatePath), false);
assert.equal(launchctl.updateLoaded, false);
await backend.verifyReplacementPreconditions(config);
await backend.replace(config);
await backend.verifyDeployment(config);
assert.equal(launchctl.updateLoaded, true);

await backend.uninstall();
assert.equal(await fileExists(updatePath), false);
});
});

test('maps install, stop, start, restart, and uninstall onto one LaunchAgent service', async () => {
await withFixture(async ({ homeDir, cliPath, launchctl }) => {
let processChecks = 0;
Expand Down Expand Up @@ -145,6 +255,8 @@ test('restores the previous loaded LaunchAgent when deployment bootstrap fails',
interface FakeLaunchctl {
loaded: boolean;
running: boolean;
updateLoaded: boolean;
updateRunning: boolean;
failNextBootstrap: boolean;
readonly calls: string[][];
readonly run: (args: readonly string[]) => Promise<{
Expand All @@ -159,18 +271,23 @@ function createFakeLaunchctl(): FakeLaunchctl {
const fake: FakeLaunchctl = {
loaded: false,
running: false,
updateLoaded: false,
updateRunning: false,
failNextBootstrap: false,
calls: [],
run: async (args) => {
fake.calls.push([...args]);
if (args[0] === 'print' && args[1] === DOMAIN) {
return { exitCode: 0, stdout: 'domain = gui\n', stderr: '' };
}
if (args[0] === 'print' && args[1] === TARGET) {
return fake.loaded
if (args[0] === 'print' && (args[1] === TARGET || args[1] === UPDATE_TARGET)) {
const update = args[1] === UPDATE_TARGET;
const loaded = update ? fake.updateLoaded : fake.loaded;
const running = update ? fake.updateRunning : fake.running;
return loaded
? {
exitCode: 0,
stdout: fake.running
stdout: running
? `state = running\npid = ${String(pid)}\nlast exit code = 0\n`
: 'state = not running\nlast exit code = 0\n',
stderr: '',
Expand All @@ -182,14 +299,25 @@ function createFakeLaunchctl(): FakeLaunchctl {
fake.failNextBootstrap = false;
return { exitCode: 5, stdout: '', stderr: 'Input/output error' };
}
fake.loaded = true;
fake.running = true;
const update = args[2]?.endsWith('.update.plist') ?? false;
if (update) {
fake.updateLoaded = true;
fake.updateRunning = false;
} else {
fake.loaded = true;
fake.running = true;
}
pid += 1;
return { exitCode: 0, stdout: '', stderr: '' };
}
if (args[0] === 'bootout') {
fake.running = false;
fake.loaded = false;
if (args[1] === UPDATE_TARGET) {
fake.updateRunning = false;
fake.updateLoaded = false;
} else {
fake.running = false;
fake.loaded = false;
}
return { exitCode: 0, stdout: '', stderr: '' };
}
if (args[0] === 'kickstart') {
Expand Down
Loading