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 @@ -39,7 +39,7 @@ test('retains a successful Guest mount and rehydrates the same authority after r
},
});

const result = await first.importInvitation(invitation('guest-one'), false);
const result = await first.importInvitation(invitation('guest-one'), false, 'import-one');
assert.equal(result.kind, 'connected');
if (result.kind !== 'connected') return;
await first.close();
Expand Down Expand Up @@ -68,7 +68,7 @@ test('removes failed activation desire instead of creating recoverable profile s
},
});

const result = await mounts.importInvitation(invitation('guest-two'), false);
const result = await mounts.importInvitation(invitation('guest-two'), false, 'import-two');
assert.deepEqual(result.kind === 'error' ? result.reason : result.kind, 'peer_path_unavailable');
assert.deepEqual(await store.read(), []);
assert.equal(unmounted.length, 1);
Expand All @@ -94,7 +94,7 @@ test('settles admitted finalization before committing unmount desire', async ()
throw new Error('connection shutdown failed');
},
});
const importing = mounts.importInvitation(invitation('guest-three'), false);
const importing = mounts.importInvitation(invitation('guest-three'), false, 'import-three');
await finalizing;
const [retained] = await store.read();
assert.ok(retained);
Expand Down Expand Up @@ -204,8 +204,9 @@ test('settles admitted finalization before closing and retains the mount', async
},
});

const importing = mounts.importInvitation(invitation('guest-closing'), false);
const importing = mounts.importInvitation(invitation('guest-closing'), false, 'import-closing');
await finalizing;
assert.equal(mounts.cancelImport('import-closing'), 'settling');
let closed = false;
const closing = mounts.close().then(() => {
closed = true;
Expand Down Expand Up @@ -235,7 +236,7 @@ test('retains and reconciles a mount when finalization outcome is unknown', asyn
},
});

const result = await mounts.importInvitation(invitation('guest-unknown'), false);
const result = await mounts.importInvitation(invitation('guest-unknown'), false, 'import-unknown');
assert.equal(result.kind, 'error');
assert.equal((await store.read()).length, 1);
await reconciled;
Expand All @@ -244,6 +245,34 @@ test('retains and reconciles a mount when finalization outcome is unknown', asyn
await mounts.close();
});

test('cancels an in-flight import and removes its durable mount desire', async () => {
const store = memoryStore();
let connecting!: () => void;
const started = new Promise<void>((resolve) => {
connecting = resolve;
});
const mounts = service(store, {
mount: async (_target, signal) => {
connecting();
await new Promise<void>((_resolve, reject) => {
signal.addEventListener('abort', () => reject(signal.reason), { once: true });
});
},
});

const importing = mounts.importInvitation(
invitation('guest-cancelled'),
false,
'import-cancelled',
);
await started;
assert.equal(mounts.cancelImport('import-cancelled'), 'cancelled');

assert.equal((await importing).kind, 'error');
assert.deepEqual(await store.read(), []);
await mounts.close();
});

function service(
store: GuestSessionMountStore,
overrides: {
Expand Down
88 changes: 88 additions & 0 deletions apps/desktop/src/main/__tests__/runtime-host-management.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@

import assert from 'node:assert/strict';
import { test } from 'node:test';
import { RuntimeHostOperationError } from '@maka/runtime-host/client';
import {
RUNTIME_HOST_OPERATOR_PEER_RELAY_DISCOVERY_CAPABILITY,
runtimeHostAccessCredentialFingerprint,
type RuntimeHostServiceManagementFrame,
} from '@maka/runtime-host/operator';
import { createDesktopRuntimeHostManagement } from '../runtime-host-management.js';
import { createDesktopRuntimeHostPeerMeshManagement } from '../runtime-host-peer-mesh-management.js';
import type { DesktopRuntimeHostManagementProvider } from '../runtime-host-management-provider.js';
import type {
DesktopRuntimeHostSshAccessInput,
Expand All @@ -37,6 +39,92 @@ import type {

const DEPLOYMENT_ID = '11111111-1111-4111-8111-111111111111';

test('cancels a live Runtime Host Mesh status query', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
let markStarted!: () => void;
const started = new Promise<void>((resolve) => {
markStarted = resolve;
});
const pending = new Promise<never>(() => undefined);
const management = createDesktopRuntimeHostPeerMeshManagement({
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
},
localHost: {
getSnapshot: async () => assert.fail('status must use the live Host'),
inspectManaged: async () => assert.fail('status must use the live Host'),
},
runLocal: async () => assert.fail('status must use the live Host'),
liveHost: () => ({
request: ((operation: string) => {
assert.equal(operation, 'peer.mesh.query');
markStarted();
return pending;
}) as never,
}),
profiles: {
resolveManagedService: async () => assert.fail('status must use the live Host'),
},
runRemote: async () => assert.fail('status must use the live Host'),
});
const execute = handlers.get('runtime-host-peer-mesh:execute');
const cancel = handlers.get('runtime-host-peer-mesh:cancel');
assert.ok(execute && cancel);

const status = execute(
{},
{ kind: 'local_host' },
'status',
undefined,
undefined,
undefined,
undefined,
'status-1',
) as Promise<unknown>;
await started;
cancel({}, 'status-1');
await assert.rejects(status, /cancelled/u);
management.close();
});

test('projects an unknown live Mesh mutation outcome across IPC', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const management = createDesktopRuntimeHostPeerMeshManagement({
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
},
localHost: {
getSnapshot: async () => assert.fail('mutation must use the live Host'),
inspectManaged: async () => assert.fail('mutation must use the live Host'),
},
runLocal: async () => assert.fail('mutation must use the live Host'),
liveHost: () => ({
request: (() =>
Promise.reject(
new RuntimeHostOperationError(
'peer.mesh.close',
'commit_outcome_unknown',
'Mesh close outcome is unknown',
),
)) as never,
}),
profiles: {
resolveManagedService: async () => assert.fail('mutation must use the live Host'),
},
runRemote: async () => assert.fail('mutation must use the live Host'),
});
const execute = handlers.get('runtime-host-peer-mesh:execute');
assert.ok(execute);

assert.deepEqual(
await execute({}, { kind: 'local_host' }, 'close', 'mesh-1', undefined, undefined, undefined, 'close-1'),
{ kind: 'outcome_unknown' },
);
management.close();
});

