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
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,9 @@ test('publishes update progress and waits for the managed profile to reconnect',
runUpdatePolicy: async () => assert.fail('update policy is not expected'),
runUpdateReconciliation: async () =>
assert.fail('update reconciliation is not expected'),
setupPackageMode: 'published',
resolveSshDevelopmentPeerTarget: async () =>
assert.fail('published update must not inspect the development target'),
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.3.0' }),
currentHostEpoch: () => 'host-before-update',
awaitUpdatedConnection: async (...args) => {
Expand Down Expand Up @@ -1175,6 +1178,9 @@ function unusedUpdateDependencies() {
runPeerManagement: async (): Promise<never> =>
assert.fail('direct peer management is not expected'),
directPeerClientAvailable: false,
setupPackageMode: 'published' as const,
resolveSshDevelopmentPeerTarget: async (): Promise<never> =>
assert.fail('published update must not inspect the development target'),
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.2.3' } as const),
currentHostEpoch: () => undefined,
awaitUpdatedConnection: async () => undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ test('persists a verified on-demand SSH profile without endpoint or credential p

test('onboards WSL as a credential-free environment profile', async () => {
let saved: DesktopRuntimeHostProfileAddInput | undefined;
const peerTargets: string[] = [];
const harness = createHarness({
profiles: {
addAndEnable: async (input) => {
Expand All @@ -108,6 +109,10 @@ test('onboards WSL as a credential-free environment profile', async () => {
operatorPath: '/home/operator/.local/share/maka/operator',
};
},
resolveSetupPackage: (peerTarget) => {
peerTargets.push(peerTarget);
return { kind: 'npm', specifier: 'maka-agent@0.2.0' };
},
});

const result = await harness.invoke('runtime-host-onboarding:start', {
Expand All @@ -125,6 +130,7 @@ test('onboards WSL as a credential-free environment profile', async () => {
rootId: 'a'.repeat(64),
operatorPath: '/home/operator/.local/share/maka/operator',
});
assert.deepEqual(peerTargets, ['none']);
await harness.onboarding.close();
});

Expand Down Expand Up @@ -294,6 +300,9 @@ function createHarness(overrides: HarnessOverrides = {}) {
addAndEnableVerified: async () => assert.fail('profile must not be saved'),
...profiles,
},
setupPackageMode: 'published',
resolveSshDevelopmentPeerTarget: async () =>
assert.fail('published setup must not inspect the development target'),
resolveSetupPackage: () => ({ kind: 'npm', specifier: 'maka-agent@0.2.0' }),
runSetup: async () => assert.fail('SSH must not start'),
runWslSetup: async () => assert.fail('WSL must not start'),
Expand Down
24 changes: 16 additions & 8 deletions apps/desktop/src/main/__tests__/runtime-host-setup-package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,19 @@ import { join, resolve } from 'node:path';
import { test } from 'node:test';
import { createRuntimeHostSetupPackageResolver } from '../runtime-host-setup-package.js';

test('development setup lazily builds one local CLI archive unless explicitly overridden', async () => {
test('development setup lazily caches CLI archives by peer target unless overridden', async () => {
const repoRoot = resolve('/workspace');
const archive = join(repoRoot, 'packages', 'cli', 'release', 'maka-agent-dev.tgz');
let builds = 0;
let closes = 0;
const targets: string[] = [];
const resolvePackage = createRuntimeHostSetupPackageResolver({
isPackaged: false,
appPath: join(repoRoot, 'apps', 'desktop'),
environment: {},
startDevelopmentArchiveBuild: (resolvedRoot) => {
startDevelopmentArchiveBuild: (resolvedRoot, target) => {
builds += 1;
targets.push(target);
assert.equal(resolvedRoot, repoRoot);
return {
result: Promise.resolve(archive),
Expand All @@ -44,7 +46,11 @@ test('development setup lazily builds one local CLI archive unless explicitly ov
},
});

assert.deepEqual(await Promise.all([resolvePackage.resolve(), resolvePackage.resolve()]), [
assert.equal(resolvePackage.mode, 'development');
assert.deepEqual(await Promise.all([
resolvePackage.resolve('linux-x64'),
resolvePackage.resolve('linux-x64'),
]), [
{
kind: 'development_archive',
path: archive,
Expand All @@ -54,7 +60,9 @@ test('development setup lazily builds one local CLI archive unless explicitly ov
path: archive,
},
]);
assert.equal(builds, 1);
await resolvePackage.resolve('none');
assert.equal(builds, 2);
assert.deepEqual(targets, ['linux-x64', 'none']);

const override = join(tmpdir(), 'explicit.tgz');
const resolveOverride = createRuntimeHostSetupPackageResolver({
Expand All @@ -63,12 +71,12 @@ test('development setup lazily builds one local CLI archive unless explicitly ov
environment: { MAKA_RUNTIME_HOST_SETUP_ARCHIVE: override },
startDevelopmentArchiveBuild: () => assert.fail('override must bypass the local build'),
});
assert.deepEqual(await resolveOverride.resolve(), {
assert.deepEqual(await resolveOverride.resolve('none'), {
kind: 'development_archive',
path: override,
});
await Promise.all([resolvePackage.close(), resolveOverride.close()]);
assert.equal(closes, 1);
assert.equal(closes, 2);
});

test('cancelling the last waiter closes its build before a new setup starts', async () => {
Expand Down Expand Up @@ -107,10 +115,10 @@ test('cancelling the last waiter closes its build before a new setup starts', as
},
});

const first = resolver.resolve(cancelled.signal);
const first = resolver.resolve('linux-x64', cancelled.signal);
cancelled.abort(new Error('setup cancelled'));
await closeStarted;
const second = resolver.resolve();
const second = resolver.resolve('linux-x64');
await Promise.resolve();
assert.equal(builds, 1);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,37 @@ import {
RUNTIME_HOST_OPERATOR_PEER_MANAGEMENT_CAPABILITY,
RUNTIME_HOST_SETUP_FRAME_PREFIX,
} from '@maka/runtime-host/operator';
import { createDesktopRuntimeHostSshTerminal } from '../runtime-host-ssh-terminal.js';
import {
createDesktopRuntimeHostSshTerminal,
runtimeHostDevelopmentPeerTargetFromUname,
} from '../runtime-host-ssh-terminal.js';

test('maps supported SSH uname identities to development peer targets', () => {
assert.equal(runtimeHostDevelopmentPeerTargetFromUname('Linux', 'x86_64'), 'linux-x64');
assert.equal(runtimeHostDevelopmentPeerTargetFromUname('Linux', 'aarch64'), 'linux-arm64');
assert.equal(runtimeHostDevelopmentPeerTargetFromUname('Darwin', 'arm64'), 'darwin-arm64');
assert.throws(
() => runtimeHostDevelopmentPeerTargetFromUname('Linux', 'riscv64'),
/not available/u,
);
});

test('detects the development peer target through the bounded SSH preflight', async () => {
const harness = createHarness('pending');
const detection = harness.terminal.resolveDevelopmentPeerTarget({
destination: 'operator@example.com',
});
await waitFor(() => harness.pty.hasDataListener());
const command = harness.launchArgs[0]?.at(-1) ?? '';
const marker = command.match(/__MAKA_RUNTIME_HOST_TARGET_[0-9a-f]+__/u)?.[0];
assert.ok(marker);
harness.pty.emitData(`${marker}Linux:x86_64\r\n`);
harness.pty.exit(0);

assert.equal(await detection, 'linux-x64');
assert.doesNotMatch(JSON.stringify(harness.events), /MAKA_RUNTIME_HOST_TARGET/u);
await harness.terminal.close();
});

test('keeps a connecting SSH prompt observable across renderer presentation changes', async () => {
const harness = createHarness('pending');
Expand Down
14 changes: 12 additions & 2 deletions apps/desktop/src/main/runtime-host-boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,10 @@ import {
createDesktopRuntimeHostSshTerminal,
} from "./runtime-host-ssh-terminal.js";
import { runDesktopRuntimeHostWslSetup } from './runtime-host-wsl-controller.js';
import { createRuntimeHostSetupPackageResolver } from "./runtime-host-setup-package.js";
import {
createRuntimeHostSetupPackageResolver,
desktopRuntimeHostDevelopmentPeerTarget,
} from "./runtime-host-setup-package.js";
import { configureDesktopRuntimeHostPeerClient } from './runtime-host-peer-client.js';
import { createDesktopRuntimeHostLocalOperator } from './runtime-host-local-operator.js';
import { createDesktopLocalRuntimeHostRemoteAccess } from './runtime-host-local-remote-access.js';
Expand Down Expand Up @@ -399,7 +402,10 @@ const localRuntimeHostRemoteAccess = createDesktopLocalRuntimeHostRemoteAccess({
rootId: startupLocalStorageRoot.rootId,
directPeerAvailable: runtimeHostDirectPeerAvailable,
manager: () => runtimeHostManager,
resolveSetupPackage: runtimeHostSetupPackage.resolve,
resolveSetupPackage: (signal) => runtimeHostSetupPackage.resolve(
desktopRuntimeHostDevelopmentPeerTarget(),
signal,
),
operator: localRuntimeHostOperator,
});
const native = assembleDesktopNativeCapabilities({
Expand Down Expand Up @@ -474,6 +480,8 @@ const runtimeHostOnboarding = createDesktopRuntimeHostOnboarding({
runSetup: runtimeHostSshTerminal.runSetup,
runWslSetup: runDesktopRuntimeHostWslSetup,
listWslDistributions: listRuntimeHostWslDistributions,
setupPackageMode: runtimeHostSetupPackage.mode,
resolveSshDevelopmentPeerTarget: runtimeHostSshTerminal.resolveDevelopmentPeerTarget,
resolveSetupPackage: runtimeHostSetupPackage.resolve,
send: (snapshot) =>
mainWindowController.send("runtime-host-onboarding:changed", snapshot),
Expand All @@ -487,6 +495,8 @@ const runtimeHostManagement = createDesktopRuntimeHostManagement({
runUpdate: runtimeHostSshTerminal.runUpdate,
runUpdatePolicy: runtimeHostSshTerminal.runUpdatePolicy,
runUpdateReconciliation: runtimeHostSshTerminal.runUpdateReconciliation,
setupPackageMode: runtimeHostSetupPackage.mode,
resolveSshDevelopmentPeerTarget: runtimeHostSshTerminal.resolveDevelopmentPeerTarget,
resolveUpdatePackage: runtimeHostSetupPackage.resolve,
currentHostEpoch: (profileId) =>
runtimeHostManager?.current(profileId)?.candidate?.client.hostEpoch,
Expand Down
19 changes: 17 additions & 2 deletions apps/desktop/src/main/runtime-host-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import type {
RuntimeHostServiceUpdateReconciliationTerminalFrame,
RuntimeHostServiceUpdateTerminalFrame,
} from './runtime-host-ssh-terminal.js';
import type { DesktopRuntimeHostDevelopmentPeerTarget } from './runtime-host-setup-package.js';

const MANAGEMENT_ACTIONS = new Set<DesktopRuntimeHostManagementAction>([
'status',
Expand Down Expand Up @@ -104,7 +105,15 @@ export function createDesktopRuntimeHostManagement(input: {
input: DesktopRuntimeHostSshUpdateReconciliationInput,
onProgress: (phase: DesktopRuntimeHostManagementProgress['phase']) => void,
) => Promise<RuntimeHostServiceUpdateReconciliationTerminalFrame>;
readonly resolveUpdatePackage: () =>
readonly setupPackageMode: 'published' | 'development';
readonly resolveSshDevelopmentPeerTarget: (input: {
readonly destination: string;
readonly sshPort?: number;
readonly signal?: AbortSignal;
}) => Promise<Exclude<DesktopRuntimeHostDevelopmentPeerTarget, 'none'>>;
readonly resolveUpdatePackage: (
peerTarget: DesktopRuntimeHostDevelopmentPeerTarget,
) =>
| DesktopRuntimeHostSetupPackage
| Promise<DesktopRuntimeHostSetupPackage>;
readonly currentHostEpoch: (profileId: string) => string | undefined;
Expand Down Expand Up @@ -466,7 +475,13 @@ export function createDesktopRuntimeHostManagement(input: {
await managedMutationTarget(profileIdValue);
const previousHostEpoch = input.currentHostEpoch(profileId);
input.sendProgress({ profileId, phase: 'preparing_cli' });
const setupPackage = await input.resolveUpdatePackage();
const peerTarget = input.setupPackageMode === 'development'
? await input.resolveSshDevelopmentPeerTarget({
destination: transport.destination,
...(transport.sshPort === undefined ? {} : { sshPort: transport.sshPort }),
})
: 'none';
const setupPackage = await input.resolveUpdatePackage(peerTarget);
const response = await input.runUpdate(
{
destination: transport.destination,
Expand Down
32 changes: 30 additions & 2 deletions apps/desktop/src/main/runtime-host-onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type {
DesktopRuntimeHostSshSetupInput,
} from './runtime-host-ssh-terminal.js';
import type { DesktopRuntimeHostWslSetupInput } from './runtime-host-wsl-controller.js';
import type { DesktopRuntimeHostDevelopmentPeerTarget } from './runtime-host-setup-package.js';
import { requireProjectDirectoryRoots } from '../shared/runtime-host-project-directory-policy.js';

type OnboardingState = DesktopRuntimeHostOnboardingSnapshot extends infer Snapshot
Expand Down Expand Up @@ -65,7 +66,14 @@ export function createDesktopRuntimeHostOnboarding(input: {
) => Promise<{ readonly rootId: string; readonly operatorPath: string }>;
readonly listWslDistributions: () => Promise<readonly string[]>;
readonly send: (snapshot: DesktopRuntimeHostOnboardingSnapshot) => void;
readonly setupPackageMode: 'published' | 'development';
readonly resolveSshDevelopmentPeerTarget: (input: {
readonly destination: string;
readonly sshPort?: number;
readonly signal?: AbortSignal;
}) => Promise<Exclude<DesktopRuntimeHostDevelopmentPeerTarget, 'none'>>;
readonly resolveSetupPackage: (
peerTarget: DesktopRuntimeHostDevelopmentPeerTarget,
signal?: AbortSignal,
) => DesktopRuntimeHostSetupPackage | Promise<DesktopRuntimeHostSetupPackage>;
}): { close(): Promise<void> } {
Expand Down Expand Up @@ -113,8 +121,14 @@ export function createDesktopRuntimeHostOnboarding(input: {
signal: AbortSignal,
): Promise<DesktopRuntimeHostOnboardingSnapshot> => {
try {
const setupPackage = await input.resolveSetupPackage(signal);
if (request.kind === 'wsl') return await runWsl(request, setupPackage, signal);
if (request.kind === 'wsl') {
const setupPackage = await input.resolveSetupPackage('none', signal);
return await runWsl(request, setupPackage, signal);
}
const peerTarget = input.setupPackageMode === 'development'
? await resolveSshDevelopmentPeerTarget(request, signal)
: 'none';
const setupPackage = await input.resolveSetupPackage(peerTarget, signal);
const lifecycle = setupPackage.kind === 'npm' ? 'on_demand' : 'supervised';
signal.throwIfAborted();
publish({ kind: 'running', phase: 'connecting_ssh' });
Expand Down Expand Up @@ -203,6 +217,20 @@ export function createDesktopRuntimeHostOnboarding(input: {
}
};

const resolveSshDevelopmentPeerTarget = async (
request: Extract<DesktopRuntimeHostOnboardingInput, { readonly kind: 'ssh' }>,
signal: AbortSignal,
): Promise<Exclude<DesktopRuntimeHostDevelopmentPeerTarget, 'none'>> => {
publish({ kind: 'running', phase: 'connecting_ssh' });
const target = await input.resolveSshDevelopmentPeerTarget({
destination: request.destination,
...(request.sshPort === undefined ? {} : { sshPort: request.sshPort }),
signal,
});
publish({ kind: 'running', phase: 'preparing_cli' });
return target;
};

const runWsl = async (
request: Extract<DesktopRuntimeHostOnboardingInput, { readonly kind: 'wsl' }>,
setupPackage: DesktopRuntimeHostSetupPackage,
Expand Down
Loading
Loading