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
5 changes: 3 additions & 2 deletions docs/windows-test-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ Locations intentionally omit line numbers so unrelated edits do not invalidate t
| Classification | Count |
|---|---:|
| windows-backend-gap | 27 |
| portable-candidate | 18 |
| portable-candidate | 19 |
| platform-contract | 31 |

Total Windows-excluded declarations: **76**
Total Windows-excluded declarations: **77**

## Inventory

Expand All @@ -32,6 +32,7 @@ Total Windows-excluded declarations: **76**
| platform-contract | `apps/desktop/src/main/__tests__/shell-env.test.ts` bounds shell output instead of buffering until the global timeout | `process.platform === 'win32'` |
| portable-candidate | `packages/cli/src/__tests__/pi-transcript.test.ts` shortens POSIX paths under the home directory | `process.platform === 'win32'` |
| portable-candidate | `packages/cli/src/__tests__/pi-transcript.test.ts` keeps POSIX paths outside the home directory absolute | `process.platform === 'win32'` |
| portable-candidate | `packages/cli/src/__tests__/runtime-host-local-managed-activation.test.ts` local CLI cold-starts through the installed ${legacy ? 'legacy' : 'Node'} operator | `process.platform === 'win32'` |
| portable-candidate | `packages/cli/src/__tests__/runtime-host-setup.test.ts` managed operator binds its Client Data Root and routes deployment cleanup | `process.platform === 'win32'` |
| portable-candidate | `packages/eval/src/__tests__/install-preflight.test.ts` rejects an unusable trials root before invoking external prerequisites | `process.platform === 'win32' \|\| process.geteuid?.() === 0` |
| windows-backend-gap | `packages/runtime-host/src/__tests__/connection-effect-coordinator.test.ts` leaves canonical onboarding state unchanged when the durable intent cannot be published | `process.platform === 'win32'` |
Expand Down
101 changes: 101 additions & 0 deletions packages/cli/src/__tests__/runtime-host-cli-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -714,3 +714,104 @@ async function waitFor(predicate: () => boolean): Promise<void> {
}
assert.fail('condition was not reached');
}

test('local CLI delegates a managed cold start once and reconnects without a launch claim', async () => {
const calls: string[] = [];
const connection = {
rootId: 'root-id',
hostEpoch: 'host-epoch',
connectionId: 'connection-id',
closed: new Promise<void>(() => {}),
close: async () => {},
subscribeConfigurationChanges: () => () => {},
subscribeConnectionCatalogChanges: () => () => {},
subscribeProjectCatalogChanges: () => () => {},
subscribeSessionCatalogChanges: () => () => {},
subscribeScheduledTaskChanges: () => () => {},
} as unknown as RuntimeHostConnection;
const context = await connectRuntimeHostCliConnection(
{ rootPath: '/managed-root' },
{
connectOrSpawn: async (input) => {
assert.equal(input.managedLaunchClaim, undefined);
calls.push('connect');
return calls.length === 1
? { kind: 'failed', reason: 'managed_root_requires_operator' }
: connectedHostResult(connection);
},
connectActivatedHost: async () => {
calls.push('connect');
return connectedHostResult(connection);
},
activateLocalManagedHost: async (input) => {
assert.equal(input.rootPath, '/managed-root');
calls.push('operator');
},
},
);
assert.deepEqual(calls, ['connect', 'operator', 'connect']);
await context.close();
});

test('local CLI does not loop if operator activation fails to make the Host available', async () => {
let activations = 0;
await assert.rejects(
connectRuntimeHostCliConnection(
{ rootPath: '/managed-root' },
{
connectOrSpawn: async () => ({ kind: 'failed', reason: 'managed_root_requires_operator' }),
connectActivatedHost: async () => ({ kind: 'unavailable', reason: 'not_registered' }),
activateLocalManagedHost: async () => {
activations += 1;
},
},
),
/could not join it \(not_registered\)/,
);
assert.equal(activations, 1);
});

test('local CLI propagates operator failure without attempting unmanaged recovery', async () => {
let connections = 0;
const failure = new Error('operator failed');
await assert.rejects(
connectRuntimeHostCliConnection(
{ rootPath: '/managed-root' },
{
connectOrSpawn: async () => {
connections += 1;
return { kind: 'failed', reason: 'managed_root_requires_operator' };
},
activateLocalManagedHost: async () => {
throw failure;
},
},
),
(error) => error === failure,
);
assert.equal(connections, 1);
});

