blob: 333c18ef27e243305fd1890ad3bb084d839f4e33 [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 { randomUUID } from 'node:crypto';
import { chmod, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import { resolveStorageRoot } from '@maka/storage/root-authority';
import {
claimRuntimeHostManagedDeployment,
resolveRuntimeHostManagedDeploymentConfigPath,
resolveRuntimeHostNpmDeploymentLayout,
type RuntimeHostManagedDeploymentConfig,
} from '@maka/runtime-host/operator';
import { activateLocalManagedRuntimeHost } from '../runtime-host-local-managed-activation.js';
import { connectRuntimeHostCliConnection } from '../runtime-host-cli-context.js';
import {
connectRuntimeHost,
HostHandoffCancelledError,
HostHandoffRequiredError,
} from '@maka/runtime-host/client';
import {
INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
RUNTIME_HOST_COMPATIBILITY_EPOCH,
RUNTIME_HOST_PROTOCOL_VERSION,
type HostIncompatible,
} from '@maka/runtime-host/protocol';
for (const legacy of [false, true]) {
test(`local CLI cold-starts through the installed ${legacy ? 'legacy' : 'Node'} operator`, {
skip: process.platform === 'win32',
timeout: 30_000,
}, async (t) => {
const base = await mkdtemp(join(tmpdir(), 'maka-local-managed-'));
const capability = await resolveStorageRoot({ path: join(base, 'state'), kind: 'interactive' });
const config: RuntimeHostManagedDeploymentConfig = {
schemaVersion: 1,
state: 'active',
deploymentId: randomUUID(),
configRevision: 1,
deploymentRoot: join(base, 'deployment'),
root: { path: capability.canonicalPath, id: capability.rootId },
projectDirectoryRoots: [],
launch: {
kind: 'exact_package',
nodePath: process.execPath,
package: {
kind: 'npm_registry',
version: '1.2.3',
integrity: `sha512-${Buffer.alloc(64, 1).toString('base64')}`,
},
},
listeners: {
localIpc: true,
websocket: { host: '127.0.0.1', port: 0, path: '/runtime-host' },
},
lifecycle: { mode: 'on_demand', availability: 'activation' },
reconciliation: { trigger: 'manual' },
};
const layout = resolveRuntimeHostNpmDeploymentLayout(
config.deploymentRoot,
config.launch.package.integrity,
);
await mkdir(dirname(layout.candidateEntrypoint), { recursive: true });
await symlink(
fileURLToPath(import.meta.resolve('@maka/runtime-host/execution-candidate-main')),
layout.candidateEntrypoint,
);
await claimRuntimeHostManagedDeployment(capability, config);
const modulePath = join(config.deploymentRoot, legacy ? 'legacy-entry.mjs' : 'operator.mjs');
await writeFile(
modulePath,
`
import { activateRuntimeHostManagedDeployment } from ${JSON.stringify(import.meta.resolve('@maka/runtime-host/client'))};
import { encodeRuntimeHostActivationFrame } from ${JSON.stringify(import.meta.resolve('@maka/runtime-host/operator'))};
const result = await activateRuntimeHostManagedDeployment({ rootId: process.argv.at(-1) });
process.stdout.write(encodeRuntimeHostActivationFrame(result));
`,
);
if (legacy) {
const launcher = join(config.deploymentRoot, 'operator');
await writeFile(
launcher,
'#!/bin/sh\nexec ' + "'" + process.execPath + "' '" + modulePath + '\' "$@"\n',
);
await chmod(launcher, 0o700);
}
let first: Awaited<ReturnType<typeof connectRuntimeHostCliConnection>> | undefined;
let second: typeof first;
t.after(async () => {
await second?.close();
if (first) {
const diagnostics = await first.connection.request('host.diagnostics.query', {});
await first.close();
try {
process.kill(diagnostics.pid, 'SIGTERM');
} catch {}
}
await rm(base, { recursive: true, force: true });
await rm(dirname(resolveRuntimeHostManagedDeploymentConfigPath(capability.rootId)), {
recursive: true,
force: true,
});
});
first = await connectRuntimeHostCliConnection({ rootPath: capability.canonicalPath });
second = await connectRuntimeHostCliConnection(
{ rootPath: capability.canonicalPath },
{
activateLocalManagedHost: async () =>
assert.fail('a running managed Host must be joined directly'),
},
);
assert.equal(first.connection.rootId, capability.rootId);
assert.equal(second.connection.hostEpoch, first.connection.hostEpoch);
const observation = await connectRuntimeHost({
rootPath: capability.canonicalPath,
protocol: { min: RUNTIME_HOST_PROTOCOL_VERSION, max: RUNTIME_HOST_PROTOCOL_VERSION },
compositionId: INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
});
assert.equal(observation.kind, 'connected');
if (observation.kind !== 'connected') return;
await observation.connection.close();
const incompatible = {
kind: 'incompatible' as const,
registration: observation.registration,
handshake: {
kind: 'incompatible',
hostEpoch: observation.registration.hostEpoch,
protocolMin: 0,
protocolMax: 0,
compatibilityEpoch: RUNTIME_HOST_COMPATIBILITY_EPOCH - 1,
compositionId: INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID,
compositionRevision: 'legacy',
state: 'ready',
replacement: 'wait_for_idle_exit',
} satisfies HostIncompatible,
};
// Managed on-demand registers as ephemeral too. Both cold activation and
// an already-running conflict must read the actual managed authority, not
// offer a local npm owner claim based on that process lifetime label.
for (const cold of [true, false]) {
let activations = 0;
const dependencies = {
connectOrSpawn: async () =>
cold
? { kind: 'failed' as const, reason: 'managed_root_requires_operator' as const }
: incompatible,
activateLocalManagedHost: async () => {
activations += 1;
},
connectActivatedHost: async () => incompatible,
resolveInstallation: async () => assert.fail('managed Host is not a local npm owner'),
restartDeployment: async () => assert.fail('managed Host must never be replaced here'),
};
await assert.rejects(
connectRuntimeHostCliConnection({ rootPath: capability.canonicalPath }, dependencies),
(error: unknown) => {
assert.ok(error instanceof HostHandoffRequiredError);
assert.equal(error.view.reason, 'operator_required');
assert.deepEqual(error.view.actions, ['cancel', 'retry']);
assert.equal(error.view.mayExitNaturally, false);
return true;
},
);
assert.equal(activations, cold ? 1 : 0);
await assert.rejects(
connectRuntimeHostCliConnection(
{
rootPath: capability.canonicalPath,
handoffSurface: (submit) => ({
update: (view) => submit(view.revision, 'cancel'),
close() {},
}),
},
dependencies,
),
HostHandoffCancelledError,
);
}
// Exercise the real operator subprocess failure contract, including nonzero exit.
await writeFile(
modulePath,
`
import { encodeRuntimeHostActivationFrame } from ${JSON.stringify(import.meta.resolve('@maka/runtime-host/operator'))};
process.stdout.write(encodeRuntimeHostActivationFrame({schemaVersion: 1, kind: 'error', error: {code: 'activation_failed', message: 'Operator recovery is required'}}));
process.exitCode = 1;
`,
);
await assert.rejects(activateLocalManagedRuntimeHost({ rootPath: capability.canonicalPath }), {
message: 'Operator recovery is required',
});
await writeFile(modulePath, `process.stdout.write('not an activation frame');`);
await assert.rejects(
activateLocalManagedRuntimeHost({ rootPath: capability.canonicalPath }),
/invalid activation result/,
);
const controller = new AbortController();
controller.abort(new Error('activation cancelled'));
await assert.rejects(
activateLocalManagedRuntimeHost({
rootPath: capability.canonicalPath,
signal: controller.signal,
}),
/activation cancelled/,
);
});
}