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
13 changes: 9 additions & 4 deletions apps/desktop/src/main/__tests__/runtime-host-management.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@

import assert from 'node:assert/strict';
import { test } from 'node:test';
import { runtimeHostAccessCredentialFingerprint } from '@maka/runtime-host/operator';
import {
runtimeHostAccessCredentialFingerprint,
type RuntimeHostServiceManagementFrame,
} from '@maka/runtime-host/operator';
import { createDesktopRuntimeHostManagement } from '../runtime-host-management.js';
import type {
DesktopRuntimeHostSshAccessInput,
Expand Down Expand Up @@ -438,11 +441,10 @@ test('rechecks uninstall intent before retrying the remote service', async () =>
function serviceResult(
action: DesktopRuntimeHostSshManagementInput['action'],
operatorAccess = false,
) {
return {
): Extract<RuntimeHostServiceManagementFrame, { kind: 'result' }> {
const result = {
schemaVersion: 1 as const,
kind: 'result' as const,
action,
...(operatorAccess
? { operatorCapabilities: ['access-management-v1' as const] }
: {}),
Expand All @@ -457,6 +459,9 @@ function serviceResult(
projectDirectoryRoots: [],
},
};
return action === 'retire'
? { ...result, action, retirement: { kind: 'stopped' } }
: { ...result, action };
}

function accessCredential(
Expand Down
298 changes: 297 additions & 1 deletion packages/cli/src/__tests__/runtime-host-service-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ import {
RUNTIME_HOST_OPERATOR_ACCESS_MANAGEMENT_CAPABILITY,
RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST_ENV,
} from '@maka/runtime-host/operator';
import { resolveStorageRoot } from '@maka/storage/root-authority';
import { RUNTIME_HOST_RETIREMENT_EXIT_CODE } from '@maka/runtime-host/server';
import { resolveStorageRoot, tryAcquireInteractiveRootOwner } from '@maka/storage/root-authority';
import { parseRuntimeHostCommand } from '../runtime-host-cli.js';
import {
removeRuntimeHostManagedDeployment,
Expand Down Expand Up @@ -128,6 +129,33 @@ describe('managed Runtime Host service', () => {
retainManagedDeployment: true,
},
);
assert.deepEqual(
parseRuntimeHostCommand([
'service',
'retire',
'--framed',
'--allow-interrupt-active-tasks',
'--expected-service-id',
'b'.repeat(64),
'--expected-root-path',
'/srv/maka',
'--expected-root-id',
'a'.repeat(64),
]),
{
kind: 'runtime-host-service-manage',
action: 'retire',
json: false,
framed: true,
allowInterruptActiveTasks: true,
expectedTarget: {
serviceId: 'b'.repeat(64),
rootPath: '/srv/maka',
rootId: 'a'.repeat(64),
},
},
);
assert.equal(parseRuntimeHostCommand(['service', 'retire', '--framed']).kind, 'error');
assert.deepEqual(
parseRuntimeHostCommand([
'service',
Expand Down Expand Up @@ -522,6 +550,10 @@ describe('managed Runtime Host service', () => {
assert.match(unit, /"Cash\$\$=\/home\/\$\$ada\/My Projects"/u);
assert.match(unit, /"\/opt\/\$\$Node 24\/bin\/node"/u);
assert.match(unit, /"\/opt\/Maka\/\$\$current\/cli\.js"/u);
assert.ok(unit.includes(`SuccessExitStatus=${String(RUNTIME_HOST_RETIREMENT_EXIT_CODE)}\n`));
assert.ok(
unit.includes(`RestartPreventExitStatus=${String(RUNTIME_HOST_RETIREMENT_EXIT_CODE)}\n`),
);
assert.match(unit, /^Restart=always$/mu);
assert.match(unit, /^StartLimitIntervalSec=60s$/mu);
assert.match(unit, /^StartLimitBurst=5$/mu);
Expand All @@ -545,6 +577,7 @@ describe('managed Runtime Host service', () => {
'Persistent user services are disabled',
);
},
withLifecycleLock: async (_root, operation) => operation(),
createBackend: createUnusedBackend,
writeOutput: (value) => {
output += value;
Expand All @@ -563,6 +596,126 @@ describe('managed Runtime Host service', () => {
});
});

it('emits bounded retirement facts in framed output', async () => {
let output = '';
let lifecycleLocked = false;
const exitCode = await runManagedRuntimeHostServiceCli(
{
action: 'retire',
json: false,
framed: true,
clientDataRoot: '/config/Maka',
defaultRootPath: '/config/Maka/workspaces/default',
nodePath: '/usr/bin/node',
cliPath: '/opt/maka/cli.js',
},
{
manage: async () => {
assert.equal(lifecycleLocked, true);
return {
schemaVersion: 1,
action: 'retire',
service: {
manager: 'systemd_user',
installed: true,
enabled: true,
active: false,
state: 'stopped',
pid: null,
lastExitCode: 0,
installedVersion: '1.2.3',
config: null,
},
retirement: { kind: 'retired', hostEpoch: 'host-1', pid: 42 },
};
},
withLifecycleLock: async (_root, operation) => {
lifecycleLocked = true;
try {
return await operation();
} finally {
lifecycleLocked = false;
}
},
createBackend: createUnusedBackend,
writeOutput: (value) => {
output += value;
},
},
);
assert.equal(exitCode, 0);
const frame = decodeRuntimeHostServiceManagementFrame(output);
assert.equal(frame?.kind, 'result');
assert.deepEqual(
frame?.kind === 'result' && frame.action === 'retire' ? frame.retirement : null,
{
kind: 'retired',
hostEpoch: 'host-1',
pid: 42,
},
);
});

it('reports active work as a blocked retirement result', async () => {
const service = {
manager: 'systemd_user',
installed: true,
enabled: true,
active: true,
state: 'running',
pid: 42,
lastExitCode: 0,
installedVersion: '1.2.3',
config: null,
} as const;
const run = async (framed: boolean) => {
let output = '';
const exitCode = await runManagedRuntimeHostServiceCli(
{
action: 'retire',
json: !framed,
framed,
clientDataRoot: '/config/Maka',
defaultRootPath: '/config/Maka/workspaces/default',
nodePath: '/usr/bin/node',
cliPath: '/opt/maka/cli.js',
},
{
manage: async () => ({
schemaVersion: 1,
action: 'retire',
service,
retirement: { kind: 'active_tasks' },
}),
withLifecycleLock: async (_root, operation) => operation(),
createBackend: createUnusedBackend,
writeOutput: (value) => {
output += value;
},
},
);
return { exitCode, output };
};

const json = await run(false);
assert.equal(json.exitCode, 1);
assert.deepEqual(JSON.parse(json.output), {
schemaVersion: 1,
action: 'retire',
service,
retirement: { kind: 'active_tasks' },
ok: false,
});

const framed = await run(true);
assert.equal(framed.exitCode, 1);
const frame = decodeRuntimeHostServiceManagementFrame(framed.output);
assert.deepEqual(
frame?.kind === 'result' && frame.action === 'retire' ? frame.retirement : null,
{ kind: 'active_tasks' },
);
});

it('projects requested operator capabilities without launch configuration', async (t) => {
const previousCapabilityRequest = process.env[RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST_ENV];
delete process.env[RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST_ENV];
Expand Down Expand Up @@ -724,6 +877,149 @@ describe('managed Runtime Host service', () => {
assert.equal(repaired.service.config?.rootPath, root.canonicalPath);
});

it('retires the exact managed Host only after active work is authorized', async (t) => {
const base = await mkdtemp(join(tmpdir(), 'maka-runtime-host-retirement-'));
t.after(() => rm(base, { recursive: true, force: true }));
const clientDataRoot = join(base, 'config');
const root = await resolveStorageRoot({ path: join(base, 'state'), kind: 'interactive' });
const cliPath = join(base, 'cli.js');
await writeFile(cliPath, '#!/usr/bin/env node\n', 'utf8');
let serviceState: 'running' | 'starting' | 'stopped' = 'running';
let startingPid: number | null = null;
let stops = 0;
let observedStartingFence = false;
const backend: RuntimeHostServiceBackend = {
...createReadyBackend(),
status: async () => ({
manager: 'systemd_user',
installed: true,
enabled: true,
active: serviceState === 'running',
state: serviceState,
pid: serviceState === 'running' ? 42 : serviceState === 'starting' ? startingPid : null,
lastExitCode: 0,
}),
stop: async () => {
stops += 1;
if (serviceState === 'starting' && startingPid === null) {
const contender = await tryAcquireInteractiveRootOwner(root);
observedStartingFence = contender === undefined;
await contender?.close();
}
serviceState = 'stopped';
},
};
const common = {
clientDataRoot,
defaultRootPath: root.canonicalPath,
nodePath: process.execPath,
cliPath,
} as const;
await manageRuntimeHostService({ ...common, action: 'install' }, backend, {
waitForReady: async () => undefined,
});
const expectedTarget = {
serviceId: resolveRuntimeHostManagedServiceId(clientDataRoot),
rootPath: root.canonicalPath,
rootId: root.rootId,
} as const;
const deps = {
prepareRetirement: async (
_config: RuntimeHostManagedServiceConfig,
expectedPid: number,
allow: boolean,
) => {
assert.equal(expectedPid, 42);
return allow
? ({ kind: 'prepared', hostEpoch: 'host-1', pid: 42 } as const)
: ({ kind: 'active_tasks' } as const);
},
} as const;

const blocked = await manageRuntimeHostService(
{ ...common, action: 'retire', expectedTarget },
backend,
deps,
);
assert.deepEqual(blocked.retirement, { kind: 'active_tasks' });
assert.equal(stops, 0);

const retired = await manageRuntimeHostService(
{
...common,
action: 'retire',
expectedTarget,
allowInterruptActiveTasks: true,
},
backend,
deps,
);
assert.deepEqual(retired.retirement, {
kind: 'retired',
hostEpoch: 'host-1',
pid: 42,
});
assert.equal(stops, 1);

const conflictingOwner = await tryAcquireInteractiveRootOwner(root);
assert.ok(conflictingOwner);
await assert.rejects(
manageRuntimeHostService({ ...common, action: 'retire', expectedTarget }, backend, deps),
(error: unknown) =>
error instanceof RuntimeHostServiceManagerError && error.code === 'retirement_failed',
);
await conflictingOwner.close();

serviceState = 'starting';
const starting = await manageRuntimeHostService(
{ ...common, action: 'retire', expectedTarget },
backend,
deps,
);
assert.deepEqual(starting.retirement, { kind: 'stopped' });
assert.equal(serviceState, 'stopped');
assert.equal(observedStartingFence, true);

serviceState = 'starting';
const competingStarter = await tryAcquireInteractiveRootOwner(root);
assert.ok(competingStarter);
const stopsBeforeConflict = stops;
await assert.rejects(
manageRuntimeHostService({ ...common, action: 'retire', expectedTarget }, backend, deps),
(error: unknown) =>
error instanceof RuntimeHostServiceManagerError && error.code === 'retirement_failed',
);
assert.equal(stops, stopsBeforeConflict);
await competingStarter.close();

serviceState = 'starting';
startingPid = 42;
const startingBlocked = await manageRuntimeHostService(
{ ...common, action: 'retire', expectedTarget },
backend,
deps,
);
assert.deepEqual(startingBlocked.retirement, { kind: 'active_tasks' });
assert.equal(serviceState, 'starting');

const startingRetired = await manageRuntimeHostService(
{
...common,
action: 'retire',
expectedTarget,
allowInterruptActiveTasks: true,
},
backend,
deps,
);
assert.deepEqual(startingRetired.retirement, {
kind: 'retired',
hostEpoch: 'host-1',
pid: 42,
});
assert.equal(serviceState, 'stopped');
});

it('restores the deployed service when the replacement never becomes ready', async (t) => {
const base = await mkdtemp(join(tmpdir(), 'maka-runtime-host-service-rollback-'));
t.after(() => rm(base, { recursive: true, force: true }));
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/__tests__/runtime-host-setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ async function createReleasePackage(base: string, version: string): Promise<stri
}

function serviceResult(
action: RuntimeHostManagedServiceResult['action'],
action: Exclude<RuntimeHostManagedServiceResult['action'], 'retire'>,
config: RuntimeHostManagedServiceConfig | null,
installedVersion: string | null,
): RuntimeHostManagedServiceResult {
Expand Down
Loading