| /* |
| * 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'), |
| }; |
| } |