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 @@ -143,7 +143,7 @@ function createDesktopRuntimeHostCandidate(
connection,
candidateDeps,
observationRegistry,
'ephemeral',
'owned_ephemeral',
'local',
);
}
Expand Down Expand Up @@ -244,7 +244,7 @@ test('rejects a stale target generation when two profiles share one Host', async
first.connection,
deps(ipc),
undefined,
'ephemeral',
'owned_ephemeral',
'local',
);
await firstCandidate.close();
Expand All @@ -255,7 +255,7 @@ test('rejects a stale target generation when two profiles share one Host', async
second.connection,
deps(ipc),
undefined,
'ephemeral',
'owned_ephemeral',
'local',
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
const owner = await startRuntimeHostDesktopManager({} as DesktopRuntimeHostCandidateStartInput, {
startCandidate: async (input) => {
starts += 1;
interactions.push(input.remote?.sshInteraction);
interactions.push(input.profileTarget?.sshInteraction);
if (starts === 2) {
resolveSecondStart();
await secondReleased;
Expand Down Expand Up @@ -142,9 +142,9 @@ test('quiesces reconnect and waits for the Host process before update install',
});

test('quiesces Local reconnect while a managed service changes', async () => {
const current = candidateHarness({ lifecycleMode: 'service' });
const current = candidateHarness({ ownership: 'supervised' });
const replacement = candidateHarness({
lifecycleMode: 'service',
ownership: 'supervised',
hostEpoch: 'service-after',
});
let starts = 0;
Expand Down Expand Up @@ -477,12 +477,12 @@ test('keeps active-task confirmation bound to the current Host', async () => {
await owner.close();
});

for (const lifecycleMode of ['service', 'remote'] as const) {
test(`does not retire a ${lifecycleMode} Host for a Desktop update`, async () => {
const current = candidateHarness({ lifecycleMode });
for (const ownership of ['supervised', 'external'] as const) {
test(`leaves ${ownership} Host ownership intact during a Desktop update`, async () => {
const current = candidateHarness({ ownership });
const owner = await startRuntimeHostDesktopManager({} as DesktopRuntimeHostCandidateStartInput, {
startCandidate: async () => ready(current.candidate),
waitForHostExit: async () => assert.fail(`${lifecycleMode} Host exit must not be awaited`),
waitForHostExit: async () => assert.fail(`${ownership} Host exit must not be awaited`),
});

const retirement = await owner.retireOwnedLocalHost('refuse_active_work');
Expand All @@ -496,7 +496,7 @@ for (const lifecycleMode of ['service', 'remote'] as const) {

test('keeps Local and remote Hosts active and routes work by owning Host', async () => {
const local = candidateHarness({ hostId: 'host-a' });
const remote = candidateHarness({ hostId: 'host-b', lifecycleMode: 'remote' });
const remote = candidateHarness({ hostId: 'host-b', ownership: 'external' });
let starts = 0;
const manager = await startRuntimeHostDesktopManager(
{} as DesktopRuntimeHostCandidateStartInput,
Expand Down Expand Up @@ -757,7 +757,7 @@ test('bounds an in-flight pairing finalization and preserves its unknown outcome

test('coalesces concurrent enable requests for one remote profile', async () => {
const local = candidateHarness({ hostId: 'host-a' });
const remote = candidateHarness({ hostId: 'host-b', lifecycleMode: 'remote' });
const remote = candidateHarness({ hostId: 'host-b', ownership: 'external' });
let starts = 0;
let releaseRemote!: () => void;
const remoteReady = new Promise<void>((resolve) => {
Expand Down Expand Up @@ -787,7 +787,7 @@ test('coalesces concurrent enable requests for one remote profile', async () =>

test('waits for an in-flight remote enable before closing', async () => {
const local = candidateHarness({ hostId: 'host-a' });
const remote = candidateHarness({ hostId: 'host-b', lifecycleMode: 'remote' });
const remote = candidateHarness({ hostId: 'host-b', ownership: 'external' });
let starts = 0;
let releaseRemote!: () => void;
const remoteReady = new Promise<void>((resolve) => {
Expand Down Expand Up @@ -1159,7 +1159,7 @@ function candidateHarness(
delayDisconnect?: boolean;
disconnectOnPrepare?: boolean;
activeTasks?: boolean | 'always';
lifecycleMode?: 'ephemeral' | 'service' | 'remote';
ownership?: 'owned_ephemeral' | 'supervised' | 'external';
hostId?: string;
hostEpoch?: string;
finalizeFailures?: Error[];
Expand All @@ -1182,7 +1182,7 @@ function candidateHarness(
const retirementModes: string[] = [];
const candidate = {
closed,
hostLifecycleMode: options.lifecycleMode ?? 'ephemeral',
hostOwnership: options.ownership ?? 'owned_ephemeral',
hostPid: 42,
client: {
hostId: options.hostId ?? 'test-host',
Expand Down Expand Up @@ -1273,7 +1273,7 @@ function ready(candidate: DesktopRuntimeHostCandidate): DesktopRuntimeHostCandid
function remoteTarget(
id: string,
target = 'default',
): NonNullable<DesktopRuntimeHostCandidateStartInput['remote']> {
): NonNullable<DesktopRuntimeHostCandidateStartInput['profileTarget']> {
return {
profile: {
id,
Expand Down
68 changes: 67 additions & 1 deletion apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,16 @@ test('persists a verified on-demand SSH profile without endpoint or credential p
});

const result = await harness.invoke('runtime-host-onboarding:start', {
kind: 'ssh',
name: 'Lab',
destination: 'operator@example.com',
projectDirectoryRoots: [{ label: 'Work', path: '/srv/work' }],
});

assert.equal((result as { kind?: string }).kind, 'complete');
assert.equal(saved?.profile.name, 'Lab');
assert.deepEqual(saved?.profile.transport, {
if (saved?.profile.kind !== 'remote') assert.fail('expected remote profile');
assert.deepEqual(saved.profile.transport, {
kind: 'ssh',
destination: 'operator@example.com',
activation: {
Expand Down Expand Up @@ -90,10 +92,68 @@ test('persists a verified on-demand SSH profile without endpoint or credential p
assert.equal(harness.handlers.size, 0);
});

test('onboards WSL as a credential-free environment profile', async () => {
let saved: DesktopRuntimeHostProfileAddInput | undefined;
const harness = createHarness({
profiles: {
addAndEnable: async (input) => {
saved = input;
return { kind: 'connected', snapshot: { entries: [], defaultProfileId: 'local' } };
},
},
runWslSetup: async (_input, _onProgress, onComplete) => {
onComplete();
return {
rootId: 'a'.repeat(64),
operatorPath: '/home/operator/.local/share/maka/operator',
};
},
});

const result = await harness.invoke('runtime-host-onboarding:start', {
kind: 'wsl',
distribution: 'Ubuntu-24.04',
});

assert.equal((result as { kind?: string }).kind, 'complete');
assert.equal(saved?.credential, undefined);
assert.deepEqual(saved?.profile, {
id: saved?.profile.id,
name: 'Ubuntu-24.04',
kind: 'environment',
provider: { kind: 'wsl', distribution: 'Ubuntu-24.04' },
rootId: 'a'.repeat(64),
operatorPath: '/home/operator/.local/share/maka/operator',
});
await harness.onboarding.close();
});

test('projects WSL setup failures as recoverable onboarding state', async () => {
const harness = createHarness({
runWslSetup: async () => {
throw new Error('WSL requires Linux Node.js');
},
});

assert.deepEqual(
await harness.invoke('runtime-host-onboarding:start', {
kind: 'wsl',
distribution: 'Ubuntu',
}),
{
kind: 'failed',
message: 'WSL requires Linux Node.js',
revision: 3,
},
);
await harness.onboarding.close();
});

test('projects invalid setup input as a recoverable failure', async () => {
const harness = createHarness();

const result = await harness.invoke('runtime-host-onboarding:start', {
kind: 'ssh',
destination: '',
});
assert.deepEqual(result, {
Expand All @@ -113,6 +173,7 @@ test('rejects relative remote Project roots before starting SSH setup', async ()
const harness = createHarness();

const result = await harness.invoke('runtime-host-onboarding:start', {
kind: 'ssh',
destination: 'operator@example.com',
projectDirectoryRoots: [{ label: 'Work', path: 'srv/work' }],
});
Expand Down Expand Up @@ -166,6 +227,7 @@ test('finishes Host pairing after the cancellable SSH phase has completed', asyn
});

const setup = harness.invoke('runtime-host-onboarding:start', {
kind: 'ssh',
destination: 'operator@example.com',
}) as Promise<unknown>;
while (!completeReceived) await Promise.resolve();
Expand Down Expand Up @@ -203,6 +265,7 @@ test('resolves the setup package only when onboarding starts', async () => {
assert.equal(resolutions, 0);
assert.deepEqual(
await harness.invoke('runtime-host-onboarding:start', {
kind: 'ssh',
destination: 'operator@example.com',
}),
{
Expand All @@ -227,11 +290,14 @@ function createHarness(overrides: HarnessOverrides = {}) {
const onboarding = createDesktopRuntimeHostOnboarding({
clientInstanceId: 'stable-client',
profiles: {
addAndEnable: async () => assert.fail('profile must not be saved'),
addAndEnableVerified: async () => assert.fail('profile must not be saved'),
...profiles,
},
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'),
listWslDistributions: async () => [],
...rest,
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ describe('useSessionNavigationController', () => {
profileName: 'Remote Mac',
profileKind: 'remote',
}),
session('environment', {
profileId: 'wsl-ubuntu',
profileName: 'Ubuntu',
profileKind: 'environment',
}),
session('archived', { isArchived: true }),
session('hidden'),
];
Expand All @@ -153,7 +158,7 @@ describe('useSessionNavigationController', () => {

assert.deepEqual(
controller().selectors.visibleSessions.map(({ id }) => id),
['root', 'remote'],
['root', 'remote', 'environment'],
);
assert.equal(controller().selectors.activeRowId, 'root');
assert.equal(controller().selectors.activeParentSession?.id, 'root');
Expand All @@ -163,9 +168,10 @@ describe('useSessionNavigationController', () => {
});
assert.deepEqual(
controller().selectors.groups.map(({ id }) => id),
['project:project', 'runtime-host:remote-profile'],
['project:project', 'runtime-host:remote-profile', 'runtime-host:wsl-ubuntu'],
);
assert.equal(controller().selectors.sessionMeta(sessions[2]!), 'Remote Mac');
assert.equal(controller().selectors.sessionMeta(sessions[3]!), 'Ubuntu');
});

it('owns Session jumps and preserves turn-target clearing semantics', async () => {
Expand Down
16 changes: 8 additions & 8 deletions apps/desktop/src/main/__tests__/task-entry-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import type { RuntimeHostProfileKind } from '@maka/runtime-host/profile-kind';
import { UNRESOLVED_NEW_TASK_DRAFT_KEY } from '../../renderer/new-task-reload-intent.js';
import {
resolveProjectSelection,
Expand All @@ -44,19 +45,18 @@ function project(

function readyHost(input: {
id: string;
kind?: 'local' | 'remote';
kind?: RuntimeHostProfileKind;
selectedProjectId?: string | null;
defaultProjectId?: string;
selectNoProject?: boolean;
projects?: ReturnType<typeof project>[];
}): Extract<TaskEntryHost, { state: 'available' }> {
const profile = input.kind === 'remote'
? {
id: input.id,
name: input.id,
kind: 'remote' as const,
}
: { id: 'local' as const, name: 'Local' as const, kind: 'local' as const };
const kind = input.kind ?? 'local';
const profile = {
id: kind === 'local' ? 'local' : input.id,
name: kind === 'local' ? 'Local' : input.id,
kind,
};
return {
profile,
hostId: `host-${input.id}`,
Expand Down
Loading
Loading