blob: 33dc79f2ce5352c62d25082ca8fa4fcce1476d0b [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import assert from 'node:assert/strict';
import { execFile as execFileCallback, spawn } from 'node:child_process';
import {
mkdir,
mkdtemp,
readFile,
readdir,
realpath,
rename,
rm,
symlink,
writeFile,
} from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { basename, dirname, join } from 'node:path';
import { test } from 'node:test';
import { promisify } from 'node:util';
import {
claimRuntimeHostManagedDeployment,
decodeRuntimeHostSetupFrame,
encodeRuntimeHostSetupFrame,
resolveRuntimeHostManagedDeploymentConfigPath,
runtimeHostManagedOperatorCommand,
RUNTIME_HOST_SETUP_FRAME_PREFIX,
type RuntimeHostManagedDeploymentConfig,
} from '@maka/runtime-host/operator';
import { RuntimeHostOperationError } from '@maka/runtime-host/client';
import {
resolveRootControlNamespace,
resolveRootOwnershipNamespace,
resolveStorageRoot,
tryAcquireStateRootOwner,
} from '@maka/storage/root-authority';
import {
acknowledgeRuntimeHostManagedDeploymentCleanup,
assertRuntimeHostManagedOperatorDeployment,
convergeRuntimeHostManagedOperator,
convergeRuntimeHostManagedWindowsTaskLauncher,
prepareRuntimeHostManagedPackageDeployment,
pruneRuntimeHostManagedPackages,
readRuntimeHostManagedDeploymentCleanupReceipt,
resolveRuntimeHostManagedControlRoot,
resolveRuntimeHostManagedDeploymentRoot,
verifyRuntimeHostManagedWindowsTaskLauncher,
} from '../runtime-host-managed-deployment.js';
import {
resolvePackagedRuntimeHostWindowsTaskLauncherPath,
resolveRuntimeHostWindowsTaskLauncherPath,
runtimeHostManagedWindowsTaskLauncherPath,
} from '../runtime-host-windows-task-launcher-artifact.js';
import { runRuntimeHostSetupCli } from '../runtime-host-setup-command.js';
import { RuntimeHostAccessUnavailableError } from '../runtime-host-access-command.js';
import { replaceRuntimeHostLifecycle } from '../runtime-host-lifecycle-transaction.js';
import { manageRuntimeHostManagedLifecycle } from '../runtime-host-managed-lifecycle-manager.js';
import { createOpenRcRuntimeHostLifecycleProvider } from '../runtime-host-openrc-service.js';
import { resolveRuntimeHostLifecycleProvider } from '../runtime-host-service-management-command.js';
import {
resolveRuntimeHostManagedServiceId,
type RuntimeHostServiceBackend,
} from '../runtime-host-service-manager.js';
const execFile = promisify(execFileCallback);
const PACKAGE_INTEGRITY = `sha512-${Buffer.alloc(64, 7).toString('base64')}`;
test('on-demand setup installs one exact deployment without a service backend', async (t) => {
const base = await realpath(await mkdtemp(join(tmpdir(), 'maka-runtime-host-on-demand-setup-')));
const stateRoot = join(base, 'state');
const clientDataRoot = join(base, 'client');
const canonicalDataHome = join(base, 'canonical-data-home');
const dataHome = join(base, 'data-home');
await mkdir(canonicalDataHome);
await symlink(canonicalDataHome, dataHome);
const previousDataHome = process.env.XDG_DATA_HOME;
process.env.XDG_DATA_HOME = dataHome;
const outputs: string[] = [];
let rootId = '';
let projectedOperatorDeploymentRoot = '';
let pairingAttempts = 0;
t.after(async () => {
if (previousDataHome === undefined) delete process.env.XDG_DATA_HOME;
else process.env.XDG_DATA_HOME = previousDataHome;
await Promise.all([
rm(base, { recursive: true, force: true }),
rootId
? rm(dirname(resolveRuntimeHostManagedDeploymentConfigPath(rootId)), {
recursive: true,
force: true,
})
: Promise.resolve(),
rootId
? rm(join(resolveRootControlNamespace(), rootId), {
recursive: true,
force: true,
})
: Promise.resolve(),
rootId
? rm(join(resolveRootOwnershipNamespace(), `${rootId}.lock`), {
force: true,
})
: Promise.resolve(),
]);
});
const options = {
json: true,
lifecycle: 'on_demand',
clientDataRoot,
defaultRootPath: stateRoot,
sourcePackageRoot: base,
version: '1.2.3',
principalId: 'desktop:client-1',
preset: 'desktop-client',
} as const;
const deployment = (serviceId: string) => ({
version: '1.2.3',
root: join(canonicalDataHome, 'Maka', 'runtime-host-services', serviceId),
cliPath: '/verified/package/dist/cli.js',
activate: async () => undefined,
cleanup: async () => undefined,
rollback: async () => undefined,
});
const activateManaged = async (input: { readonly rootId: string }) => {
rootId = input.rootId;
return {
schemaVersion: 1 as const,
kind: 'result' as const,
deploymentId: `${rootId.slice(0, 8)}-${rootId.slice(8, 12)}-4${rootId.slice(13, 16)}-8${rootId.slice(17, 20)}-${rootId.slice(20, 32)}`,
configRevision: 1,
rootId,
hostEpoch: 'host-epoch',
pid: 1234,
protocolVersion: 1,
endpoint: {
host: '127.0.0.1' as const,
port: 43_210,
websocketPath: '/runtime-host',
},
};
};
const overrides = {
createBackend: () => assert.fail('on-demand setup must not create a service backend'),
manageService: async () => assert.fail('on-demand setup must not manage a service'),
resolveRegistryCandidate: async () => ({
kind: 'npm_registry',
version: '1.2.3',
integrity: PACKAGE_INTEGRITY,
}),
withRegistryPackage: async (_candidate, use) => use('/verified/package'),
prepareDeployment: async (input) => deployment(input.serviceId),
openDeployment: async (input) => deployment(input.serviceId),
prunePackages: async () => undefined,
activateManaged,
activateDesired: activateManaged,
convergeOperator: async (_current, desired) => {
projectedOperatorDeploymentRoot = desired?.deploymentRoot ?? '';
},
verifyOperator: async () => undefined,
replaceCredential: async () => {
pairingAttempts += 1;
if (pairingAttempts === 1) throw new RuntimeHostAccessUnavailableError('unavailable');
if (pairingAttempts === 2) {
throw new RuntimeHostOperationError(
'access.credential.replace',
'host_not_ready',
'Runtime Host is not ready',
);
}
return {
rootId,
credential: 'secret-token',
credentialId: 'credential-1',
principalKind: 'remote_owner' as const,
principalId: 'desktop:client-1',
operationGrants: [] as const,
canPublishClientCapabilities: false,
canUseHostPaths: false,
};
},
verifyCredential: async ({ endpoint, rootId: expectedRootId }) => {
assert.equal(endpoint, 'ws://127.0.0.1:43210/runtime-host');
assert.equal(expectedRootId, rootId);
},
writeOutput: (value) => outputs.push(value),
} satisfies NonNullable<Parameters<typeof runRuntimeHostSetupCli>[1]>;
assert.equal(
await runRuntimeHostSetupCli({ ...options, reuseExistingEnvironment: true }, overrides),
0,
);
assert.equal(pairingAttempts, 3);
const complete = outputs
.map(decodeRuntimeHostSetupFrame)
.find((frame) => frame?.kind === 'complete');
assert.ok(complete?.kind === 'complete');
assert.equal(complete.operator.kind, 'node');
if (complete.operator.kind !== 'node') assert.fail('Setup returned a legacy operator');
assert.equal(complete.operator.nodePath, process.execPath);
const persisted = JSON.parse(
await readFile(resolveRuntimeHostManagedDeploymentConfigPath(rootId), 'utf8'),
) as {
deploymentRoot: string;
launch: { nodePath: string };
lifecycle: { mode: string };
listeners: { websocket: { port: number } };
reconciliation: { trigger: string };
};
assert.equal(
persisted.deploymentRoot,
join(canonicalDataHome, 'Maka', 'runtime-host-services', rootId),
);
assert.equal(complete.operator.modulePath, join(persisted.deploymentRoot, 'operator.mjs'));
assert.equal(projectedOperatorDeploymentRoot, persisted.deploymentRoot);
assert.equal(persisted.lifecycle.mode, 'on_demand');
assert.equal(persisted.listeners.websocket.port, 0);
assert.equal(persisted.reconciliation.trigger, 'activation');
const retryOutputs: string[] = [];
persisted.launch.nodePath =
process.platform === 'win32' ? 'C:\\Program Files\\nodejs\\node.exe' : '/opt/maka/node';
await writeFile(
resolveRuntimeHostManagedDeploymentConfigPath(rootId),
`${JSON.stringify(persisted)}\n`,
);
projectedOperatorDeploymentRoot = '/stale/operator/projection';
assert.equal(
await runRuntimeHostSetupCli(options, {
...overrides,
replaceLifecycle: async () => assert.fail('an idempotent retry must not replace lifecycle'),
writeOutput: (value) => retryOutputs.push(value),
}),
0,
);
const retryComplete = retryOutputs
.map(decodeRuntimeHostSetupFrame)
.find((frame) => frame?.kind === 'complete');
assert.equal(
retryComplete?.kind === 'complete' ? retryComplete.deploymentId : undefined,
complete.deploymentId,
);
assert.equal(
retryComplete?.kind === 'complete' && retryComplete.operator.kind === 'node'
? retryComplete.operator.nodePath
: undefined,
persisted.launch.nodePath,
);
assert.deepEqual(
JSON.parse(await readFile(resolveRuntimeHostManagedDeploymentConfigPath(rootId), 'utf8')),
persisted,
);
assert.equal(projectedOperatorDeploymentRoot, persisted.deploymentRoot);
const rejected: string[] = [];
assert.equal(
await runRuntimeHostSetupCli(
{ ...options, directPeer: { coordinationRelays: [] } },
{ ...overrides, writeOutput: (value) => rejected.push(value) },
),
1,
);
const failure = rejected
.map(decodeRuntimeHostSetupFrame)
.find((frame) => frame?.kind === 'error');
assert.equal(
failure?.kind === 'error' ? failure.error.code : undefined,
'unsupported_lifecycle_configuration',
);
// Discovery reuses canonical identity even when a newer/older Desktop selects
// another package, and never opens storage, activates, pairs, or changes settings.
const discoveryOwner = await tryAcquireStateRootOwner(
await resolveStorageRoot({ path: stateRoot, kind: 'interactive' }),
);
assert.ok(discoveryOwner);
t.after(() => discoveryOwner.close());
for (const version of ['1.2.2', '1.2.3', '1.2.4']) {
const discovery: string[] = [];
assert.equal(
await runRuntimeHostSetupCli(
{ ...options, version, reuseExistingEnvironment: true },
{
...overrides,
resolveRegistryCandidate: async () => assert.fail('discovery must not resolve a package'),
prepareDeployment: async () => assert.fail('discovery must not stage a package'),
replaceLifecycle: async () => assert.fail('discovery must not retire a Host'),
activateManaged: async () => assert.fail('discovery must not activate'),
replaceCredential: async () => assert.fail('local environment discovery must not pair'),
writeOutput: (value) => discovery.push(value),
},
),
0,
);
const binding = discovery
.map(decodeRuntimeHostSetupFrame)
.find((frame) => frame?.kind === 'existing_environment');
assert.equal(binding?.kind, 'existing_environment');
assert.equal(
binding && 'deploymentId' in binding ? binding.deploymentId : undefined,
complete.deploymentId,
);
assert.deepEqual(
JSON.parse(await readFile(resolveRuntimeHostManagedDeploymentConfigPath(rootId), 'utf8')),
persisted,
);
}
await discoveryOwner.close();
const replacementIntegrity = `sha512-${Buffer.alloc(64, 9).toString('base64')}`;
const replacementOptions = { ...options, version: '1.2.4' } as const;
const replacementPackage = {
...overrides,
resolveRegistryCandidate: async () => ({
kind: 'npm_registry' as const,
version: '1.2.4',
integrity: replacementIntegrity,
}),
};
const refused: string[] = [];
assert.equal(
await runRuntimeHostSetupCli(replacementOptions, {
...replacementPackage,
writeOutput: (value) => refused.push(value),
}),
1,
);
const refusedReplacement = refused
.map(decodeRuntimeHostSetupFrame)
.find((frame) => frame?.kind === 'error');
assert.equal(
refusedReplacement?.kind === 'error' ? refusedReplacement.error.code : undefined,
'version_change_requires_update',
);
const beforeBusyUpdate = await readFile(
resolveRuntimeHostManagedDeploymentConfigPath(rootId),
'utf8',
);
const busyOutputs: string[] = [];
assert.equal(
await runRuntimeHostSetupCli(
{ ...replacementOptions, updateExisting: true },
{
...replacementPackage,
replaceLifecycle: async (input) => {
// Model the source Host refusing retirement while a TUI owns work.
assert.equal(input.allowInterruptActiveTasks, false);
return { kind: 'active_tasks' };
},
writeOutput: (value) => busyOutputs.push(value),
},
),
1,
);
assert.equal(
await readFile(resolveRuntimeHostManagedDeploymentConfigPath(rootId), 'utf8'),
beforeBusyUpdate,
);
assert.ok(
busyOutputs
.map(decodeRuntimeHostSetupFrame)
.some((frame) => frame?.kind === 'error' && frame.error.code === 'active_tasks'),
);
const replacementOutputs: string[] = [];
let replacementAllowedInterrupt: boolean | undefined;
assert.equal(
await runRuntimeHostSetupCli(
{ ...replacementOptions, updateExisting: true },
{
...replacementPackage,
openDeployment: async () =>
assert.fail('a changed exact package must be staged before replacement'),
replaceLifecycle: async (input) => {
replacementAllowedInterrupt = input.allowInterruptActiveTasks;
return replaceRuntimeHostLifecycle(input);
},
writeOutput: (value) => replacementOutputs.push(value),
},
),
0,
);
const replaced = JSON.parse(
await readFile(resolveRuntimeHostManagedDeploymentConfigPath(rootId), 'utf8'),
) as RuntimeHostManagedDeploymentConfig;
assert.equal(replaced.launch.package.version, '1.2.4');
assert.equal(replaced.launch.package.integrity, replacementIntegrity);
assert.equal(replacementAllowedInterrupt, false);
assert.equal(
replacementOutputs.map(decodeRuntimeHostSetupFrame).some((frame) => frame?.kind === 'complete'),
true,
);
const uninstalled = await manageRuntimeHostManagedLifecycle(
rootId,
{
action: 'uninstall',
clientDataRoot,
defaultRootPath: stateRoot,
nodePath: process.execPath,
cliPath: '/verified/package/dist/cli.js',
expectedTarget: {
serviceId: rootId,
rootPath: stateRoot,
rootId,
deploymentId: complete.deploymentId,
},
},
{
resolveProvider: () => assert.fail('on-demand uninstall must not resolve a provider'),
},
);
assert.equal(uninstalled.action, 'uninstall');
assert.equal(uninstalled.retirement.kind, 'stopped');
});
test('fresh supervised setup discovers its provider before constructing a legacy backend', async (t) => {
const base = await realpath(await mkdtemp(join(tmpdir(), 'maka-runtime-host-supervised-setup-')));
const stateRoot = join(base, 'state');
const clientDataRoot = join(base, 'client');
let rootId = '';
t.after(async () => {
await Promise.all([
rm(base, { recursive: true, force: true }),
rootId
? rm(join(resolveRootControlNamespace(), rootId), { recursive: true, force: true })
: Promise.resolve(),
rootId
? rm(join(resolveRootOwnershipNamespace(), `${rootId}.lock`), { force: true })
: Promise.resolve(),
]);
});
const result = await runRuntimeHostSetupCli(
{
json: true,
lifecycle: 'supervised',
clientDataRoot,
defaultRootPath: stateRoot,
sourcePackageRoot: base,
version: '1.2.3',
principalId: 'desktop:client-1',
preset: 'desktop-client',
},
{
createBackend: () => assert.fail('a fresh canonical setup has no legacy backend'),
discoverLifecycleProvider: async (discoveredRootId) => {
rootId = discoveredRootId;
return {
provider: createOpenRcRuntimeHostLifecycleProvider(rootId, 'openrc_user'),
availability: 'session',
};
},
resolveRegistryCandidate: async () => ({
kind: 'npm_registry',
version: '1.2.3',
integrity: PACKAGE_INTEGRITY,
}),
withRegistryPackage: async (_candidate, use) => use('/verified/package'),
prepareDeployment: async ({ serviceId }) => ({
version: '1.2.3',
root: join(base, 'deployment'),
cliPath: '/verified/package/dist/cli.js',
operator: {
kind: 'node' as const,
platform: 'posix' as const,
nodePath: '/usr/bin/node',
modulePath: '/opt/maka/operator.mjs',
},
activate: async () => undefined,
cleanup: async () => undefined,
rollback: async () => undefined,
}),
allocateLoopbackPort: async () => 43_210,
replaceLifecycle: async ({ desired }) => {
assert.equal(desired.lifecycle.mode, 'supervised');
assert.equal(desired.lifecycle.provider, 'openrc_user');
return { kind: 'replaced', config: desired };
},
prunePackages: async () => undefined,
replaceCredential: async () => ({
rootId,
credential: 'secret-token',
credentialId: 'credential-1',
principalKind: 'remote_owner',
principalId: 'desktop:client-1',
operationGrants: [],
canPublishClientCapabilities: false,
canUseHostPaths: false,
}),
verifyCredential: async () => undefined,
writeOutput: () => undefined,
},
);
assert.equal(result, 0);
});
test('managed setup frames reject malformed machine output', () => {
assert.equal(
decodeRuntimeHostSetupFrame(
encodeRuntimeHostSetupFrame({
schemaVersion: 1,
sequence: 0,
kind: 'progress',
phase: 'checking_environment',
}),
)?.kind,
'progress',
);
assert.equal(
decodeRuntimeHostSetupFrame(
`${RUNTIME_HOST_SETUP_FRAME_PREFIX}${Buffer.from(
JSON.stringify({
schemaVersion: 1,
sequence: 0,
kind: 'complete',
version: '0.2.0',
rootId: 'root',
endpoint: 'ws://example.com/runtime-host',
credentialId: 'credential',
credential: 'secret',
}),
).toString('base64url')}\n`,
),
undefined,
);
assert.equal(
decodeRuntimeHostSetupFrame(
`${RUNTIME_HOST_SETUP_FRAME_PREFIX}${Buffer.from(
JSON.stringify({
schemaVersion: 1,
sequence: 1,
kind: 'complete',
version: '0.2.0',
serviceId: 'a'.repeat(64),
deploymentId: '00000000-0000-4000-8000-000000000001',
operator: {
kind: 'legacy_posix_executable',
executablePath: '/opt/maka/operator',
},
rootPath: '/workspaces/default',
rootId: 'a'.repeat(64),
endpoint: 'ws://127.0.0.1:4321/runtime-host',
credentialId: 'credential',
credential: 'secret',
}),
).toString('base64url')}\n`,
),
undefined,
);
});
test('persisted OpenRC providers resolve without reselecting the platform default', () => {
const rootId = 'a'.repeat(64);
const openRc = resolveRuntimeHostLifecycleProvider({
schemaVersion: 1,
state: 'active',
deploymentId: '00000000-0000-4000-8000-000000000001',
configRevision: 1,
deploymentRoot: '/opt/maka',
root: { id: rootId, path: '/srv/maka' },
projectDirectoryRoots: [],
launch: {
kind: 'exact_package',
nodePath: '/usr/bin/node',
package: { kind: 'npm_registry', version: '0.2.0', integrity: PACKAGE_INTEGRITY },
},
listeners: { localIpc: true },
lifecycle: { mode: 'supervised', provider: 'openrc_user', availability: 'session' },
reconciliation: { trigger: 'scheduled', provider: 'openrc_supervised_loop' },
});
assert.equal(openRc.supervisor.provider, 'openrc_user');
assert.equal(openRc.reconciliationTrigger.provider, 'openrc_supervised_loop');
});
test('registry package identity avoids local content and recovers an interrupted removal', async (t) => {
const base = await realpath(await mkdtemp(join(tmpdir(), 'maka-runtime-host-registry-package-')));
t.after(() => rm(base, { recursive: true, force: true }));
const version = '0.2.0';
const localPackage = await createReleasePackage(join(base, 'local'), version);
const registryPackage = await createReleasePackage(join(base, 'registry'), version);
await writeFile(join(localPackage, 'dist', 'cli.js'), 'local package\n');
await writeFile(join(registryPackage, 'dist', 'cli.js'), 'registry package\n');
const clientDataRoot = join(base, 'config', 'Maka');
const serviceId = resolveRuntimeHostManagedServiceId(clientDataRoot);
await mkdir(join(base, 'durable-data'));
await symlink(join(base, 'durable-data'), join(base, 'data'));
const pathOptions = {
env: { XDG_DATA_HOME: join(base, 'data') },
homeDir: join(base, 'home'),
platform: 'linux' as const,
};
const local = await prepareRuntimeHostManagedPackageDeployment(
{ serviceId, clientDataRoot, sourcePackageRoot: localPackage, version },
pathOptions,
);
const registry = await prepareRuntimeHostManagedPackageDeployment(
{
serviceId,
clientDataRoot,
sourcePackageRoot: registryPackage,
version,
packageIntegrity: PACKAGE_INTEGRITY,
},
pathOptions,
);
assert.equal(
registry.root,
join(base, 'durable-data', 'Maka', 'runtime-host-services', serviceId),
);
assert.notEqual(local.cliPath, registry.cliPath);
assert.match(registry.cliPath, /\/versions\/registry-[a-f0-9]{64}\/dist\/cli\.js$/u);
assert.equal(await readFile(registry.cliPath, 'utf8'), 'registry package\n');
await registry.cleanup();
assert.deepEqual(await readdir(dirname(dirname(dirname(registry.cliPath)))), [
basename(dirname(dirname(registry.cliPath))),
]);
const registryRoot = dirname(dirname(registry.cliPath));
const versionsRoot = dirname(registryRoot);
await rename(registryRoot, join(versionsRoot, `.${basename(registryRoot)}.interrupted.deleted`));
const recovered = await prepareRuntimeHostManagedPackageDeployment(
{
serviceId,
clientDataRoot,
sourcePackageRoot: registryPackage,
version,
packageIntegrity: PACKAGE_INTEGRITY,
},
pathOptions,
);
assert.equal(await readFile(recovered.cliPath, 'utf8'), 'registry package\n');
assert.deepEqual(await readdir(versionsRoot), [basename(registryRoot)]);
const retiredRoot = join(dirname(recovered.root), `.${serviceId}.retired`);
await rename(recovered.root, retiredRoot);
const republished = await prepareRuntimeHostManagedPackageDeployment(
{
serviceId,
clientDataRoot,
sourcePackageRoot: registryPackage,
version,
packageIntegrity: PACKAGE_INTEGRITY,
},
pathOptions,
);
assert.equal(await readFile(republished.cliPath, 'utf8'), 'registry package\n');
assert.deepEqual(await readdir(dirname(republished.root)), [serviceId]);
const redirectedDataHome = join(base, 'redirected-data');
const outsideMaka = join(base, 'outside', 'Maka');
await mkdir(redirectedDataHome);
await mkdir(outsideMaka, { recursive: true });
await writeFile(join(outsideMaka, 'sentinel'), 'outside\n');
await symlink(outsideMaka, join(redirectedDataHome, 'Maka'));
await assert.rejects(
prepareRuntimeHostManagedPackageDeployment(
{
serviceId,
clientDataRoot,
sourcePackageRoot: registryPackage,
version,
packageIntegrity: PACKAGE_INTEGRITY,
},
{ ...pathOptions, env: { XDG_DATA_HOME: redirectedDataHome } },
),
/redirected managed Runtime Host deployment path/u,
);
assert.equal(await readFile(join(outsideMaka, 'sentinel'), 'utf8'), 'outside\n');
assert.deepEqual(await readdir(outsideMaka), ['sentinel']);
const redirectedServiceDataHome = join(base, 'redirected-service-data');
const managedServices = join(redirectedServiceDataHome, 'Maka', 'runtime-host-services');
const outsideServiceRoot = join(
base,
'outside-service',
'Maka',
'runtime-host-services',
serviceId,
);
await mkdir(managedServices, { recursive: true });
await mkdir(outsideServiceRoot, { recursive: true });
await writeFile(join(outsideServiceRoot, 'sentinel'), 'outside service\n');
await symlink(outsideServiceRoot, join(managedServices, serviceId));
await assert.rejects(
prepareRuntimeHostManagedPackageDeployment(
{
serviceId,
clientDataRoot,
sourcePackageRoot: registryPackage,
version,
packageIntegrity: PACKAGE_INTEGRITY,
},
{ ...pathOptions, env: { XDG_DATA_HOME: redirectedServiceDataHome } },
),
/redirected managed Runtime Host deployment path/u,
);
assert.equal(await readFile(join(outsideServiceRoot, 'sentinel'), 'utf8'), 'outside service\n');
assert.deepEqual(await readdir(outsideServiceRoot), ['sentinel']);
const redirectedVersionsDataHome = join(base, 'redirected-versions-data');
const redirectedVersionsDeploymentRoot = resolveRuntimeHostManagedDeploymentRoot(serviceId, {
...pathOptions,
env: { XDG_DATA_HOME: redirectedVersionsDataHome },
});
const outsideVersions = join(base, 'outside-versions');
await mkdir(redirectedVersionsDeploymentRoot, { recursive: true });
await mkdir(outsideVersions);
await writeFile(join(outsideVersions, 'sentinel'), 'outside versions\n');
await symlink(outsideVersions, join(redirectedVersionsDeploymentRoot, 'versions'));
await assert.rejects(
prepareRuntimeHostManagedPackageDeployment(
{
serviceId,
clientDataRoot,
sourcePackageRoot: registryPackage,
version,
packageIntegrity: PACKAGE_INTEGRITY,
},
{ ...pathOptions, env: { XDG_DATA_HOME: redirectedVersionsDataHome } },
),
/package store is redirected/u,
);
assert.equal(await readFile(join(outsideVersions, 'sentinel'), 'utf8'), 'outside versions\n');
assert.deepEqual(await readdir(outsideVersions), ['sentinel']);
const redirectedPackageDataHome = join(base, 'redirected-package-data');
const redirectedPackageDeploymentRoot = resolveRuntimeHostManagedDeploymentRoot(serviceId, {
...pathOptions,
env: { XDG_DATA_HOME: redirectedPackageDataHome },
});
const redirectedPackageVersions = join(redirectedPackageDeploymentRoot, 'versions');
const outsideRetainedPackage = await createReleasePackage(
join(base, 'outside-retained'),
version,
);
await mkdir(redirectedPackageVersions, { recursive: true });
await writeFile(join(outsideRetainedPackage, 'sentinel'), 'outside retained\n');
await symlink(outsideRetainedPackage, join(redirectedPackageVersions, basename(registryRoot)));
await assert.rejects(
prepareRuntimeHostManagedPackageDeployment(
{
serviceId,
clientDataRoot,
sourcePackageRoot: registryPackage,
version,
packageIntegrity: PACKAGE_INTEGRITY,
},
{ ...pathOptions, env: { XDG_DATA_HOME: redirectedPackageDataHome } },
),
/published package is redirected/u,
);
assert.equal(
await readFile(join(outsideRetainedPackage, 'sentinel'), 'utf8'),
'outside retained\n',
);
const authorityStateRoot = join(base, 'authority-state');
const capability = await resolveStorageRoot({
path: authorityStateRoot,
kind: 'interactive',
});
const authorityServiceId = capability.rootId;
t.after(() =>
Promise.all([
rm(dirname(resolveRuntimeHostManagedDeploymentConfigPath(authorityServiceId)), {
recursive: true,
force: true,
}),
rm(join(resolveRootControlNamespace(), authorityServiceId), {
recursive: true,
force: true,
}),
rm(join(resolveRootOwnershipNamespace(), `${authorityServiceId}.lock`), {
force: true,
}),
]),
);
const currentPathOptions = {
env: { XDG_DATA_HOME: join(base, 'current-data') },
homeDir: join(base, 'home'),
platform: 'linux' as const,
};
const currentDeploymentRoot = resolveRuntimeHostManagedDeploymentRoot(
authorityServiceId,
currentPathOptions,
);
const currentConfig: RuntimeHostManagedDeploymentConfig = {
schemaVersion: 1,
state: 'active',
deploymentId: '00000000-0000-4000-8000-000000000002',
configRevision: 1,
deploymentRoot: currentDeploymentRoot,
root: { path: capability.canonicalPath, id: authorityServiceId },
projectDirectoryRoots: [],
launch: {
kind: 'exact_package',
nodePath: process.execPath,
package: { kind: 'npm_registry', version, integrity: PACKAGE_INTEGRITY },
},
listeners: { localIpc: true },
lifecycle: { mode: 'on_demand', availability: 'activation' },
reconciliation: { trigger: 'manual' },
};
await claimRuntimeHostManagedDeployment(capability, currentConfig);
await acknowledgeRuntimeHostManagedDeploymentCleanup({
serviceId: authorityServiceId,
deploymentId: '00000000-0000-4000-8000-000000000001',
deploymentRoot: resolveRuntimeHostManagedDeploymentRoot(authorityServiceId, {
...currentPathOptions,
env: { XDG_DATA_HOME: join(base, 'retired-data') },
}),
stateRootPath: capability.canonicalPath,
});
const liveOwner = await tryAcquireStateRootOwner(capability);
assert.ok(liveOwner);
try {
const preparedWithLiveAuthority = await prepareRuntimeHostManagedPackageDeployment(
{
serviceId: authorityServiceId,
clientDataRoot,
sourcePackageRoot: registryPackage,
version,
packageIntegrity: PACKAGE_INTEGRITY,
deploymentRoot: currentDeploymentRoot,
},
{
...currentPathOptions,
env: { XDG_DATA_HOME: join(base, 'drifted-data') },
},
);
assert.equal(preparedWithLiveAuthority.root, currentDeploymentRoot);
assert.equal(await readFile(preparedWithLiveAuthority.cliPath, 'utf8'), 'registry package\n');
assert.equal(
await readRuntimeHostManagedDeploymentCleanupReceipt(authorityServiceId),
undefined,
);
assert.equal(liveOwner.closed, false);
} finally {
await liveOwner.close();
}
await convergeRuntimeHostManagedOperator(undefined, currentConfig);
assert.equal(
(await readFile(join(currentDeploymentRoot, 'operator.mjs'), 'utf8')).includes(
join(currentDeploymentRoot, 'versions', basename(registryRoot), 'dist', 'cli.js'),
),
true,
);
await pruneRuntimeHostManagedPackages(currentConfig);
assert.deepEqual(await readdir(join(currentDeploymentRoot, 'versions')), [
basename(registryRoot),
]);
await assert.rejects(readdir(join(base, 'drifted-data')), { code: 'ENOENT' });
});
test('managed operator binds its Client Data Root and routes deployment cleanup', {
skip: process.platform === 'win32',
}, async (t) => {
const base = await mkdtemp(join(tmpdir(), 'maka-runtime-host-operator-'));
t.after(() => rm(base, { recursive: true, force: true }));
const version = '0.2.0';
const sourcePackageRoot = await createReleasePackage(base, version);
const clientDataRoot = join(base, 'config', 'Maka');
const capability = await resolveStorageRoot({
path: join(base, 'state'),
kind: 'interactive',
});
const serviceId = capability.rootId;
const deployment = await prepareRuntimeHostManagedPackageDeployment(
{
serviceId,
clientDataRoot,
sourcePackageRoot,
version,
packageIntegrity: PACKAGE_INTEGRITY,
},
{
env: { XDG_DATA_HOME: join(base, 'data') },
homeDir: join(base, 'home'),
platform: 'linux',
},
);
const config: RuntimeHostManagedDeploymentConfig = {
schemaVersion: 1,
state: 'active',
deploymentId: '00000000-0000-4000-8000-000000000001',
configRevision: 1,
deploymentRoot: deployment.root,
root: { id: serviceId, path: capability.canonicalPath },
projectDirectoryRoots: [],
launch: {
kind: 'exact_package',
nodePath: process.execPath,
package: {
kind: 'npm_registry',
version,
integrity: PACKAGE_INTEGRITY,
},
},
listeners: { localIpc: true },
lifecycle: { mode: 'on_demand', availability: 'activation' },
reconciliation: { trigger: 'manual' },
};
const legacyOperatorPath = join(deployment.root, 'operator');
await writeFile(legacyOperatorPath, '#!/bin/sh\nexit 99\n');
await deployment.activate();
assert.match(await readFile(legacyOperatorPath, 'utf8'), /operator\.mjs/u);
await convergeRuntimeHostManagedOperator(undefined, config);
const operator = runtimeHostManagedOperatorCommand(config, 'posix');
const authorityRoot = join(base, 'authority');
await mkdir(authorityRoot);
const authority = { authorityRoot, durabilityBoundary: authorityRoot };
await claimRuntimeHostManagedDeployment(capability, config, authority);
await assertRuntimeHostManagedOperatorDeployment(
serviceId,
config.deploymentId,
deployment.cliPath,
{ authority },
);
await assert.rejects(
assertRuntimeHostManagedOperatorDeployment(
serviceId,
config.deploymentId,
join(deployment.root, 'versions', 'stale', 'dist', 'cli.js'),
{ authority },
),
/different deployment generation or exact package/u,
);
await deployment.cleanup();
const invocationPath = join(base, 'operator-argv.json');
await writeFile(
deployment.cliPath,
`require('node:fs').writeFileSync(process.env.MAKA_TEST_OUTPUT, JSON.stringify(process.argv.slice(2)));\n`,
);
await execFile(legacyOperatorPath, ['status'], {
env: {
...process.env,
XDG_CONFIG_HOME: join(base, 'different-config'),
MAKA_TEST_OUTPUT: invocationPath,
},
});
assert.deepEqual(JSON.parse(await readFile(invocationPath, 'utf8')), [
'runtime-host',
'service',
'status',
'--client-data-root',
resolveRuntimeHostManagedControlRoot(serviceId),
'--managed-root-id',
serviceId,
'--operator-deployment-id',
'00000000-0000-4000-8000-000000000001',
]);
await execFile(
operator.nodePath,
[operator.modulePath, 'access', 'list', '--root', '/runtime-root', '--framed'],
{
env: { ...process.env, MAKA_TEST_OUTPUT: invocationPath },
},
);
assert.deepEqual(JSON.parse(await readFile(invocationPath, 'utf8')), [
'runtime-host',
'access',
'list',
'--root',
'/runtime-root',
'--framed',
]);
await execFile(
operator.nodePath,
[operator.modulePath, 'activate', '--framed', '--root-id', 'a'.repeat(64)],
{ env: { ...process.env, MAKA_TEST_OUTPUT: invocationPath } },
);
assert.deepEqual(JSON.parse(await readFile(invocationPath, 'utf8')), [
'runtime-host',
'activate',
'--framed',
'--root-id',
'a'.repeat(64),
]);
await execFile(
operator.nodePath,
[operator.modulePath, 'connect', '--framed', '--root-id', 'a'.repeat(64)],
{ env: { ...process.env, MAKA_TEST_OUTPUT: invocationPath } },
);
assert.deepEqual(JSON.parse(await readFile(invocationPath, 'utf8')), [
'runtime-host',
'connect',
'--framed',
'--root-id',
'a'.repeat(64),
]);
await execFile(
operator.nodePath,
[
operator.modulePath,
'__cleanup-managed-deployment',
'--expected-service-id',
serviceId,
'--expected-root-path',
'/srv/maka',
'--expected-root-id',
'a'.repeat(64),
],
{
env: { ...process.env, MAKA_TEST_OUTPUT: invocationPath },
},
);
assert.deepEqual(JSON.parse(await readFile(invocationPath, 'utf8')), [
'runtime-host',
'service',
'cleanup-deployment',
'--expected-service-id',
serviceId,
'--expected-root-path',
'/srv/maka',
'--expected-root-id',
'a'.repeat(64),
'--client-data-root',
resolveRuntimeHostManagedControlRoot(serviceId),
'--managed-root-id',
serviceId,
'--operator-deployment-id',
'00000000-0000-4000-8000-000000000001',
]);
await writeFile(deployment.cliPath, "process.kill(process.pid, 'SIGTERM');\n");
const signalExit = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
(resolve, reject) => {
const child = spawn(operator.nodePath, [operator.modulePath, 'status'], { stdio: 'ignore' });
child.once('error', reject);
child.once('exit', (code, signal) => resolve({ code, signal }));
},
);
assert.deepEqual(signalExit, { code: null, signal: 'SIGTERM' });
});
test('managed Windows task launcher is projected to a stable deployment path', async (t) => {
const base = await mkdtemp(join(tmpdir(), 'maka-runtime-host-windows-launcher-'));
t.after(() => rm(base, { recursive: true, force: true }));
const sourcePackageRoot = await createReleasePackage(base, '0.2.0');
const sourceLauncher = join(
sourcePackageRoot,
'native',
'runtime-host-windows-task-launcher',
'prebuilds',
'win32-x64',
'maka-runtime-host-task-launcher.exe',
);
await mkdir(dirname(sourceLauncher), { recursive: true });
await writeFile(sourceLauncher, 'launcher-v1');
const serviceId = 'a'.repeat(64);
const deployment = await prepareRuntimeHostManagedPackageDeployment(
{
serviceId,
clientDataRoot: join(base, 'client'),
sourcePackageRoot,
version: '0.2.0',
packageIntegrity: PACKAGE_INTEGRITY,
},
{
env: { XDG_DATA_HOME: join(base, 'data') },
homeDir: join(base, 'home'),
platform: 'linux',
},
);
const config: RuntimeHostManagedDeploymentConfig = {
schemaVersion: 1,
state: 'active',
deploymentId: '00000000-0000-4000-8000-000000000001',
configRevision: 1,
deploymentRoot: deployment.root,
root: { id: serviceId, path: join(base, 'state') },
projectDirectoryRoots: [],
launch: {
kind: 'exact_package',
nodePath: process.execPath,
package: { kind: 'npm_registry', version: '0.2.0', integrity: PACKAGE_INTEGRITY },
},
listeners: { localIpc: true },
lifecycle: { mode: 'supervised', provider: 'windows_task', availability: 'session' },
reconciliation: { trigger: 'scheduled', provider: 'windows_task_timer' },
};
await convergeRuntimeHostManagedWindowsTaskLauncher(config);
const projected = runtimeHostManagedWindowsTaskLauncherPath(
deployment.root,
Buffer.from('launcher-v1'),
);
assert.equal(await readFile(projected, 'utf8'), 'launcher-v1');
assert.equal(await resolveRuntimeHostWindowsTaskLauncherPath(deployment.cliPath), projected);
const packaged = await resolvePackagedRuntimeHostWindowsTaskLauncherPath(deployment.cliPath);
await writeFile(packaged, 'launcher-v2');
await assert.rejects(
verifyRuntimeHostManagedWindowsTaskLauncher(config),
/does not match its deployment/u,
);
await convergeRuntimeHostManagedWindowsTaskLauncher(config);
await verifyRuntimeHostManagedWindowsTaskLauncher(config);
assert.notEqual(
runtimeHostManagedWindowsTaskLauncherPath(deployment.root, Buffer.from('launcher-v2')),
projected,
);
});
async function createReleasePackage(base: string, version: string): Promise<string> {
const root = join(base, `source-package-${version}`);
await mkdir(join(root, 'dist'), { recursive: true });
await mkdir(join(root, 'node_modules', '@maka', 'runtime-host'), {
recursive: true,
});
await writeFile(join(root, 'package.json'), JSON.stringify({ name: 'maka-agent', version }));
await writeFile(join(root, 'dist', 'cli.js'), '#!/usr/bin/env node\n');
await writeFile(
join(root, 'node_modules', '@maka', 'runtime-host', 'package.json'),
JSON.stringify({ name: '@maka/runtime-host', version: '0.1.0' }),
);
return root;
}
function unusedBackend(): RuntimeHostServiceBackend {
return {
preflightDeployment: async () => undefined,
stageDeployment: async () => assert.fail('Backend is not expected'),
replace: async () => assert.fail('Backend is not expected'),
verifyReplacementPreconditions: async () => assert.fail('Backend is not expected'),
verifyDeployment: async () => assert.fail('Backend is not expected'),
status: async () => assert.fail('Backend is not expected'),
start: async () => assert.fail('Backend is not expected'),
stop: async () => assert.fail('Backend is not expected'),
restart: async () => assert.fail('Backend is not expected'),
retire: async () => assert.fail('Backend is not expected'),
logs: async () => assert.fail('Backend is not expected'),
uninstall: async () => assert.fail('Backend is not expected'),
};
}