test('activated managed Host incompatibility stays operator-owned', async () => {
await assert.rejects(
connectRuntimeHostCliConnection(
{ rootPath: '/managed-root' },
{
connectOrSpawn: async () => ({ kind: 'failed', reason: 'managed_root_requires_operator' }),
activateLocalManagedHost: async () => {},
connectActivatedHost: async () => ({
kind: 'incompatible',
registration: hostRegistration(),
handshake: incompatibleRemoteHandshake(),
}),
},
),
(error: unknown) => {
assert.ok(error instanceof Error);
assert.ok(!(error instanceof RuntimeHostCliConflictError));
assert.match(error.message, /incompatible/);
assert.match(error.message, /configured operator/);
return true;
},
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* 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';

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);
// 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/,
);
});
}
32 changes: 31 additions & 1 deletion packages/cli/src/runtime-host-cli-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* under the License.
*/

import { activateLocalManagedRuntimeHost } from './runtime-host-local-managed-activation.js';
import { randomUUID } from 'node:crypto';
import { join } from 'node:path';
import { NO_REAL_CONNECTION_CODE } from '@maka/core/connection-error-copy';
Expand All @@ -27,6 +28,7 @@ import type {
import type { ChatDefaultPermissionMode } from '@maka/core/settings';
import {
connectOrSpawnRuntimeHost,
connectRuntimeHost,
connectRuntimeHostProfile,
createClientRuntimeHostProfileCatalog,
createRuntimeHostPeerClientFromEnvironment,
Expand Down Expand Up @@ -121,6 +123,8 @@ export interface RuntimeHostCliTarget {

interface RuntimeHostCliContextDeps {
readonly connectOrSpawn: typeof connectOrSpawnRuntimeHost;
readonly connectActivatedHost: typeof connectRuntimeHost;
readonly activateLocalManagedHost: typeof activateLocalManagedRuntimeHost;
readonly connectProfile: typeof connectRuntimeHostProfile;
readonly readConnectionCatalog: typeof readRuntimeHostConnectionCatalog;
readonly loadClientInstanceId: typeof loadOrCreateRuntimeHostClientInstanceId;
Expand Down Expand Up @@ -154,6 +158,8 @@ export async function connectRuntimeHostCliConnection(
): Promise<RuntimeHostCliConnectionOnlyContextWithIdentity> {
const deps: RuntimeHostCliContextDeps = {
connectOrSpawn: connectOrSpawnRuntimeHost,
activateLocalManagedHost: activateLocalManagedRuntimeHost,
connectActivatedHost: connectRuntimeHost,
connectProfile: connectRuntimeHostProfile,
readConnectionCatalog: readRuntimeHostConnectionCatalog,
loadClientInstanceId: loadOrCreateRuntimeHostClientInstanceId,
Expand Down Expand Up @@ -197,10 +203,34 @@ export async function connectRuntimeHostCliConnection(
...(signal ? { signal } : {}),
});
}
const connected = await deps.connectOrSpawn({
let connected = await deps.connectOrSpawn({
...connectInput,
...(signal ? { signal } : {}),
});
if (connected.kind === 'failed' && connected.reason === 'managed_root_requires_operator') {
await deps.activateLocalManagedHost({
rootPath: input.rootPath,
...(signal ? { signal } : {}),
});
signal?.throwIfAborted();
// Rejoin over Local IPC. The operator retains launch and update authority.
const activated = await deps.connectActivatedHost(connectInput);
if (activated.kind === 'connected') {
if (signal?.aborted) {
await activated.connection.close();
signal.throwIfAborted();
}
connected = activated;
} else {
const ConnectionError =
activated.kind === 'incompatible' || activated.kind === 'upgrade_required'
? RuntimeHostPermanentReconnectError
: Error;
throw new ConnectionError(
`The installed Runtime Host was activated but the local CLI could not join it (${activated.kind === 'unavailable' ? activated.reason : activated.kind}). Use a compatible CLI or update the managed Host through its configured operator.`,
);
}
}
if (connected.kind === 'incompatible') {
throw new RuntimeHostCliConflictError(connected.handshake, connected.registration);
}
Expand Down
Loading