test('requires explicit interruption authority before a provider restarts active work', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const provider = {
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/main/runtime-host-boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,14 +514,15 @@ const runtimeHostProfileService = createDesktopRuntimeHostProfileService({
});
const guestSessionMountService = createDesktopGuestSessionMountService({
store: createGuestSessionMountStore(runtimeHostCredentialStore),
mount: async (target, signal) => {
mount: async (target, signal, onConnectionPhase) => {
if (target.profile.kind !== 'remote' || !target.credential) {
throw new Error('A shared Session requires a remote Guest target');
}
if (!runtimeHostManager) throw new Error('Runtime Host manager is unavailable');
await runtimeHostManager.mountGuest(
{ profile: target.profile, credential: target.credential },
signal,
onConnectionPhase,
);
},
finalizeAccess: async (mountId, signal) => {
Expand Down
11 changes: 9 additions & 2 deletions apps/desktop/src/main/runtime-host-desktop-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
type ResolvedRuntimeHostProfile,
type RuntimeHostReconnectBackoff,
type RuntimeHostReconnectLifecycle,
type RuntimeHostConnectionPhase,
type RuntimeHostRetirementMode,
type RuntimeHostSshInteraction,
} from '@maka/runtime-host/client';
Expand Down Expand Up @@ -70,6 +71,7 @@ export interface RuntimeHostDesktopManager {
mountGuest(
profileTarget: NonNullable<DesktopRuntimeHostCandidateStartInput['profileTarget']>,
signal?: AbortSignal,
onConnectionPhase?: (phase: RuntimeHostConnectionPhase) => void,
): Promise<void>;
finalizeGuestAccess(mountId: string, signal?: AbortSignal): Promise<void>;
unmountGuest(mountId: string): Promise<void>;
Expand Down Expand Up @@ -500,19 +502,21 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager {
mountGuest(
profileTarget: NonNullable<DesktopRuntimeHostCandidateStartInput['profileTarget']>,
signal?: AbortSignal,
onConnectionPhase?: (phase: RuntimeHostConnectionPhase) => void,
): Promise<void> {
if (!isSessionGuestProfile(profileTarget.profile)) {
return Promise.reject(new Error('A Session Guest target is required'));
}
return this.#mutateTarget(profileTarget.profile.id, () =>
this.#enable(profileTarget, true, signal),
this.#enable(profileTarget, true, signal, onConnectionPhase),
);
}

async #enable(
profileTarget: NonNullable<DesktopRuntimeHostCandidateStartInput['profileTarget']>,
allowSameRoot: boolean,
signal?: AbortSignal,
onConnectionPhase?: (phase: RuntimeHostConnectionPhase) => void,
): Promise<void> {
signal?.throwIfAborted();
if (this.#closed) throw new Error('Desktop Runtime Host manager is closed');
Expand Down Expand Up @@ -540,7 +544,10 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager {
) return;
if (existing) await this.#removeTarget(existing);

const target = this.#createTarget(withRuntimeHostTarget(this.#baseInput, profileTarget));
const target = this.#createTarget({
...withRuntimeHostTarget(this.#baseInput, profileTarget),
...(onConnectionPhase ? { onConnectionPhase } : {}),
});
this.#targets.set(profileId, target);
this.#publishState(target, {
epoch: target.epoch,
Expand Down
Loading
Loading