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 numberDiff line numberDiff line change
Expand Up@@ -41,8 +41,8 @@ import {
} from '../runtime-host-desktop-manager.js';

test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, async () => {
const first = candidateHarness({ delayDisconnect: true });
const second = candidateHarness();
const first = candidateHarness({ delayDisconnect: true, hostEpoch: 'host-before' });
const second = candidateHarness({ hostEpoch: 'host-after' });
const queue = [ready(first.candidate), ready(second.candidate)];
let starts = 0;
const interactions: Array<string | undefined> = [];
Expand DownExpand Up@@ -71,6 +71,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
});

first.disconnect();
const replacementReady = owner.waitUntilReady(owner.defaultProfileId(), 'host-before');
const botMessage = owner.handleBotIncomingMessage({ text: 'hello' } as BotIncomingMessage);
const stop = owner.stopSession({
hostId: 'test-host',
Expand All@@ -95,7 +96,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
assert.equal(second.botMessages, 0);
assert.deepEqual(second.stoppedSessions, []);
releaseSecond();
await Promise.all([botMessage, stop]);
await Promise.all([botMessage, stop, replacementReady]);

assert.equal(first.botMessages, 0);
assert.equal(second.botMessages, 1);
Expand DownExpand Up@@ -815,6 +816,7 @@ function candidateHarness(
activeTasks?: boolean;
lifecycleMode?: 'ephemeral' | 'service' | 'remote';
hostId?: string;
hostEpoch?: string;
finalizeFailures?: Error[];
disconnectOnFinalizeFailure?: boolean;
onPrepare?: () => void;
Expand All@@ -837,6 +839,7 @@ function candidateHarness(
hostLifecycleMode: options.lifecycleMode ?? 'ephemeral',
client: {
hostId: options.hostId ?? 'test-host',
hostEpoch: options.hostEpoch ?? 'test-host-epoch',
get lifecycleState() {
return lifecycleState;
},
Expand Down
146 changes: 146 additions & 0 deletions apps/desktop/src/main/__tests__/runtime-host-management.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@ import type {
DesktopRuntimeHostSshAccessInput,
DesktopRuntimeHostSshCleanupInput,
DesktopRuntimeHostSshManagementInput,
DesktopRuntimeHostSshUpdateInput,
} from '../runtime-host-ssh-terminal.js';

test('identifies, rotates, and revokes managed credentials without exposing secrets', async () => {
Expand DownExpand Up@@ -64,6 +65,7 @@ test('identifies, rotates, and revokes managed credentials without exposing secr
];

createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -185,6 +187,8 @@ test('manages only the service identity bound by Desktop onboarding', async () =
const uninstallOrder: string[] = [];
let operatorAccess = false;
let cleared = 0;
let statusGate: Promise<void> | undefined;
let releaseStatus: (() => void) | undefined;
const managedProfile = {
id: 'office',
name: 'Office',
Expand All@@ -203,6 +207,7 @@ test('manages only the service identity bound by Desktop onboarding', async () =
operatorPath: '/home/operator/.local/share/maka/operator',
};
const management = createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand All@@ -229,6 +234,7 @@ test('manages only the service identity bound by Desktop onboarding', async () =
},
runServiceManagement: async (input) => {
managementInputs.push(input);
if (input.action === 'status') await statusGate;
if (input.action === 'uninstall') {
uninstallOrder.push('uninstall-service');
}
Expand All@@ -243,6 +249,19 @@ test('manages only the service identity bound by Desktop onboarding', async () =
const run = handlers.get('runtime-host-management:run');
assert.ok(run);

statusGate = new Promise((resolve) => {
releaseStatus = resolve;
});
const firstStatus = run({}, 'office', 'status');
const secondStatus = run({}, 'office', 'status');
await Promise.resolve();
await Promise.resolve();
assert.equal(managementInputs.length, 1);
releaseStatus?.();
await Promise.all([firstStatus, secondStatus]);
statusGate = undefined;
managementInputs.length = 0;

await assert.rejects(
run({}, 'manual', 'uninstall') as Promise<unknown>,
/not bound to a managed service/u,
Expand DownExpand Up@@ -309,6 +328,121 @@ test('manages only the service identity bound by Desktop onboarding', async () =
assert.equal(handlers.size, 0);
});

test('publishes update progress and waits for the managed profile to reconnect', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const updates: DesktopRuntimeHostSshUpdateInput[] = [];
const progress: unknown[] = [];
const connectionCompletions: unknown[] = [];
let failConnection = false;
let bindingPresent = true;
let removeBindingAfterUpdate = false;
const profile = {
id: 'office',
name: 'Office',
kind: 'remote' as const,
rootId: 'a'.repeat(64),
transport: {
kind: 'ssh' as const,
destination: 'operator@example.com',
remotePort: 7443,
websocketPath: '/runtime-host',
},
};
const service = {
id: 'b'.repeat(64),
rootPath: '/srv/maka',
operatorPath: '/home/operator/.local/share/maka/operator',
};
createDesktopRuntimeHostManagement({
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
},
profiles: {
resolveManagedService: async () =>
bindingPresent ? { profile, service, state: 'active' as const } : undefined,
resolveManagedAccess: async () => undefined,
rotateManagedCredential: async () => assert.fail('credential rotation is not expected'),
markManagedServiceUninstalling: async (binding) => binding,
markManagedServiceCleanupPending: async (binding) => binding,
clearManagedServiceBinding: async () => undefined,
},
runServiceManagement: async () => assert.fail('ordinary management is not expected'),
runUpdate: async (input, onProgress) => {
updates.push(input);
onProgress('staging');
if (removeBindingAfterUpdate) bindingPresent = false;
return {
schemaVersion: 1,
kind: 'result',
action: 'update',
service: {
platform: 'linux',
arch: 'x64',
osRelease: '6.8.0',
state: 'running',
pid: 43,
lastExitCode: 0,
installedVersion: '1.3.0',
projectDirectoryRoots: [],
},
operatorCapabilities: ['access-management-v1'],
update: { kind: 'updated', previousVersion: '1.2.3', targetVersion: '1.3.0' },
};
},
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.3.0' }),
currentHostEpoch: () => 'host-before-update',
awaitUpdatedConnection: async (...args) => {
connectionCompletions.push(args);
if (failConnection) throw new Error('authentication required');
},
sendProgress: (event) => progress.push(event),
runAccessManagement: async () => assert.fail('access management is not expected'),
cleanupManagedDeployment: async () => assert.fail('cleanup is not expected'),
});

const update = handlers.get('runtime-host-management:update');
assert.ok(update);
const response = await update({}, profile.id, false);
assert.equal((response as { accessManagementAvailable: boolean }).accessManagementAvailable, true);
assert.deepEqual(updates, [{
destination: profile.transport.destination,
setupPackage: { kind: 'npm', specifier: 'maka-agent@1.3.0' },
expectedTarget: {
serviceId: service.id,
rootPath: service.rootPath,
rootId: profile.rootId,
},
}]);
assert.deepEqual(progress, [{ profileId: profile.id, phase: 'staging' }]);
assert.deepEqual(connectionCompletions, [
[profile.id, profile.rootId, 'host-before-update', true],
]);

removeBindingAfterUpdate = true;
const changedProfile = await update({}, profile.id, false);
assert.equal(
(changedProfile as { kind: string; error?: { message: string } }).error?.message,
'The Runtime Host update completed, but Desktop could not reconnect: ' +
'Runtime Host profile changed while its service was updating',
);

bindingPresent = true;
removeBindingAfterUpdate = false;
failConnection = true;
const reconnectFailure = await update({}, profile.id, false);
assert.deepEqual(reconnectFailure, {
schemaVersion: 1,
kind: 'error',
action: 'update',
error: {
code: 'desktop_reconnect_failed',
message:
'The Runtime Host update completed, but Desktop could not reconnect: authentication required',
},
});
});

test('resumes deployment cleanup without invoking the removed operator', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const profile = {
Expand All@@ -333,6 +467,7 @@ test('resumes deployment cleanup without invoking the removed operator', async (
let state: 'active' | 'uninstalling' | 'cleanup_pending' = 'active';
let clearAttempts = 0;
createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -384,6 +519,7 @@ test('rechecks uninstall intent before retrying the remote service', async () =>
const handlers = new Map<string, (...args: unknown[]) => unknown>();
let marked = false;
createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -464,6 +600,16 @@ function serviceResult(
: { ...result, action };
}

function unusedUpdateDependencies() {
return {
runUpdate: async (): Promise<never> => assert.fail('update is not expected'),
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.2.3' } as const),
currentHostEpoch: () => undefined,
awaitUpdatedConnection: async () => undefined,
sendProgress: () => undefined,
};
}

function accessCredential(
credentialId: string,
principalId: string,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,43 @@ test("keeps Local enabled while a new remote Host connects", async () => {
);
});

test("reconnects an enabled remote Host with interactive SSH", async () => {
const root = await clientRoot();
const catalog = createClientRuntimeHostProfileCatalog(root);
await catalog.create(MANAGED_PROFILE, "opaque-token");
const calls: string[] = [];
const service = createDesktopRuntimeHostProfileService({
clientDataRoot: root,
startup: {
preferences: {
schemaVersion: 2,
defaultProfileId: LOCAL_RUNTIME_HOST_PROFILE.id,
enabledRemoteProfileIds: [MANAGED_PROFILE.id],
},
pairingIntents: [],
remotes: [{ profile: MANAGED_PROFILE, credential: "opaque-token" }],
unavailable: new Map(),
},
catalog,
states: () => [connectingLocal()],
enable: async (target, interaction) => {
calls.push(`enable:${target.profile.id}:${interaction}`);
},
disable: async (profileId) => {
calls.push(`disable:${profileId}`);
},
setDefault: () => undefined,
finalizePairing: async () => undefined,
});

await service.reconnect(MANAGED_PROFILE.id, MANAGED_PROFILE.rootId);

assert.deepEqual(calls, [
`disable:${MANAGED_PROFILE.id}`,
`enable:${MANAGED_PROFILE.id}:terminal`,
]);
});

test("does not enable the same State Root twice", async () => {
const root = await clientRoot();
const startup = await resolveDesktopRuntimeHostStartup(root);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -308,6 +308,70 @@ test('keeps a received management result when SSH teardown times out', async ()
await harness.terminal.close();
});

test('runs an exact update package and reports progress before an active-work result', async () => {
const harness = createHarness('pending');
const phases: string[] = [];
const update = harness.terminal.runUpdate(
{
destination: 'operator@example.com',
setupPackage: { kind: 'npm', specifier: 'maka-agent@1.3.0' },
expectedTarget: {
serviceId: 'b'.repeat(64),
rootPath: '/srv/maka',
rootId: 'a'.repeat(64),
},
},
(phase) => phases.push(phase),
);
await waitFor(() => harness.pty.hasDataListener());
const remoteCommand = harness.launchArgs.at(-1)?.at(-1) ?? '';
assert.match(remoteCommand, /--package.*maka-agent@1\.3\.0/u);
assert.match(remoteCommand, /runtime-host.*service.*update/u);
assert.match(remoteCommand, /MAKA_RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST/u);
harness.pty.emitData('Password: ');
harness.pty.emitData(
encodeRuntimeHostServiceManagementFrame({
schemaVersion: 1,
kind: 'progress',
action: 'update',
phase: 'retiring',
currentVersion: '1.2.3',
targetVersion: '1.3.0',
}),
);
harness.pty.emitData(
encodeRuntimeHostServiceManagementFrame({
schemaVersion: 1,
kind: 'result',
action: 'update',
service: {
platform: 'linux',
arch: 'x64',
osRelease: '6.8.0',
state: 'running',
pid: 42,
lastExitCode: 0,
installedVersion: '1.2.3',
projectDirectoryRoots: [],
},
update: {
kind: 'active_tasks',
currentVersion: '1.2.3',
targetVersion: '1.3.0',
},
}),
);
harness.pty.exit(1);

const result = await update;
assert.equal(result.kind, 'result');
assert.equal(result.kind === 'result' ? result.update.kind : undefined, 'active_tasks');
assert.deepEqual(phases, ['retiring']);
assert.deepEqual(harness.events.map(({ kind }) => kind), ['opened', 'data', 'connected']);
assert.doesNotMatch(JSON.stringify(harness.events), /MAKA_RUNTIME_HOST_SERVICE/u);
await harness.terminal.close();
});

test('keeps a prepared access credential out of the SSH terminal projection', async () => {
const harness = createHarness('pending');
const credential = 'maka_rh_secret-replacement';
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(runtime-host): add safe managed Host updates by M4n5ter · Pull Request #3591 · apache/maka · GitHub
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 numberDiff line numberDiff line change
Expand Up@@ -41,8 +41,8 @@ import {
} from '../runtime-host-desktop-manager.js';

test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, async () => {
const first = candidateHarness({ delayDisconnect: true });
const second = candidateHarness();
const first = candidateHarness({ delayDisconnect: true, hostEpoch: 'host-before' });
const second = candidateHarness({ hostEpoch: 'host-after' });
const queue = [ready(first.candidate), ready(second.candidate)];
let starts = 0;
const interactions: Array<string | undefined> = [];
Expand DownExpand Up@@ -71,6 +71,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
});

first.disconnect();
const replacementReady = owner.waitUntilReady(owner.defaultProfileId(), 'host-before');
const botMessage = owner.handleBotIncomingMessage({ text: 'hello' } as BotIncomingMessage);
const stop = owner.stopSession({
hostId: 'test-host',
Expand All@@ -95,7 +96,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
assert.equal(second.botMessages, 0);
assert.deepEqual(second.stoppedSessions, []);
releaseSecond();
await Promise.all([botMessage, stop]);
await Promise.all([botMessage, stop, replacementReady]);

assert.equal(first.botMessages, 0);
assert.equal(second.botMessages, 1);
Expand DownExpand Up@@ -815,6 +816,7 @@ function candidateHarness(
activeTasks?: boolean;
lifecycleMode?: 'ephemeral' | 'service' | 'remote';
hostId?: string;
hostEpoch?: string;
finalizeFailures?: Error[];
disconnectOnFinalizeFailure?: boolean;
onPrepare?: () => void;
Expand All@@ -837,6 +839,7 @@ function candidateHarness(
hostLifecycleMode: options.lifecycleMode ?? 'ephemeral',
client: {
hostId: options.hostId ?? 'test-host',
hostEpoch: options.hostEpoch ?? 'test-host-epoch',
get lifecycleState() {
return lifecycleState;
},
Expand Down
146 changes: 146 additions & 0 deletions apps/desktop/src/main/__tests__/runtime-host-management.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@ import type {
DesktopRuntimeHostSshAccessInput,
DesktopRuntimeHostSshCleanupInput,
DesktopRuntimeHostSshManagementInput,
DesktopRuntimeHostSshUpdateInput,
} from '../runtime-host-ssh-terminal.js';

test('identifies, rotates, and revokes managed credentials without exposing secrets', async () => {
Expand DownExpand Up@@ -64,6 +65,7 @@ test('identifies, rotates, and revokes managed credentials without exposing secr
];

createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -185,6 +187,8 @@ test('manages only the service identity bound by Desktop onboarding', async () =
const uninstallOrder: string[] = [];
let operatorAccess = false;
let cleared = 0;
let statusGate: Promise<void> | undefined;
let releaseStatus: (() => void) | undefined;
const managedProfile = {
id: 'office',
name: 'Office',
Expand All@@ -203,6 +207,7 @@ test('manages only the service identity bound by Desktop onboarding', async () =
operatorPath: '/home/operator/.local/share/maka/operator',
};
const management = createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand All@@ -229,6 +234,7 @@ test('manages only the service identity bound by Desktop onboarding', async () =
},
runServiceManagement: async (input) => {
managementInputs.push(input);
if (input.action === 'status') await statusGate;
if (input.action === 'uninstall') {
uninstallOrder.push('uninstall-service');
}
Expand All@@ -243,6 +249,19 @@ test('manages only the service identity bound by Desktop onboarding', async () =
const run = handlers.get('runtime-host-management:run');
assert.ok(run);

statusGate = new Promise((resolve) => {
releaseStatus = resolve;
});
const firstStatus = run({}, 'office', 'status');
const secondStatus = run({}, 'office', 'status');
await Promise.resolve();
await Promise.resolve();
assert.equal(managementInputs.length, 1);
releaseStatus?.();
await Promise.all([firstStatus, secondStatus]);
statusGate = undefined;
managementInputs.length = 0;

await assert.rejects(
run({}, 'manual', 'uninstall') as Promise<unknown>,
/not bound to a managed service/u,
Expand DownExpand Up@@ -309,6 +328,121 @@ test('manages only the service identity bound by Desktop onboarding', async () =
assert.equal(handlers.size, 0);
});

test('publishes update progress and waits for the managed profile to reconnect', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const updates: DesktopRuntimeHostSshUpdateInput[] = [];
const progress: unknown[] = [];
const connectionCompletions: unknown[] = [];
let failConnection = false;
let bindingPresent = true;
let removeBindingAfterUpdate = false;
const profile = {
id: 'office',
name: 'Office',
kind: 'remote' as const,
rootId: 'a'.repeat(64),
transport: {
kind: 'ssh' as const,
destination: 'operator@example.com',
remotePort: 7443,
websocketPath: '/runtime-host',
},
};
const service = {
id: 'b'.repeat(64),
rootPath: '/srv/maka',
operatorPath: '/home/operator/.local/share/maka/operator',
};
createDesktopRuntimeHostManagement({
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
},
profiles: {
resolveManagedService: async () =>
bindingPresent ? { profile, service, state: 'active' as const } : undefined,
resolveManagedAccess: async () => undefined,
rotateManagedCredential: async () => assert.fail('credential rotation is not expected'),
markManagedServiceUninstalling: async (binding) => binding,
markManagedServiceCleanupPending: async (binding) => binding,
clearManagedServiceBinding: async () => undefined,
},
runServiceManagement: async () => assert.fail('ordinary management is not expected'),
runUpdate: async (input, onProgress) => {
updates.push(input);
onProgress('staging');
if (removeBindingAfterUpdate) bindingPresent = false;
return {
schemaVersion: 1,
kind: 'result',
action: 'update',
service: {
platform: 'linux',
arch: 'x64',
osRelease: '6.8.0',
state: 'running',
pid: 43,
lastExitCode: 0,
installedVersion: '1.3.0',
projectDirectoryRoots: [],
},
operatorCapabilities: ['access-management-v1'],
update: { kind: 'updated', previousVersion: '1.2.3', targetVersion: '1.3.0' },
};
},
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.3.0' }),
currentHostEpoch: () => 'host-before-update',
awaitUpdatedConnection: async (...args) => {
connectionCompletions.push(args);
if (failConnection) throw new Error('authentication required');
},
sendProgress: (event) => progress.push(event),
runAccessManagement: async () => assert.fail('access management is not expected'),
cleanupManagedDeployment: async () => assert.fail('cleanup is not expected'),
});

const update = handlers.get('runtime-host-management:update');
assert.ok(update);
const response = await update({}, profile.id, false);
assert.equal((response as { accessManagementAvailable: boolean }).accessManagementAvailable, true);
assert.deepEqual(updates, [{
destination: profile.transport.destination,
setupPackage: { kind: 'npm', specifier: 'maka-agent@1.3.0' },
expectedTarget: {
serviceId: service.id,
rootPath: service.rootPath,
rootId: profile.rootId,
},
}]);
assert.deepEqual(progress, [{ profileId: profile.id, phase: 'staging' }]);
assert.deepEqual(connectionCompletions, [
[profile.id, profile.rootId, 'host-before-update', true],
]);

removeBindingAfterUpdate = true;
const changedProfile = await update({}, profile.id, false);
assert.equal(
(changedProfile as { kind: string; error?: { message: string } }).error?.message,
'The Runtime Host update completed, but Desktop could not reconnect: ' +
'Runtime Host profile changed while its service was updating',
);

bindingPresent = true;
removeBindingAfterUpdate = false;
failConnection = true;
const reconnectFailure = await update({}, profile.id, false);
assert.deepEqual(reconnectFailure, {
schemaVersion: 1,
kind: 'error',
action: 'update',
error: {
code: 'desktop_reconnect_failed',
message:
'The Runtime Host update completed, but Desktop could not reconnect: authentication required',
},
});
});

test('resumes deployment cleanup without invoking the removed operator', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const profile = {
Expand All@@ -333,6 +467,7 @@ test('resumes deployment cleanup without invoking the removed operator', async (
let state: 'active' | 'uninstalling' | 'cleanup_pending' = 'active';
let clearAttempts = 0;
createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -384,6 +519,7 @@ test('rechecks uninstall intent before retrying the remote service', async () =>
const handlers = new Map<string, (...args: unknown[]) => unknown>();
let marked = false;
createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -464,6 +600,16 @@ function serviceResult(
: { ...result, action };
}

function unusedUpdateDependencies() {
return {
runUpdate: async (): Promise<never> => assert.fail('update is not expected'),
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.2.3' } as const),
currentHostEpoch: () => undefined,
awaitUpdatedConnection: async () => undefined,
sendProgress: () => undefined,
};
}

function accessCredential(
credentialId: string,
principalId: string,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,43 @@ test("keeps Local enabled while a new remote Host connects", async () => {
);
});

test("reconnects an enabled remote Host with interactive SSH", async () => {
const root = await clientRoot();
const catalog = createClientRuntimeHostProfileCatalog(root);
await catalog.create(MANAGED_PROFILE, "opaque-token");
const calls: string[] = [];
const service = createDesktopRuntimeHostProfileService({
clientDataRoot: root,
startup: {
preferences: {
schemaVersion: 2,
defaultProfileId: LOCAL_RUNTIME_HOST_PROFILE.id,
enabledRemoteProfileIds: [MANAGED_PROFILE.id],
},
pairingIntents: [],
remotes: [{ profile: MANAGED_PROFILE, credential: "opaque-token" }],
unavailable: new Map(),
},
catalog,
states: () => [connectingLocal()],
enable: async (target, interaction) => {
calls.push(`enable:${target.profile.id}:${interaction}`);
},
disable: async (profileId) => {
calls.push(`disable:${profileId}`);
},
setDefault: () => undefined,
finalizePairing: async () => undefined,
});

await service.reconnect(MANAGED_PROFILE.id, MANAGED_PROFILE.rootId);

assert.deepEqual(calls, [
`disable:${MANAGED_PROFILE.id}`,
`enable:${MANAGED_PROFILE.id}:terminal`,
]);
});

test("does not enable the same State Root twice", async () => {
const root = await clientRoot();
const startup = await resolveDesktopRuntimeHostStartup(root);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -308,6 +308,70 @@ test('keeps a received management result when SSH teardown times out', async ()
await harness.terminal.close();
});

test('runs an exact update package and reports progress before an active-work result', async () => {
const harness = createHarness('pending');
const phases: string[] = [];
const update = harness.terminal.runUpdate(
{
destination: 'operator@example.com',
setupPackage: { kind: 'npm', specifier: 'maka-agent@1.3.0' },
expectedTarget: {
serviceId: 'b'.repeat(64),
rootPath: '/srv/maka',
rootId: 'a'.repeat(64),
},
},
(phase) => phases.push(phase),
);
await waitFor(() => harness.pty.hasDataListener());
const remoteCommand = harness.launchArgs.at(-1)?.at(-1) ?? '';
assert.match(remoteCommand, /--package.*maka-agent@1\.3\.0/u);
assert.match(remoteCommand, /runtime-host.*service.*update/u);
assert.match(remoteCommand, /MAKA_RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST/u);
harness.pty.emitData('Password: ');
harness.pty.emitData(
encodeRuntimeHostServiceManagementFrame({
schemaVersion: 1,
kind: 'progress',
action: 'update',
phase: 'retiring',
currentVersion: '1.2.3',
targetVersion: '1.3.0',
}),
);
harness.pty.emitData(
encodeRuntimeHostServiceManagementFrame({
schemaVersion: 1,
kind: 'result',
action: 'update',
service: {
platform: 'linux',
arch: 'x64',
osRelease: '6.8.0',
state: 'running',
pid: 42,
lastExitCode: 0,
installedVersion: '1.2.3',
projectDirectoryRoots: [],
},
update: {
kind: 'active_tasks',
currentVersion: '1.2.3',
targetVersion: '1.3.0',
},
}),
);
harness.pty.exit(1);

const result = await update;
assert.equal(result.kind, 'result');
assert.equal(result.kind === 'result' ? result.update.kind : undefined, 'active_tasks');
assert.deepEqual(phases, ['retiring']);
assert.deepEqual(harness.events.map(({ kind }) => kind), ['opened', 'data', 'connected']);
assert.doesNotMatch(JSON.stringify(harness.events), /MAKA_RUNTIME_HOST_SERVICE/u);
await harness.terminal.close();
});

test('keeps a prepared access credential out of the SSH terminal projection', async () => {
const harness = createHarness('pending');
const credential = 'maka_rh_secret-replacement';
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(runtime-host): add safe managed Host updates by M4n5ter · Pull Request #3591 · apache/maka · GitHub
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 numberDiff line numberDiff line change
Expand Up@@ -41,8 +41,8 @@ import {
} from '../runtime-host-desktop-manager.js';

test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, async () => {
const first = candidateHarness({ delayDisconnect: true });
const second = candidateHarness();
const first = candidateHarness({ delayDisconnect: true, hostEpoch: 'host-before' });
const second = candidateHarness({ hostEpoch: 'host-after' });
const queue = [ready(first.candidate), ready(second.candidate)];
let starts = 0;
const interactions: Array<string | undefined> = [];
Expand DownExpand Up@@ -71,6 +71,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
});

first.disconnect();
const replacementReady = owner.waitUntilReady(owner.defaultProfileId(), 'host-before');
const botMessage = owner.handleBotIncomingMessage({ text: 'hello' } as BotIncomingMessage);
const stop = owner.stopSession({
hostId: 'test-host',
Expand All@@ -95,7 +96,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
assert.equal(second.botMessages, 0);
assert.deepEqual(second.stoppedSessions, []);
releaseSecond();
await Promise.all([botMessage, stop]);
await Promise.all([botMessage, stop, replacementReady]);

assert.equal(first.botMessages, 0);
assert.equal(second.botMessages, 1);
Expand DownExpand Up@@ -815,6 +816,7 @@ function candidateHarness(
activeTasks?: boolean;
lifecycleMode?: 'ephemeral' | 'service' | 'remote';
hostId?: string;
hostEpoch?: string;
finalizeFailures?: Error[];
disconnectOnFinalizeFailure?: boolean;
onPrepare?: () => void;
Expand All@@ -837,6 +839,7 @@ function candidateHarness(
hostLifecycleMode: options.lifecycleMode ?? 'ephemeral',
client: {
hostId: options.hostId ?? 'test-host',
hostEpoch: options.hostEpoch ?? 'test-host-epoch',
get lifecycleState() {
return lifecycleState;
},
Expand Down
146 changes: 146 additions & 0 deletions apps/desktop/src/main/__tests__/runtime-host-management.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@ import type {
DesktopRuntimeHostSshAccessInput,
DesktopRuntimeHostSshCleanupInput,
DesktopRuntimeHostSshManagementInput,
DesktopRuntimeHostSshUpdateInput,
} from '../runtime-host-ssh-terminal.js';

test('identifies, rotates, and revokes managed credentials without exposing secrets', async () => {
Expand DownExpand Up@@ -64,6 +65,7 @@ test('identifies, rotates, and revokes managed credentials without exposing secr
];

createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -185,6 +187,8 @@ test('manages only the service identity bound by Desktop onboarding', async () =
const uninstallOrder: string[] = [];
let operatorAccess = false;
let cleared = 0;
let statusGate: Promise<void> | undefined;
let releaseStatus: (() => void) | undefined;
const managedProfile = {
id: 'office',
name: 'Office',
Expand All@@ -203,6 +207,7 @@ test('manages only the service identity bound by Desktop onboarding', async () =
operatorPath: '/home/operator/.local/share/maka/operator',
};
const management = createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand All@@ -229,6 +234,7 @@ test('manages only the service identity bound by Desktop onboarding', async () =
},
runServiceManagement: async (input) => {
managementInputs.push(input);
if (input.action === 'status') await statusGate;
if (input.action === 'uninstall') {
uninstallOrder.push('uninstall-service');
}
Expand All@@ -243,6 +249,19 @@ test('manages only the service identity bound by Desktop onboarding', async () =
const run = handlers.get('runtime-host-management:run');
assert.ok(run);

statusGate = new Promise((resolve) => {
releaseStatus = resolve;
});
const firstStatus = run({}, 'office', 'status');
const secondStatus = run({}, 'office', 'status');
await Promise.resolve();
await Promise.resolve();
assert.equal(managementInputs.length, 1);
releaseStatus?.();
await Promise.all([firstStatus, secondStatus]);
statusGate = undefined;
managementInputs.length = 0;

await assert.rejects(
run({}, 'manual', 'uninstall') as Promise<unknown>,
/not bound to a managed service/u,
Expand DownExpand Up@@ -309,6 +328,121 @@ test('manages only the service identity bound by Desktop onboarding', async () =
assert.equal(handlers.size, 0);
});

test('publishes update progress and waits for the managed profile to reconnect', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const updates: DesktopRuntimeHostSshUpdateInput[] = [];
const progress: unknown[] = [];
const connectionCompletions: unknown[] = [];
let failConnection = false;
let bindingPresent = true;
let removeBindingAfterUpdate = false;
const profile = {
id: 'office',
name: 'Office',
kind: 'remote' as const,
rootId: 'a'.repeat(64),
transport: {
kind: 'ssh' as const,
destination: 'operator@example.com',
remotePort: 7443,
websocketPath: '/runtime-host',
},
};
const service = {
id: 'b'.repeat(64),
rootPath: '/srv/maka',
operatorPath: '/home/operator/.local/share/maka/operator',
};
createDesktopRuntimeHostManagement({
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
},
profiles: {
resolveManagedService: async () =>
bindingPresent ? { profile, service, state: 'active' as const } : undefined,
resolveManagedAccess: async () => undefined,
rotateManagedCredential: async () => assert.fail('credential rotation is not expected'),
markManagedServiceUninstalling: async (binding) => binding,
markManagedServiceCleanupPending: async (binding) => binding,
clearManagedServiceBinding: async () => undefined,
},
runServiceManagement: async () => assert.fail('ordinary management is not expected'),
runUpdate: async (input, onProgress) => {
updates.push(input);
onProgress('staging');
if (removeBindingAfterUpdate) bindingPresent = false;
return {
schemaVersion: 1,
kind: 'result',
action: 'update',
service: {
platform: 'linux',
arch: 'x64',
osRelease: '6.8.0',
state: 'running',
pid: 43,
lastExitCode: 0,
installedVersion: '1.3.0',
projectDirectoryRoots: [],
},
operatorCapabilities: ['access-management-v1'],
update: { kind: 'updated', previousVersion: '1.2.3', targetVersion: '1.3.0' },
};
},
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.3.0' }),
currentHostEpoch: () => 'host-before-update',
awaitUpdatedConnection: async (...args) => {
connectionCompletions.push(args);
if (failConnection) throw new Error('authentication required');
},
sendProgress: (event) => progress.push(event),
runAccessManagement: async () => assert.fail('access management is not expected'),
cleanupManagedDeployment: async () => assert.fail('cleanup is not expected'),
});

const update = handlers.get('runtime-host-management:update');
assert.ok(update);
const response = await update({}, profile.id, false);
assert.equal((response as { accessManagementAvailable: boolean }).accessManagementAvailable, true);
assert.deepEqual(updates, [{
destination: profile.transport.destination,
setupPackage: { kind: 'npm', specifier: 'maka-agent@1.3.0' },
expectedTarget: {
serviceId: service.id,
rootPath: service.rootPath,
rootId: profile.rootId,
},
}]);
assert.deepEqual(progress, [{ profileId: profile.id, phase: 'staging' }]);
assert.deepEqual(connectionCompletions, [
[profile.id, profile.rootId, 'host-before-update', true],
]);

removeBindingAfterUpdate = true;
const changedProfile = await update({}, profile.id, false);
assert.equal(
(changedProfile as { kind: string; error?: { message: string } }).error?.message,
'The Runtime Host update completed, but Desktop could not reconnect: ' +
'Runtime Host profile changed while its service was updating',
);

bindingPresent = true;
removeBindingAfterUpdate = false;
failConnection = true;
const reconnectFailure = await update({}, profile.id, false);
assert.deepEqual(reconnectFailure, {
schemaVersion: 1,
kind: 'error',
action: 'update',
error: {
code: 'desktop_reconnect_failed',
message:
'The Runtime Host update completed, but Desktop could not reconnect: authentication required',
},
});
});

test('resumes deployment cleanup without invoking the removed operator', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const profile = {
Expand All@@ -333,6 +467,7 @@ test('resumes deployment cleanup without invoking the removed operator', async (
let state: 'active' | 'uninstalling' | 'cleanup_pending' = 'active';
let clearAttempts = 0;
createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -384,6 +519,7 @@ test('rechecks uninstall intent before retrying the remote service', async () =>
const handlers = new Map<string, (...args: unknown[]) => unknown>();
let marked = false;
createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -464,6 +600,16 @@ function serviceResult(
: { ...result, action };
}

function unusedUpdateDependencies() {
return {
runUpdate: async (): Promise<never> => assert.fail('update is not expected'),
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.2.3' } as const),
currentHostEpoch: () => undefined,
awaitUpdatedConnection: async () => undefined,
sendProgress: () => undefined,
};
}

function accessCredential(
credentialId: string,
principalId: string,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,43 @@ test("keeps Local enabled while a new remote Host connects", async () => {
);
});

test("reconnects an enabled remote Host with interactive SSH", async () => {
const root = await clientRoot();
const catalog = createClientRuntimeHostProfileCatalog(root);
await catalog.create(MANAGED_PROFILE, "opaque-token");
const calls: string[] = [];
const service = createDesktopRuntimeHostProfileService({
clientDataRoot: root,
startup: {
preferences: {
schemaVersion: 2,
defaultProfileId: LOCAL_RUNTIME_HOST_PROFILE.id,
enabledRemoteProfileIds: [MANAGED_PROFILE.id],
},
pairingIntents: [],
remotes: [{ profile: MANAGED_PROFILE, credential: "opaque-token" }],
unavailable: new Map(),
},
catalog,
states: () => [connectingLocal()],
enable: async (target, interaction) => {
calls.push(`enable:${target.profile.id}:${interaction}`);
},
disable: async (profileId) => {
calls.push(`disable:${profileId}`);
},
setDefault: () => undefined,
finalizePairing: async () => undefined,
});

await service.reconnect(MANAGED_PROFILE.id, MANAGED_PROFILE.rootId);

assert.deepEqual(calls, [
`disable:${MANAGED_PROFILE.id}`,
`enable:${MANAGED_PROFILE.id}:terminal`,
]);
});

test("does not enable the same State Root twice", async () => {
const root = await clientRoot();
const startup = await resolveDesktopRuntimeHostStartup(root);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -308,6 +308,70 @@ test('keeps a received management result when SSH teardown times out', async ()
await harness.terminal.close();
});

test('runs an exact update package and reports progress before an active-work result', async () => {
const harness = createHarness('pending');
const phases: string[] = [];
const update = harness.terminal.runUpdate(
{
destination: 'operator@example.com',
setupPackage: { kind: 'npm', specifier: 'maka-agent@1.3.0' },
expectedTarget: {
serviceId: 'b'.repeat(64),
rootPath: '/srv/maka',
rootId: 'a'.repeat(64),
},
},
(phase) => phases.push(phase),
);
await waitFor(() => harness.pty.hasDataListener());
const remoteCommand = harness.launchArgs.at(-1)?.at(-1) ?? '';
assert.match(remoteCommand, /--package.*maka-agent@1\.3\.0/u);
assert.match(remoteCommand, /runtime-host.*service.*update/u);
assert.match(remoteCommand, /MAKA_RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST/u);
harness.pty.emitData('Password: ');
harness.pty.emitData(
encodeRuntimeHostServiceManagementFrame({
schemaVersion: 1,
kind: 'progress',
action: 'update',
phase: 'retiring',
currentVersion: '1.2.3',
targetVersion: '1.3.0',
}),
);
harness.pty.emitData(
encodeRuntimeHostServiceManagementFrame({
schemaVersion: 1,
kind: 'result',
action: 'update',
service: {
platform: 'linux',
arch: 'x64',
osRelease: '6.8.0',
state: 'running',
pid: 42,
lastExitCode: 0,
installedVersion: '1.2.3',
projectDirectoryRoots: [],
},
update: {
kind: 'active_tasks',
currentVersion: '1.2.3',
targetVersion: '1.3.0',
},
}),
);
harness.pty.exit(1);

const result = await update;
assert.equal(result.kind, 'result');
assert.equal(result.kind === 'result' ? result.update.kind : undefined, 'active_tasks');
assert.deepEqual(phases, ['retiring']);
assert.deepEqual(harness.events.map(({ kind }) => kind), ['opened', 'data', 'connected']);
assert.doesNotMatch(JSON.stringify(harness.events), /MAKA_RUNTIME_HOST_SERVICE/u);
await harness.terminal.close();
});

test('keeps a prepared access credential out of the SSH terminal projection', async () => {
const harness = createHarness('pending');
const credential = 'maka_rh_secret-replacement';
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(runtime-host): add safe managed Host updates by M4n5ter · Pull Request #3591 · apache/maka · GitHub
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 numberDiff line numberDiff line change
Expand Up@@ -41,8 +41,8 @@ import {
} from '../runtime-host-desktop-manager.js';

test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, async () => {
const first = candidateHarness({ delayDisconnect: true });
const second = candidateHarness();
const first = candidateHarness({ delayDisconnect: true, hostEpoch: 'host-before' });
const second = candidateHarness({ hostEpoch: 'host-after' });
const queue = [ready(first.candidate), ready(second.candidate)];
let starts = 0;
const interactions: Array<string | undefined> = [];
Expand DownExpand Up@@ -71,6 +71,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
});

first.disconnect();
const replacementReady = owner.waitUntilReady(owner.defaultProfileId(), 'host-before');
const botMessage = owner.handleBotIncomingMessage({ text: 'hello' } as BotIncomingMessage);
const stop = owner.stopSession({
hostId: 'test-host',
Expand All@@ -95,7 +96,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
assert.equal(second.botMessages, 0);
assert.deepEqual(second.stoppedSessions, []);
releaseSecond();
await Promise.all([botMessage, stop]);
await Promise.all([botMessage, stop, replacementReady]);

assert.equal(first.botMessages, 0);
assert.equal(second.botMessages, 1);
Expand DownExpand Up@@ -815,6 +816,7 @@ function candidateHarness(
activeTasks?: boolean;
lifecycleMode?: 'ephemeral' | 'service' | 'remote';
hostId?: string;
hostEpoch?: string;
finalizeFailures?: Error[];
disconnectOnFinalizeFailure?: boolean;
onPrepare?: () => void;
Expand All@@ -837,6 +839,7 @@ function candidateHarness(
hostLifecycleMode: options.lifecycleMode ?? 'ephemeral',
client: {
hostId: options.hostId ?? 'test-host',
hostEpoch: options.hostEpoch ?? 'test-host-epoch',
get lifecycleState() {
return lifecycleState;
},
Expand Down
146 changes: 146 additions & 0 deletions apps/desktop/src/main/__tests__/runtime-host-management.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@ import type {
DesktopRuntimeHostSshAccessInput,
DesktopRuntimeHostSshCleanupInput,
DesktopRuntimeHostSshManagementInput,
DesktopRuntimeHostSshUpdateInput,
} from '../runtime-host-ssh-terminal.js';

test('identifies, rotates, and revokes managed credentials without exposing secrets', async () => {
Expand DownExpand Up@@ -64,6 +65,7 @@ test('identifies, rotates, and revokes managed credentials without exposing secr
];

createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -185,6 +187,8 @@ test('manages only the service identity bound by Desktop onboarding', async () =
const uninstallOrder: string[] = [];
let operatorAccess = false;
let cleared = 0;
let statusGate: Promise<void> | undefined;
let releaseStatus: (() => void) | undefined;
const managedProfile = {
id: 'office',
name: 'Office',
Expand All@@ -203,6 +207,7 @@ test('manages only the service identity bound by Desktop onboarding', async () =
operatorPath: '/home/operator/.local/share/maka/operator',
};
const management = createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand All@@ -229,6 +234,7 @@ test('manages only the service identity bound by Desktop onboarding', async () =
},
runServiceManagement: async (input) => {
managementInputs.push(input);
if (input.action === 'status') await statusGate;
if (input.action === 'uninstall') {
uninstallOrder.push('uninstall-service');
}
Expand All@@ -243,6 +249,19 @@ test('manages only the service identity bound by Desktop onboarding', async () =
const run = handlers.get('runtime-host-management:run');
assert.ok(run);

statusGate = new Promise((resolve) => {
releaseStatus = resolve;
});
const firstStatus = run({}, 'office', 'status');
const secondStatus = run({}, 'office', 'status');
await Promise.resolve();
await Promise.resolve();
assert.equal(managementInputs.length, 1);
releaseStatus?.();
await Promise.all([firstStatus, secondStatus]);
statusGate = undefined;
managementInputs.length = 0;

await assert.rejects(
run({}, 'manual', 'uninstall') as Promise<unknown>,
/not bound to a managed service/u,
Expand DownExpand Up@@ -309,6 +328,121 @@ test('manages only the service identity bound by Desktop onboarding', async () =
assert.equal(handlers.size, 0);
});

test('publishes update progress and waits for the managed profile to reconnect', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const updates: DesktopRuntimeHostSshUpdateInput[] = [];
const progress: unknown[] = [];
const connectionCompletions: unknown[] = [];
let failConnection = false;
let bindingPresent = true;
let removeBindingAfterUpdate = false;
const profile = {
id: 'office',
name: 'Office',
kind: 'remote' as const,
rootId: 'a'.repeat(64),
transport: {
kind: 'ssh' as const,
destination: 'operator@example.com',
remotePort: 7443,
websocketPath: '/runtime-host',
},
};
const service = {
id: 'b'.repeat(64),
rootPath: '/srv/maka',
operatorPath: '/home/operator/.local/share/maka/operator',
};
createDesktopRuntimeHostManagement({
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
},
profiles: {
resolveManagedService: async () =>
bindingPresent ? { profile, service, state: 'active' as const } : undefined,
resolveManagedAccess: async () => undefined,
rotateManagedCredential: async () => assert.fail('credential rotation is not expected'),
markManagedServiceUninstalling: async (binding) => binding,
markManagedServiceCleanupPending: async (binding) => binding,
clearManagedServiceBinding: async () => undefined,
},
runServiceManagement: async () => assert.fail('ordinary management is not expected'),
runUpdate: async (input, onProgress) => {
updates.push(input);
onProgress('staging');
if (removeBindingAfterUpdate) bindingPresent = false;
return {
schemaVersion: 1,
kind: 'result',
action: 'update',
service: {
platform: 'linux',
arch: 'x64',
osRelease: '6.8.0',
state: 'running',
pid: 43,
lastExitCode: 0,
installedVersion: '1.3.0',
projectDirectoryRoots: [],
},
operatorCapabilities: ['access-management-v1'],
update: { kind: 'updated', previousVersion: '1.2.3', targetVersion: '1.3.0' },
};
},
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.3.0' }),
currentHostEpoch: () => 'host-before-update',
awaitUpdatedConnection: async (...args) => {
connectionCompletions.push(args);
if (failConnection) throw new Error('authentication required');
},
sendProgress: (event) => progress.push(event),
runAccessManagement: async () => assert.fail('access management is not expected'),
cleanupManagedDeployment: async () => assert.fail('cleanup is not expected'),
});

const update = handlers.get('runtime-host-management:update');
assert.ok(update);
const response = await update({}, profile.id, false);
assert.equal((response as { accessManagementAvailable: boolean }).accessManagementAvailable, true);
assert.deepEqual(updates, [{
destination: profile.transport.destination,
setupPackage: { kind: 'npm', specifier: 'maka-agent@1.3.0' },
expectedTarget: {
serviceId: service.id,
rootPath: service.rootPath,
rootId: profile.rootId,
},
}]);
assert.deepEqual(progress, [{ profileId: profile.id, phase: 'staging' }]);
assert.deepEqual(connectionCompletions, [
[profile.id, profile.rootId, 'host-before-update', true],
]);

removeBindingAfterUpdate = true;
const changedProfile = await update({}, profile.id, false);
assert.equal(
(changedProfile as { kind: string; error?: { message: string } }).error?.message,
'The Runtime Host update completed, but Desktop could not reconnect: ' +
'Runtime Host profile changed while its service was updating',
);

bindingPresent = true;
removeBindingAfterUpdate = false;
failConnection = true;
const reconnectFailure = await update({}, profile.id, false);
assert.deepEqual(reconnectFailure, {
schemaVersion: 1,
kind: 'error',
action: 'update',
error: {
code: 'desktop_reconnect_failed',
message:
'The Runtime Host update completed, but Desktop could not reconnect: authentication required',
},
});
});

test('resumes deployment cleanup without invoking the removed operator', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const profile = {
Expand All@@ -333,6 +467,7 @@ test('resumes deployment cleanup without invoking the removed operator', async (
let state: 'active' | 'uninstalling' | 'cleanup_pending' = 'active';
let clearAttempts = 0;
createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -384,6 +519,7 @@ test('rechecks uninstall intent before retrying the remote service', async () =>
const handlers = new Map<string, (...args: unknown[]) => unknown>();
let marked = false;
createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -464,6 +600,16 @@ function serviceResult(
: { ...result, action };
}

function unusedUpdateDependencies() {
return {
runUpdate: async (): Promise<never> => assert.fail('update is not expected'),
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.2.3' } as const),
currentHostEpoch: () => undefined,
awaitUpdatedConnection: async () => undefined,
sendProgress: () => undefined,
};
}

function accessCredential(
credentialId: string,
principalId: string,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,43 @@ test("keeps Local enabled while a new remote Host connects", async () => {
);
});

test("reconnects an enabled remote Host with interactive SSH", async () => {
const root = await clientRoot();
const catalog = createClientRuntimeHostProfileCatalog(root);
await catalog.create(MANAGED_PROFILE, "opaque-token");
const calls: string[] = [];
const service = createDesktopRuntimeHostProfileService({
clientDataRoot: root,
startup: {
preferences: {
schemaVersion: 2,
defaultProfileId: LOCAL_RUNTIME_HOST_PROFILE.id,
enabledRemoteProfileIds: [MANAGED_PROFILE.id],
},
pairingIntents: [],
remotes: [{ profile: MANAGED_PROFILE, credential: "opaque-token" }],
unavailable: new Map(),
},
catalog,
states: () => [connectingLocal()],
enable: async (target, interaction) => {
calls.push(`enable:${target.profile.id}:${interaction}`);
},
disable: async (profileId) => {
calls.push(`disable:${profileId}`);
},
setDefault: () => undefined,
finalizePairing: async () => undefined,
});

await service.reconnect(MANAGED_PROFILE.id, MANAGED_PROFILE.rootId);

assert.deepEqual(calls, [
`disable:${MANAGED_PROFILE.id}`,
`enable:${MANAGED_PROFILE.id}:terminal`,
]);
});

test("does not enable the same State Root twice", async () => {
const root = await clientRoot();
const startup = await resolveDesktopRuntimeHostStartup(root);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -308,6 +308,70 @@ test('keeps a received management result when SSH teardown times out', async ()
await harness.terminal.close();
});

test('runs an exact update package and reports progress before an active-work result', async () => {
const harness = createHarness('pending');
const phases: string[] = [];
const update = harness.terminal.runUpdate(
{
destination: 'operator@example.com',
setupPackage: { kind: 'npm', specifier: 'maka-agent@1.3.0' },
expectedTarget: {
serviceId: 'b'.repeat(64),
rootPath: '/srv/maka',
rootId: 'a'.repeat(64),
},
},
(phase) => phases.push(phase),
);
await waitFor(() => harness.pty.hasDataListener());
const remoteCommand = harness.launchArgs.at(-1)?.at(-1) ?? '';
assert.match(remoteCommand, /--package.*maka-agent@1\.3\.0/u);
assert.match(remoteCommand, /runtime-host.*service.*update/u);
assert.match(remoteCommand, /MAKA_RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST/u);
harness.pty.emitData('Password: ');
harness.pty.emitData(
encodeRuntimeHostServiceManagementFrame({
schemaVersion: 1,
kind: 'progress',
action: 'update',
phase: 'retiring',
currentVersion: '1.2.3',
targetVersion: '1.3.0',
}),
);
harness.pty.emitData(
encodeRuntimeHostServiceManagementFrame({
schemaVersion: 1,
kind: 'result',
action: 'update',
service: {
platform: 'linux',
arch: 'x64',
osRelease: '6.8.0',
state: 'running',
pid: 42,
lastExitCode: 0,
installedVersion: '1.2.3',
projectDirectoryRoots: [],
},
update: {
kind: 'active_tasks',
currentVersion: '1.2.3',
targetVersion: '1.3.0',
},
}),
);
harness.pty.exit(1);

const result = await update;
assert.equal(result.kind, 'result');
assert.equal(result.kind === 'result' ? result.update.kind : undefined, 'active_tasks');
assert.deepEqual(phases, ['retiring']);
assert.deepEqual(harness.events.map(({ kind }) => kind), ['opened', 'data', 'connected']);
assert.doesNotMatch(JSON.stringify(harness.events), /MAKA_RUNTIME_HOST_SERVICE/u);
await harness.terminal.close();
});

test('keeps a prepared access credential out of the SSH terminal projection', async () => {
const harness = createHarness('pending');
const credential = 'maka_rh_secret-replacement';
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(runtime-host): add safe managed Host updates by M4n5ter · Pull Request #3591 · apache/maka · GitHub
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 numberDiff line numberDiff line change
Expand Up@@ -41,8 +41,8 @@ import {
} from '../runtime-host-desktop-manager.js';

test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, async () => {
const first = candidateHarness({ delayDisconnect: true });
const second = candidateHarness();
const first = candidateHarness({ delayDisconnect: true, hostEpoch: 'host-before' });
const second = candidateHarness({ hostEpoch: 'host-after' });
const queue = [ready(first.candidate), ready(second.candidate)];
let starts = 0;
const interactions: Array<string | undefined> = [];
Expand DownExpand Up@@ -71,6 +71,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
});

first.disconnect();
const replacementReady = owner.waitUntilReady(owner.defaultProfileId(), 'host-before');
const botMessage = owner.handleBotIncomingMessage({ text: 'hello' } as BotIncomingMessage);
const stop = owner.stopSession({
hostId: 'test-host',
Expand All@@ -95,7 +96,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
assert.equal(second.botMessages, 0);
assert.deepEqual(second.stoppedSessions, []);
releaseSecond();
await Promise.all([botMessage, stop]);
await Promise.all([botMessage, stop, replacementReady]);

assert.equal(first.botMessages, 0);
assert.equal(second.botMessages, 1);
Expand DownExpand Up@@ -815,6 +816,7 @@ function candidateHarness(
activeTasks?: boolean;
lifecycleMode?: 'ephemeral' | 'service' | 'remote';
hostId?: string;
hostEpoch?: string;
finalizeFailures?: Error[];
disconnectOnFinalizeFailure?: boolean;
onPrepare?: () => void;
Expand All@@ -837,6 +839,7 @@ function candidateHarness(
hostLifecycleMode: options.lifecycleMode ?? 'ephemeral',
client: {
hostId: options.hostId ?? 'test-host',
hostEpoch: options.hostEpoch ?? 'test-host-epoch',
get lifecycleState() {
return lifecycleState;
},
Expand Down
146 changes: 146 additions & 0 deletions apps/desktop/src/main/__tests__/runtime-host-management.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@ import type {
DesktopRuntimeHostSshAccessInput,
DesktopRuntimeHostSshCleanupInput,
DesktopRuntimeHostSshManagementInput,
DesktopRuntimeHostSshUpdateInput,
} from '../runtime-host-ssh-terminal.js';

test('identifies, rotates, and revokes managed credentials without exposing secrets', async () => {
Expand DownExpand Up@@ -64,6 +65,7 @@ test('identifies, rotates, and revokes managed credentials without exposing secr
];

createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -185,6 +187,8 @@ test('manages only the service identity bound by Desktop onboarding', async () =
const uninstallOrder: string[] = [];
let operatorAccess = false;
let cleared = 0;
let statusGate: Promise<void> | undefined;
let releaseStatus: (() => void) | undefined;
const managedProfile = {
id: 'office',
name: 'Office',
Expand All@@ -203,6 +207,7 @@ test('manages only the service identity bound by Desktop onboarding', async () =
operatorPath: '/home/operator/.local/share/maka/operator',
};
const management = createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand All@@ -229,6 +234,7 @@ test('manages only the service identity bound by Desktop onboarding', async () =
},
runServiceManagement: async (input) => {
managementInputs.push(input);
if (input.action === 'status') await statusGate;
if (input.action === 'uninstall') {
uninstallOrder.push('uninstall-service');
}
Expand All@@ -243,6 +249,19 @@ test('manages only the service identity bound by Desktop onboarding', async () =
const run = handlers.get('runtime-host-management:run');
assert.ok(run);

statusGate = new Promise((resolve) => {
releaseStatus = resolve;
});
const firstStatus = run({}, 'office', 'status');
const secondStatus = run({}, 'office', 'status');
await Promise.resolve();
await Promise.resolve();
assert.equal(managementInputs.length, 1);
releaseStatus?.();
await Promise.all([firstStatus, secondStatus]);
statusGate = undefined;
managementInputs.length = 0;

await assert.rejects(
run({}, 'manual', 'uninstall') as Promise<unknown>,
/not bound to a managed service/u,
Expand DownExpand Up@@ -309,6 +328,121 @@ test('manages only the service identity bound by Desktop onboarding', async () =
assert.equal(handlers.size, 0);
});

test('publishes update progress and waits for the managed profile to reconnect', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const updates: DesktopRuntimeHostSshUpdateInput[] = [];
const progress: unknown[] = [];
const connectionCompletions: unknown[] = [];
let failConnection = false;
let bindingPresent = true;
let removeBindingAfterUpdate = false;
const profile = {
id: 'office',
name: 'Office',
kind: 'remote' as const,
rootId: 'a'.repeat(64),
transport: {
kind: 'ssh' as const,
destination: 'operator@example.com',
remotePort: 7443,
websocketPath: '/runtime-host',
},
};
const service = {
id: 'b'.repeat(64),
rootPath: '/srv/maka',
operatorPath: '/home/operator/.local/share/maka/operator',
};
createDesktopRuntimeHostManagement({
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
},
profiles: {
resolveManagedService: async () =>
bindingPresent ? { profile, service, state: 'active' as const } : undefined,
resolveManagedAccess: async () => undefined,
rotateManagedCredential: async () => assert.fail('credential rotation is not expected'),
markManagedServiceUninstalling: async (binding) => binding,
markManagedServiceCleanupPending: async (binding) => binding,
clearManagedServiceBinding: async () => undefined,
},
runServiceManagement: async () => assert.fail('ordinary management is not expected'),
runUpdate: async (input, onProgress) => {
updates.push(input);
onProgress('staging');
if (removeBindingAfterUpdate) bindingPresent = false;
return {
schemaVersion: 1,
kind: 'result',
action: 'update',
service: {
platform: 'linux',
arch: 'x64',
osRelease: '6.8.0',
state: 'running',
pid: 43,
lastExitCode: 0,
installedVersion: '1.3.0',
projectDirectoryRoots: [],
},
operatorCapabilities: ['access-management-v1'],
update: { kind: 'updated', previousVersion: '1.2.3', targetVersion: '1.3.0' },
};
},
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.3.0' }),
currentHostEpoch: () => 'host-before-update',
awaitUpdatedConnection: async (...args) => {
connectionCompletions.push(args);
if (failConnection) throw new Error('authentication required');
},
sendProgress: (event) => progress.push(event),
runAccessManagement: async () => assert.fail('access management is not expected'),
cleanupManagedDeployment: async () => assert.fail('cleanup is not expected'),
});

const update = handlers.get('runtime-host-management:update');
assert.ok(update);
const response = await update({}, profile.id, false);
assert.equal((response as { accessManagementAvailable: boolean }).accessManagementAvailable, true);
assert.deepEqual(updates, [{
destination: profile.transport.destination,
setupPackage: { kind: 'npm', specifier: 'maka-agent@1.3.0' },
expectedTarget: {
serviceId: service.id,
rootPath: service.rootPath,
rootId: profile.rootId,
},
}]);
assert.deepEqual(progress, [{ profileId: profile.id, phase: 'staging' }]);
assert.deepEqual(connectionCompletions, [
[profile.id, profile.rootId, 'host-before-update', true],
]);

removeBindingAfterUpdate = true;
const changedProfile = await update({}, profile.id, false);
assert.equal(
(changedProfile as { kind: string; error?: { message: string } }).error?.message,
'The Runtime Host update completed, but Desktop could not reconnect: ' +
'Runtime Host profile changed while its service was updating',
);

bindingPresent = true;
removeBindingAfterUpdate = false;
failConnection = true;
const reconnectFailure = await update({}, profile.id, false);
assert.deepEqual(reconnectFailure, {
schemaVersion: 1,
kind: 'error',
action: 'update',
error: {
code: 'desktop_reconnect_failed',
message:
'The Runtime Host update completed, but Desktop could not reconnect: authentication required',
},
});
});

test('resumes deployment cleanup without invoking the removed operator', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const profile = {
Expand All@@ -333,6 +467,7 @@ test('resumes deployment cleanup without invoking the removed operator', async (
let state: 'active' | 'uninstalling' | 'cleanup_pending' = 'active';
let clearAttempts = 0;
createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -384,6 +519,7 @@ test('rechecks uninstall intent before retrying the remote service', async () =>
const handlers = new Map<string, (...args: unknown[]) => unknown>();
let marked = false;
createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -464,6 +600,16 @@ function serviceResult(
: { ...result, action };
}

function unusedUpdateDependencies() {
return {
runUpdate: async (): Promise<never> => assert.fail('update is not expected'),
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.2.3' } as const),
currentHostEpoch: () => undefined,
awaitUpdatedConnection: async () => undefined,
sendProgress: () => undefined,
};
}

function accessCredential(
credentialId: string,
principalId: string,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,43 @@ test("keeps Local enabled while a new remote Host connects", async () => {
);
});

test("reconnects an enabled remote Host with interactive SSH", async () => {
const root = await clientRoot();
const catalog = createClientRuntimeHostProfileCatalog(root);
await catalog.create(MANAGED_PROFILE, "opaque-token");
const calls: string[] = [];
const service = createDesktopRuntimeHostProfileService({
clientDataRoot: root,
startup: {
preferences: {
schemaVersion: 2,
defaultProfileId: LOCAL_RUNTIME_HOST_PROFILE.id,
enabledRemoteProfileIds: [MANAGED_PROFILE.id],
},
pairingIntents: [],
remotes: [{ profile: MANAGED_PROFILE, credential: "opaque-token" }],
unavailable: new Map(),
},
catalog,
states: () => [connectingLocal()],
enable: async (target, interaction) => {
calls.push(`enable:${target.profile.id}:${interaction}`);
},
disable: async (profileId) => {
calls.push(`disable:${profileId}`);
},
setDefault: () => undefined,
finalizePairing: async () => undefined,
});

await service.reconnect(MANAGED_PROFILE.id, MANAGED_PROFILE.rootId);

assert.deepEqual(calls, [
`disable:${MANAGED_PROFILE.id}`,
`enable:${MANAGED_PROFILE.id}:terminal`,
]);
});

test("does not enable the same State Root twice", async () => {
const root = await clientRoot();
const startup = await resolveDesktopRuntimeHostStartup(root);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -308,6 +308,70 @@ test('keeps a received management result when SSH teardown times out', async ()
await harness.terminal.close();
});

test('runs an exact update package and reports progress before an active-work result', async () => {
const harness = createHarness('pending');
const phases: string[] = [];
const update = harness.terminal.runUpdate(
{
destination: 'operator@example.com',
setupPackage: { kind: 'npm', specifier: 'maka-agent@1.3.0' },
expectedTarget: {
serviceId: 'b'.repeat(64),
rootPath: '/srv/maka',
rootId: 'a'.repeat(64),
},
},
(phase) => phases.push(phase),
);
await waitFor(() => harness.pty.hasDataListener());
const remoteCommand = harness.launchArgs.at(-1)?.at(-1) ?? '';
assert.match(remoteCommand, /--package.*maka-agent@1\.3\.0/u);
assert.match(remoteCommand, /runtime-host.*service.*update/u);
assert.match(remoteCommand, /MAKA_RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST/u);
harness.pty.emitData('Password: ');
harness.pty.emitData(
encodeRuntimeHostServiceManagementFrame({
schemaVersion: 1,
kind: 'progress',
action: 'update',
phase: 'retiring',
currentVersion: '1.2.3',
targetVersion: '1.3.0',
}),
);
harness.pty.emitData(
encodeRuntimeHostServiceManagementFrame({
schemaVersion: 1,
kind: 'result',
action: 'update',
service: {
platform: 'linux',
arch: 'x64',
osRelease: '6.8.0',
state: 'running',
pid: 42,
lastExitCode: 0,
installedVersion: '1.2.3',
projectDirectoryRoots: [],
},
update: {
kind: 'active_tasks',
currentVersion: '1.2.3',
targetVersion: '1.3.0',
},
}),
);
harness.pty.exit(1);

const result = await update;
assert.equal(result.kind, 'result');
assert.equal(result.kind === 'result' ? result.update.kind : undefined, 'active_tasks');
assert.deepEqual(phases, ['retiring']);
assert.deepEqual(harness.events.map(({ kind }) => kind), ['opened', 'data', 'connected']);
assert.doesNotMatch(JSON.stringify(harness.events), /MAKA_RUNTIME_HOST_SERVICE/u);
await harness.terminal.close();
});

test('keeps a prepared access credential out of the SSH terminal projection', async () => {
const harness = createHarness('pending');
const credential = 'maka_rh_secret-replacement';
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(runtime-host): add safe managed Host updates by M4n5ter · Pull Request #3591 · apache/maka · GitHub
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 numberDiff line numberDiff line change
Expand Up@@ -41,8 +41,8 @@ import {
} from '../runtime-host-desktop-manager.js';

test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, async () => {
const first = candidateHarness({ delayDisconnect: true });
const second = candidateHarness();
const first = candidateHarness({ delayDisconnect: true, hostEpoch: 'host-before' });
const second = candidateHarness({ hostEpoch: 'host-after' });
const queue = [ready(first.candidate), ready(second.candidate)];
let starts = 0;
const interactions: Array<string | undefined> = [];
Expand DownExpand Up@@ -71,6 +71,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
});

first.disconnect();
const replacementReady = owner.waitUntilReady(owner.defaultProfileId(), 'host-before');
const botMessage = owner.handleBotIncomingMessage({ text: 'hello' } as BotIncomingMessage);
const stop = owner.stopSession({
hostId: 'test-host',
Expand All@@ -95,7 +96,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
assert.equal(second.botMessages, 0);
assert.deepEqual(second.stoppedSessions, []);
releaseSecond();
await Promise.all([botMessage, stop]);
await Promise.all([botMessage, stop, replacementReady]);

assert.equal(first.botMessages, 0);
assert.equal(second.botMessages, 1);
Expand DownExpand Up@@ -815,6 +816,7 @@ function candidateHarness(
activeTasks?: boolean;
lifecycleMode?: 'ephemeral' | 'service' | 'remote';
hostId?: string;
hostEpoch?: string;
finalizeFailures?: Error[];
disconnectOnFinalizeFailure?: boolean;
onPrepare?: () => void;
Expand All@@ -837,6 +839,7 @@ function candidateHarness(
hostLifecycleMode: options.lifecycleMode ?? 'ephemeral',
client: {
hostId: options.hostId ?? 'test-host',
hostEpoch: options.hostEpoch ?? 'test-host-epoch',
get lifecycleState() {
return lifecycleState;
},
Expand Down
146 changes: 146 additions & 0 deletions apps/desktop/src/main/__tests__/runtime-host-management.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@ import type {
DesktopRuntimeHostSshAccessInput,
DesktopRuntimeHostSshCleanupInput,
DesktopRuntimeHostSshManagementInput,
DesktopRuntimeHostSshUpdateInput,
} from '../runtime-host-ssh-terminal.js';

test('identifies, rotates, and revokes managed credentials without exposing secrets', async () => {
Expand DownExpand Up@@ -64,6 +65,7 @@ test('identifies, rotates, and revokes managed credentials without exposing secr
];

createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -185,6 +187,8 @@ test('manages only the service identity bound by Desktop onboarding', async () =
const uninstallOrder: string[] = [];
let operatorAccess = false;
let cleared = 0;
let statusGate: Promise<void> | undefined;
let releaseStatus: (() => void) | undefined;
const managedProfile = {
id: 'office',
name: 'Office',
Expand All@@ -203,6 +207,7 @@ test('manages only the service identity bound by Desktop onboarding', async () =
operatorPath: '/home/operator/.local/share/maka/operator',
};
const management = createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand All@@ -229,6 +234,7 @@ test('manages only the service identity bound by Desktop onboarding', async () =
},
runServiceManagement: async (input) => {
managementInputs.push(input);
if (input.action === 'status') await statusGate;
if (input.action === 'uninstall') {
uninstallOrder.push('uninstall-service');
}
Expand All@@ -243,6 +249,19 @@ test('manages only the service identity bound by Desktop onboarding', async () =
const run = handlers.get('runtime-host-management:run');
assert.ok(run);

statusGate = new Promise((resolve) => {
releaseStatus = resolve;
});
const firstStatus = run({}, 'office', 'status');
const secondStatus = run({}, 'office', 'status');
await Promise.resolve();
await Promise.resolve();
assert.equal(managementInputs.length, 1);
releaseStatus?.();
await Promise.all([firstStatus, secondStatus]);
statusGate = undefined;
managementInputs.length = 0;

await assert.rejects(
run({}, 'manual', 'uninstall') as Promise<unknown>,
/not bound to a managed service/u,
Expand DownExpand Up@@ -309,6 +328,121 @@ test('manages only the service identity bound by Desktop onboarding', async () =
assert.equal(handlers.size, 0);
});

test('publishes update progress and waits for the managed profile to reconnect', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const updates: DesktopRuntimeHostSshUpdateInput[] = [];
const progress: unknown[] = [];
const connectionCompletions: unknown[] = [];
let failConnection = false;
let bindingPresent = true;
let removeBindingAfterUpdate = false;
const profile = {
id: 'office',
name: 'Office',
kind: 'remote' as const,
rootId: 'a'.repeat(64),
transport: {
kind: 'ssh' as const,
destination: 'operator@example.com',
remotePort: 7443,
websocketPath: '/runtime-host',
},
};
const service = {
id: 'b'.repeat(64),
rootPath: '/srv/maka',
operatorPath: '/home/operator/.local/share/maka/operator',
};
createDesktopRuntimeHostManagement({
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
},
profiles: {
resolveManagedService: async () =>
bindingPresent ? { profile, service, state: 'active' as const } : undefined,
resolveManagedAccess: async () => undefined,
rotateManagedCredential: async () => assert.fail('credential rotation is not expected'),
markManagedServiceUninstalling: async (binding) => binding,
markManagedServiceCleanupPending: async (binding) => binding,
clearManagedServiceBinding: async () => undefined,
},
runServiceManagement: async () => assert.fail('ordinary management is not expected'),
runUpdate: async (input, onProgress) => {
updates.push(input);
onProgress('staging');
if (removeBindingAfterUpdate) bindingPresent = false;
return {
schemaVersion: 1,
kind: 'result',
action: 'update',
service: {
platform: 'linux',
arch: 'x64',
osRelease: '6.8.0',
state: 'running',
pid: 43,
lastExitCode: 0,
installedVersion: '1.3.0',
projectDirectoryRoots: [],
},
operatorCapabilities: ['access-management-v1'],
update: { kind: 'updated', previousVersion: '1.2.3', targetVersion: '1.3.0' },
};
},
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.3.0' }),
currentHostEpoch: () => 'host-before-update',
awaitUpdatedConnection: async (...args) => {
connectionCompletions.push(args);
if (failConnection) throw new Error('authentication required');
},
sendProgress: (event) => progress.push(event),
runAccessManagement: async () => assert.fail('access management is not expected'),
cleanupManagedDeployment: async () => assert.fail('cleanup is not expected'),
});

const update = handlers.get('runtime-host-management:update');
assert.ok(update);
const response = await update({}, profile.id, false);
assert.equal((response as { accessManagementAvailable: boolean }).accessManagementAvailable, true);
assert.deepEqual(updates, [{
destination: profile.transport.destination,
setupPackage: { kind: 'npm', specifier: 'maka-agent@1.3.0' },
expectedTarget: {
serviceId: service.id,
rootPath: service.rootPath,
rootId: profile.rootId,
},
}]);
assert.deepEqual(progress, [{ profileId: profile.id, phase: 'staging' }]);
assert.deepEqual(connectionCompletions, [
[profile.id, profile.rootId, 'host-before-update', true],
]);

removeBindingAfterUpdate = true;
const changedProfile = await update({}, profile.id, false);
assert.equal(
(changedProfile as { kind: string; error?: { message: string } }).error?.message,
'The Runtime Host update completed, but Desktop could not reconnect: ' +
'Runtime Host profile changed while its service was updating',
);

bindingPresent = true;
removeBindingAfterUpdate = false;
failConnection = true;
const reconnectFailure = await update({}, profile.id, false);
assert.deepEqual(reconnectFailure, {
schemaVersion: 1,
kind: 'error',
action: 'update',
error: {
code: 'desktop_reconnect_failed',
message:
'The Runtime Host update completed, but Desktop could not reconnect: authentication required',
},
});
});

test('resumes deployment cleanup without invoking the removed operator', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const profile = {
Expand All@@ -333,6 +467,7 @@ test('resumes deployment cleanup without invoking the removed operator', async (
let state: 'active' | 'uninstalling' | 'cleanup_pending' = 'active';
let clearAttempts = 0;
createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -384,6 +519,7 @@ test('rechecks uninstall intent before retrying the remote service', async () =>
const handlers = new Map<string, (...args: unknown[]) => unknown>();
let marked = false;
createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -464,6 +600,16 @@ function serviceResult(
: { ...result, action };
}

function unusedUpdateDependencies() {
return {
runUpdate: async (): Promise<never> => assert.fail('update is not expected'),
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.2.3' } as const),
currentHostEpoch: () => undefined,
awaitUpdatedConnection: async () => undefined,
sendProgress: () => undefined,
};
}

function accessCredential(
credentialId: string,
principalId: string,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,43 @@ test("keeps Local enabled while a new remote Host connects", async () => {
);
});

test("reconnects an enabled remote Host with interactive SSH", async () => {
const root = await clientRoot();
const catalog = createClientRuntimeHostProfileCatalog(root);
await catalog.create(MANAGED_PROFILE, "opaque-token");
const calls: string[] = [];
const service = createDesktopRuntimeHostProfileService({
clientDataRoot: root,
startup: {
preferences: {
schemaVersion: 2,
defaultProfileId: LOCAL_RUNTIME_HOST_PROFILE.id,
enabledRemoteProfileIds: [MANAGED_PROFILE.id],
},
pairingIntents: [],
remotes: [{ profile: MANAGED_PROFILE, credential: "opaque-token" }],
unavailable: new Map(),
},
catalog,
states: () => [connectingLocal()],
enable: async (target, interaction) => {
calls.push(`enable:${target.profile.id}:${interaction}`);
},
disable: async (profileId) => {
calls.push(`disable:${profileId}`);
},
setDefault: () => undefined,
finalizePairing: async () => undefined,
});

await service.reconnect(MANAGED_PROFILE.id, MANAGED_PROFILE.rootId);

assert.deepEqual(calls, [
`disable:${MANAGED_PROFILE.id}`,
`enable:${MANAGED_PROFILE.id}:terminal`,
]);
});

test("does not enable the same State Root twice", async () => {
const root = await clientRoot();
const startup = await resolveDesktopRuntimeHostStartup(root);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -308,6 +308,70 @@ test('keeps a received management result when SSH teardown times out', async ()
await harness.terminal.close();
});

test('runs an exact update package and reports progress before an active-work result', async () => {
const harness = createHarness('pending');
const phases: string[] = [];
const update = harness.terminal.runUpdate(
{
destination: 'operator@example.com',
setupPackage: { kind: 'npm', specifier: 'maka-agent@1.3.0' },
expectedTarget: {
serviceId: 'b'.repeat(64),
rootPath: '/srv/maka',
rootId: 'a'.repeat(64),
},
},
(phase) => phases.push(phase),
);
await waitFor(() => harness.pty.hasDataListener());
const remoteCommand = harness.launchArgs.at(-1)?.at(-1) ?? '';
assert.match(remoteCommand, /--package.*maka-agent@1\.3\.0/u);
assert.match(remoteCommand, /runtime-host.*service.*update/u);
assert.match(remoteCommand, /MAKA_RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST/u);
harness.pty.emitData('Password: ');
harness.pty.emitData(
encodeRuntimeHostServiceManagementFrame({
schemaVersion: 1,
kind: 'progress',
action: 'update',
phase: 'retiring',
currentVersion: '1.2.3',
targetVersion: '1.3.0',
}),
);
harness.pty.emitData(
encodeRuntimeHostServiceManagementFrame({
schemaVersion: 1,
kind: 'result',
action: 'update',
service: {
platform: 'linux',
arch: 'x64',
osRelease: '6.8.0',
state: 'running',
pid: 42,
lastExitCode: 0,
installedVersion: '1.2.3',
projectDirectoryRoots: [],
},
update: {
kind: 'active_tasks',
currentVersion: '1.2.3',
targetVersion: '1.3.0',
},
}),
);
harness.pty.exit(1);

const result = await update;
assert.equal(result.kind, 'result');
assert.equal(result.kind === 'result' ? result.update.kind : undefined, 'active_tasks');
assert.deepEqual(phases, ['retiring']);
assert.deepEqual(harness.events.map(({ kind }) => kind), ['opened', 'data', 'connected']);
assert.doesNotMatch(JSON.stringify(harness.events), /MAKA_RUNTIME_HOST_SERVICE/u);
await harness.terminal.close();
});

test('keeps a prepared access credential out of the SSH terminal projection', async () => {
const harness = createHarness('pending');
const credential = 'maka_rh_secret-replacement';
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(runtime-host): add safe managed Host updates by M4n5ter · Pull Request #3591 · apache/maka · GitHub
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 numberDiff line numberDiff line change
Expand Up@@ -41,8 +41,8 @@ import {
} from '../runtime-host-desktop-manager.js';

test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, async () => {
const first = candidateHarness({ delayDisconnect: true });
const second = candidateHarness();
const first = candidateHarness({ delayDisconnect: true, hostEpoch: 'host-before' });
const second = candidateHarness({ hostEpoch: 'host-after' });
const queue = [ready(first.candidate), ready(second.candidate)];
let starts = 0;
const interactions: Array<string | undefined> = [];
Expand DownExpand Up@@ -71,6 +71,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
});

first.disconnect();
const replacementReady = owner.waitUntilReady(owner.defaultProfileId(), 'host-before');
const botMessage = owner.handleBotIncomingMessage({ text: 'hello' } as BotIncomingMessage);
const stop = owner.stopSession({
hostId: 'test-host',
Expand All@@ -95,7 +96,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
assert.equal(second.botMessages, 0);
assert.deepEqual(second.stoppedSessions, []);
releaseSecond();
await Promise.all([botMessage, stop]);
await Promise.all([botMessage, stop, replacementReady]);

assert.equal(first.botMessages, 0);
assert.equal(second.botMessages, 1);
Expand DownExpand Up@@ -815,6 +816,7 @@ function candidateHarness(
activeTasks?: boolean;
lifecycleMode?: 'ephemeral' | 'service' | 'remote';
hostId?: string;
hostEpoch?: string;
finalizeFailures?: Error[];
disconnectOnFinalizeFailure?: boolean;
onPrepare?: () => void;
Expand All@@ -837,6 +839,7 @@ function candidateHarness(
hostLifecycleMode: options.lifecycleMode ?? 'ephemeral',
client: {
hostId: options.hostId ?? 'test-host',
hostEpoch: options.hostEpoch ?? 'test-host-epoch',
get lifecycleState() {
return lifecycleState;
},
Expand Down
146 changes: 146 additions & 0 deletions apps/desktop/src/main/__tests__/runtime-host-management.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@ import type {
DesktopRuntimeHostSshAccessInput,
DesktopRuntimeHostSshCleanupInput,
DesktopRuntimeHostSshManagementInput,
DesktopRuntimeHostSshUpdateInput,
} from '../runtime-host-ssh-terminal.js';

test('identifies, rotates, and revokes managed credentials without exposing secrets', async () => {
Expand DownExpand Up@@ -64,6 +65,7 @@ test('identifies, rotates, and revokes managed credentials without exposing secr
];

createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -185,6 +187,8 @@ test('manages only the service identity bound by Desktop onboarding', async () =
const uninstallOrder: string[] = [];
let operatorAccess = false;
let cleared = 0;
let statusGate: Promise<void> | undefined;
let releaseStatus: (() => void) | undefined;
const managedProfile = {
id: 'office',
name: 'Office',
Expand All@@ -203,6 +207,7 @@ test('manages only the service identity bound by Desktop onboarding', async () =
operatorPath: '/home/operator/.local/share/maka/operator',
};
const management = createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand All@@ -229,6 +234,7 @@ test('manages only the service identity bound by Desktop onboarding', async () =
},
runServiceManagement: async (input) => {
managementInputs.push(input);
if (input.action === 'status') await statusGate;
if (input.action === 'uninstall') {
uninstallOrder.push('uninstall-service');
}
Expand All@@ -243,6 +249,19 @@ test('manages only the service identity bound by Desktop onboarding', async () =
const run = handlers.get('runtime-host-management:run');
assert.ok(run);

statusGate = new Promise((resolve) => {
releaseStatus = resolve;
});
const firstStatus = run({}, 'office', 'status');
const secondStatus = run({}, 'office', 'status');
await Promise.resolve();
await Promise.resolve();
assert.equal(managementInputs.length, 1);
releaseStatus?.();
await Promise.all([firstStatus, secondStatus]);
statusGate = undefined;
managementInputs.length = 0;

await assert.rejects(
run({}, 'manual', 'uninstall') as Promise<unknown>,
/not bound to a managed service/u,
Expand DownExpand Up@@ -309,6 +328,121 @@ test('manages only the service identity bound by Desktop onboarding', async () =
assert.equal(handlers.size, 0);
});

test('publishes update progress and waits for the managed profile to reconnect', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const updates: DesktopRuntimeHostSshUpdateInput[] = [];
const progress: unknown[] = [];
const connectionCompletions: unknown[] = [];
let failConnection = false;
let bindingPresent = true;
let removeBindingAfterUpdate = false;
const profile = {
id: 'office',
name: 'Office',
kind: 'remote' as const,
rootId: 'a'.repeat(64),
transport: {
kind: 'ssh' as const,
destination: 'operator@example.com',
remotePort: 7443,
websocketPath: '/runtime-host',
},
};
const service = {
id: 'b'.repeat(64),
rootPath: '/srv/maka',
operatorPath: '/home/operator/.local/share/maka/operator',
};
createDesktopRuntimeHostManagement({
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
},
profiles: {
resolveManagedService: async () =>
bindingPresent ? { profile, service, state: 'active' as const } : undefined,
resolveManagedAccess: async () => undefined,
rotateManagedCredential: async () => assert.fail('credential rotation is not expected'),
markManagedServiceUninstalling: async (binding) => binding,
markManagedServiceCleanupPending: async (binding) => binding,
clearManagedServiceBinding: async () => undefined,
},
runServiceManagement: async () => assert.fail('ordinary management is not expected'),
runUpdate: async (input, onProgress) => {
updates.push(input);
onProgress('staging');
if (removeBindingAfterUpdate) bindingPresent = false;
return {
schemaVersion: 1,
kind: 'result',
action: 'update',
service: {
platform: 'linux',
arch: 'x64',
osRelease: '6.8.0',
state: 'running',
pid: 43,
lastExitCode: 0,
installedVersion: '1.3.0',
projectDirectoryRoots: [],
},
operatorCapabilities: ['access-management-v1'],
update: { kind: 'updated', previousVersion: '1.2.3', targetVersion: '1.3.0' },
};
},
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.3.0' }),
currentHostEpoch: () => 'host-before-update',
awaitUpdatedConnection: async (...args) => {
connectionCompletions.push(args);
if (failConnection) throw new Error('authentication required');
},
sendProgress: (event) => progress.push(event),
runAccessManagement: async () => assert.fail('access management is not expected'),
cleanupManagedDeployment: async () => assert.fail('cleanup is not expected'),
});

const update = handlers.get('runtime-host-management:update');
assert.ok(update);
const response = await update({}, profile.id, false);
assert.equal((response as { accessManagementAvailable: boolean }).accessManagementAvailable, true);
assert.deepEqual(updates, [{
destination: profile.transport.destination,
setupPackage: { kind: 'npm', specifier: 'maka-agent@1.3.0' },
expectedTarget: {
serviceId: service.id,
rootPath: service.rootPath,
rootId: profile.rootId,
},
}]);
assert.deepEqual(progress, [{ profileId: profile.id, phase: 'staging' }]);
assert.deepEqual(connectionCompletions, [
[profile.id, profile.rootId, 'host-before-update', true],
]);

removeBindingAfterUpdate = true;
const changedProfile = await update({}, profile.id, false);
assert.equal(
(changedProfile as { kind: string; error?: { message: string } }).error?.message,
'The Runtime Host update completed, but Desktop could not reconnect: ' +
'Runtime Host profile changed while its service was updating',
);

bindingPresent = true;
removeBindingAfterUpdate = false;
failConnection = true;
const reconnectFailure = await update({}, profile.id, false);
assert.deepEqual(reconnectFailure, {
schemaVersion: 1,
kind: 'error',
action: 'update',
error: {
code: 'desktop_reconnect_failed',
message:
'The Runtime Host update completed, but Desktop could not reconnect: authentication required',
},
});
});

test('resumes deployment cleanup without invoking the removed operator', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const profile = {
Expand All@@ -333,6 +467,7 @@ test('resumes deployment cleanup without invoking the removed operator', async (
let state: 'active' | 'uninstalling' | 'cleanup_pending' = 'active';
let clearAttempts = 0;
createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -384,6 +519,7 @@ test('rechecks uninstall intent before retrying the remote service', async () =>
const handlers = new Map<string, (...args: unknown[]) => unknown>();
let marked = false;
createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -464,6 +600,16 @@ function serviceResult(
: { ...result, action };
}

function unusedUpdateDependencies() {
return {
runUpdate: async (): Promise<never> => assert.fail('update is not expected'),
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.2.3' } as const),
currentHostEpoch: () => undefined,
awaitUpdatedConnection: async () => undefined,
sendProgress: () => undefined,
};
}

function accessCredential(
credentialId: string,
principalId: string,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,43 @@ test("keeps Local enabled while a new remote Host connects", async () => {
);
});

test("reconnects an enabled remote Host with interactive SSH", async () => {
const root = await clientRoot();
const catalog = createClientRuntimeHostProfileCatalog(root);
await catalog.create(MANAGED_PROFILE, "opaque-token");
const calls: string[] = [];
const service = createDesktopRuntimeHostProfileService({
clientDataRoot: root,
startup: {
preferences: {
schemaVersion: 2,
defaultProfileId: LOCAL_RUNTIME_HOST_PROFILE.id,
enabledRemoteProfileIds: [MANAGED_PROFILE.id],
},
pairingIntents: [],
remotes: [{ profile: MANAGED_PROFILE, credential: "opaque-token" }],
unavailable: new Map(),
},
catalog,
states: () => [connectingLocal()],
enable: async (target, interaction) => {
calls.push(`enable:${target.profile.id}:${interaction}`);
},
disable: async (profileId) => {
calls.push(`disable:${profileId}`);
},
setDefault: () => undefined,
finalizePairing: async () => undefined,
});

await service.reconnect(MANAGED_PROFILE.id, MANAGED_PROFILE.rootId);

assert.deepEqual(calls, [
`disable:${MANAGED_PROFILE.id}`,
`enable:${MANAGED_PROFILE.id}:terminal`,
]);
});

test("does not enable the same State Root twice", async () => {
const root = await clientRoot();
const startup = await resolveDesktopRuntimeHostStartup(root);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -308,6 +308,70 @@ test('keeps a received management result when SSH teardown times out', async ()
await harness.terminal.close();
});

test('runs an exact update package and reports progress before an active-work result', async () => {
const harness = createHarness('pending');
const phases: string[] = [];
const update = harness.terminal.runUpdate(
{
destination: 'operator@example.com',
setupPackage: { kind: 'npm', specifier: 'maka-agent@1.3.0' },
expectedTarget: {
serviceId: 'b'.repeat(64),
rootPath: '/srv/maka',
rootId: 'a'.repeat(64),
},
},
(phase) => phases.push(phase),
);
await waitFor(() => harness.pty.hasDataListener());
const remoteCommand = harness.launchArgs.at(-1)?.at(-1) ?? '';
assert.match(remoteCommand, /--package.*maka-agent@1\.3\.0/u);
assert.match(remoteCommand, /runtime-host.*service.*update/u);
assert.match(remoteCommand, /MAKA_RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST/u);
harness.pty.emitData('Password: ');
harness.pty.emitData(
encodeRuntimeHostServiceManagementFrame({
schemaVersion: 1,
kind: 'progress',
action: 'update',
phase: 'retiring',
currentVersion: '1.2.3',
targetVersion: '1.3.0',
}),
);
harness.pty.emitData(
encodeRuntimeHostServiceManagementFrame({
schemaVersion: 1,
kind: 'result',
action: 'update',
service: {
platform: 'linux',
arch: 'x64',
osRelease: '6.8.0',
state: 'running',
pid: 42,
lastExitCode: 0,
installedVersion: '1.2.3',
projectDirectoryRoots: [],
},
update: {
kind: 'active_tasks',
currentVersion: '1.2.3',
targetVersion: '1.3.0',
},
}),
);
harness.pty.exit(1);

const result = await update;
assert.equal(result.kind, 'result');
assert.equal(result.kind === 'result' ? result.update.kind : undefined, 'active_tasks');
assert.deepEqual(phases, ['retiring']);
assert.deepEqual(harness.events.map(({ kind }) => kind), ['opened', 'data', 'connected']);
assert.doesNotMatch(JSON.stringify(harness.events), /MAKA_RUNTIME_HOST_SERVICE/u);
await harness.terminal.close();
});

test('keeps a prepared access credential out of the SSH terminal projection', async () => {
const harness = createHarness('pending');
const credential = 'maka_rh_secret-replacement';
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(runtime-host): add safe managed Host updates by M4n5ter · Pull Request #3591 · apache/maka · GitHub
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 numberDiff line numberDiff line change
Expand Up@@ -41,8 +41,8 @@ import {
} from '../runtime-host-desktop-manager.js';

test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, async () => {
const first = candidateHarness({ delayDisconnect: true });
const second = candidateHarness();
const first = candidateHarness({ delayDisconnect: true, hostEpoch: 'host-before' });
const second = candidateHarness({ hostEpoch: 'host-after' });
const queue = [ready(first.candidate), ready(second.candidate)];
let starts = 0;
const interactions: Array<string | undefined> = [];
Expand DownExpand Up@@ -71,6 +71,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
});

first.disconnect();
const replacementReady = owner.waitUntilReady(owner.defaultProfileId(), 'host-before');
const botMessage = owner.handleBotIncomingMessage({ text: 'hello' } as BotIncomingMessage);
const stop = owner.stopSession({
hostId: 'test-host',
Expand All@@ -95,7 +96,7 @@ test('replaces a disconnected Runtime Host generation', { timeout: 10_000 }, asy
assert.equal(second.botMessages, 0);
assert.deepEqual(second.stoppedSessions, []);
releaseSecond();
await Promise.all([botMessage, stop]);
await Promise.all([botMessage, stop, replacementReady]);

assert.equal(first.botMessages, 0);
assert.equal(second.botMessages, 1);
Expand DownExpand Up@@ -815,6 +816,7 @@ function candidateHarness(
activeTasks?: boolean;
lifecycleMode?: 'ephemeral' | 'service' | 'remote';
hostId?: string;
hostEpoch?: string;
finalizeFailures?: Error[];
disconnectOnFinalizeFailure?: boolean;
onPrepare?: () => void;
Expand All@@ -837,6 +839,7 @@ function candidateHarness(
hostLifecycleMode: options.lifecycleMode ?? 'ephemeral',
client: {
hostId: options.hostId ?? 'test-host',
hostEpoch: options.hostEpoch ?? 'test-host-epoch',
get lifecycleState() {
return lifecycleState;
},
Expand Down
146 changes: 146 additions & 0 deletions apps/desktop/src/main/__tests__/runtime-host-management.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@ import type {
DesktopRuntimeHostSshAccessInput,
DesktopRuntimeHostSshCleanupInput,
DesktopRuntimeHostSshManagementInput,
DesktopRuntimeHostSshUpdateInput,
} from '../runtime-host-ssh-terminal.js';

test('identifies, rotates, and revokes managed credentials without exposing secrets', async () => {
Expand DownExpand Up@@ -64,6 +65,7 @@ test('identifies, rotates, and revokes managed credentials without exposing secr
];

createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -185,6 +187,8 @@ test('manages only the service identity bound by Desktop onboarding', async () =
const uninstallOrder: string[] = [];
let operatorAccess = false;
let cleared = 0;
let statusGate: Promise<void> | undefined;
let releaseStatus: (() => void) | undefined;
const managedProfile = {
id: 'office',
name: 'Office',
Expand All@@ -203,6 +207,7 @@ test('manages only the service identity bound by Desktop onboarding', async () =
operatorPath: '/home/operator/.local/share/maka/operator',
};
const management = createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand All@@ -229,6 +234,7 @@ test('manages only the service identity bound by Desktop onboarding', async () =
},
runServiceManagement: async (input) => {
managementInputs.push(input);
if (input.action === 'status') await statusGate;
if (input.action === 'uninstall') {
uninstallOrder.push('uninstall-service');
}
Expand All@@ -243,6 +249,19 @@ test('manages only the service identity bound by Desktop onboarding', async () =
const run = handlers.get('runtime-host-management:run');
assert.ok(run);

statusGate = new Promise((resolve) => {
releaseStatus = resolve;
});
const firstStatus = run({}, 'office', 'status');
const secondStatus = run({}, 'office', 'status');
await Promise.resolve();
await Promise.resolve();
assert.equal(managementInputs.length, 1);
releaseStatus?.();
await Promise.all([firstStatus, secondStatus]);
statusGate = undefined;
managementInputs.length = 0;

await assert.rejects(
run({}, 'manual', 'uninstall') as Promise<unknown>,
/not bound to a managed service/u,
Expand DownExpand Up@@ -309,6 +328,121 @@ test('manages only the service identity bound by Desktop onboarding', async () =
assert.equal(handlers.size, 0);
});

test('publishes update progress and waits for the managed profile to reconnect', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const updates: DesktopRuntimeHostSshUpdateInput[] = [];
const progress: unknown[] = [];
const connectionCompletions: unknown[] = [];
let failConnection = false;
let bindingPresent = true;
let removeBindingAfterUpdate = false;
const profile = {
id: 'office',
name: 'Office',
kind: 'remote' as const,
rootId: 'a'.repeat(64),
transport: {
kind: 'ssh' as const,
destination: 'operator@example.com',
remotePort: 7443,
websocketPath: '/runtime-host',
},
};
const service = {
id: 'b'.repeat(64),
rootPath: '/srv/maka',
operatorPath: '/home/operator/.local/share/maka/operator',
};
createDesktopRuntimeHostManagement({
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
},
profiles: {
resolveManagedService: async () =>
bindingPresent ? { profile, service, state: 'active' as const } : undefined,
resolveManagedAccess: async () => undefined,
rotateManagedCredential: async () => assert.fail('credential rotation is not expected'),
markManagedServiceUninstalling: async (binding) => binding,
markManagedServiceCleanupPending: async (binding) => binding,
clearManagedServiceBinding: async () => undefined,
},
runServiceManagement: async () => assert.fail('ordinary management is not expected'),
runUpdate: async (input, onProgress) => {
updates.push(input);
onProgress('staging');
if (removeBindingAfterUpdate) bindingPresent = false;
return {
schemaVersion: 1,
kind: 'result',
action: 'update',
service: {
platform: 'linux',
arch: 'x64',
osRelease: '6.8.0',
state: 'running',
pid: 43,
lastExitCode: 0,
installedVersion: '1.3.0',
projectDirectoryRoots: [],
},
operatorCapabilities: ['access-management-v1'],
update: { kind: 'updated', previousVersion: '1.2.3', targetVersion: '1.3.0' },
};
},
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.3.0' }),
currentHostEpoch: () => 'host-before-update',
awaitUpdatedConnection: async (...args) => {
connectionCompletions.push(args);
if (failConnection) throw new Error('authentication required');
},
sendProgress: (event) => progress.push(event),
runAccessManagement: async () => assert.fail('access management is not expected'),
cleanupManagedDeployment: async () => assert.fail('cleanup is not expected'),
});

const update = handlers.get('runtime-host-management:update');
assert.ok(update);
const response = await update({}, profile.id, false);
assert.equal((response as { accessManagementAvailable: boolean }).accessManagementAvailable, true);
assert.deepEqual(updates, [{
destination: profile.transport.destination,
setupPackage: { kind: 'npm', specifier: 'maka-agent@1.3.0' },
expectedTarget: {
serviceId: service.id,
rootPath: service.rootPath,
rootId: profile.rootId,
},
}]);
assert.deepEqual(progress, [{ profileId: profile.id, phase: 'staging' }]);
assert.deepEqual(connectionCompletions, [
[profile.id, profile.rootId, 'host-before-update', true],
]);

removeBindingAfterUpdate = true;
const changedProfile = await update({}, profile.id, false);
assert.equal(
(changedProfile as { kind: string; error?: { message: string } }).error?.message,
'The Runtime Host update completed, but Desktop could not reconnect: ' +
'Runtime Host profile changed while its service was updating',
);

bindingPresent = true;
removeBindingAfterUpdate = false;
failConnection = true;
const reconnectFailure = await update({}, profile.id, false);
assert.deepEqual(reconnectFailure, {
schemaVersion: 1,
kind: 'error',
action: 'update',
error: {
code: 'desktop_reconnect_failed',
message:
'The Runtime Host update completed, but Desktop could not reconnect: authentication required',
},
});
});

test('resumes deployment cleanup without invoking the removed operator', async () => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const profile = {
Expand All@@ -333,6 +467,7 @@ test('resumes deployment cleanup without invoking the removed operator', async (
let state: 'active' | 'uninstalling' | 'cleanup_pending' = 'active';
let clearAttempts = 0;
createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -384,6 +519,7 @@ test('rechecks uninstall intent before retrying the remote service', async () =>
const handlers = new Map<string, (...args: unknown[]) => unknown>();
let marked = false;
createDesktopRuntimeHostManagement({
...unusedUpdateDependencies(),
ipcMain: {
handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
removeHandler: (channel) => handlers.delete(channel),
Expand DownExpand Up@@ -464,6 +600,16 @@ function serviceResult(
: { ...result, action };
}

function unusedUpdateDependencies() {
return {
runUpdate: async (): Promise<never> => assert.fail('update is not expected'),
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.2.3' } as const),
currentHostEpoch: () => undefined,
awaitUpdatedConnection: async () => undefined,
sendProgress: () => undefined,
};
}

function accessCredential(
credentialId: string,
principalId: string,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,43 @@ test("keeps Local enabled while a new remote Host connects", async () => {
);
});

test("reconnects an enabled remote Host with interactive SSH", async () => {
const root = await clientRoot();
const catalog = createClientRuntimeHostProfileCatalog(root);
await catalog.create(MANAGED_PROFILE, "opaque-token");
const calls: string[] = [];
const service = createDesktopRuntimeHostProfileService({
clientDataRoot: root,
startup: {
preferences: {
schemaVersion: 2,
defaultProfileId: LOCAL_RUNTIME_HOST_PROFILE.id,
enabledRemoteProfileIds: [MANAGED_PROFILE.id],
},
pairingIntents: [],
remotes: [{ profile: MANAGED_PROFILE, credential: "opaque-token" }],
unavailable: new Map(),
},
catalog,
states: () => [connectingLocal()],
enable: async (target, interaction) => {
calls.push(`enable:${target.profile.id}:${interaction}`);
},
disable: async (profileId) => {
calls.push(`disable:${profileId}`);
},
setDefault: () => undefined,
finalizePairing: async () => undefined,
});

await service.reconnect(MANAGED_PROFILE.id, MANAGED_PROFILE.rootId);

assert.deepEqual(calls, [
`disable:${MANAGED_PROFILE.id}`,
`enable:${MANAGED_PROFILE.id}:terminal`,
]);
});

test("does not enable the same State Root twice", async () => {
const root = await clientRoot();
const startup = await resolveDesktopRuntimeHostStartup(root);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -308,6 +308,70 @@ test('keeps a received management result when SSH teardown times out', async ()
await harness.terminal.close();
});

test('runs an exact update package and reports progress before an active-work result', async () => {
const harness = createHarness('pending');
const phases: string[] = [];
const update = harness.terminal.runUpdate(
{
destination: 'operator@example.com',
setupPackage: { kind: 'npm', specifier: 'maka-agent@1.3.0' },
expectedTarget: {
serviceId: 'b'.repeat(64),
rootPath: '/srv/maka',
rootId: 'a'.repeat(64),
},
},
(phase) => phases.push(phase),
);
await waitFor(() => harness.pty.hasDataListener());
const remoteCommand = harness.launchArgs.at(-1)?.at(-1) ?? '';
assert.match(remoteCommand, /--package.*maka-agent@1\.3\.0/u);
assert.match(remoteCommand, /runtime-host.*service.*update/u);
assert.match(remoteCommand, /MAKA_RUNTIME_HOST_OPERATOR_CAPABILITY_REQUEST/u);
harness.pty.emitData('Password: ');
harness.pty.emitData(
encodeRuntimeHostServiceManagementFrame({
schemaVersion: 1,
kind: 'progress',
action: 'update',
phase: 'retiring',
currentVersion: '1.2.3',
targetVersion: '1.3.0',
}),
);
harness.pty.emitData(
encodeRuntimeHostServiceManagementFrame({
schemaVersion: 1,
kind: 'result',
action: 'update',
service: {
platform: 'linux',
arch: 'x64',
osRelease: '6.8.0',
state: 'running',
pid: 42,
lastExitCode: 0,
installedVersion: '1.2.3',
projectDirectoryRoots: [],
},
update: {
kind: 'active_tasks',
currentVersion: '1.2.3',
targetVersion: '1.3.0',
},
}),
);
harness.pty.exit(1);

const result = await update;
assert.equal(result.kind, 'result');
assert.equal(result.kind === 'result' ? result.update.kind : undefined, 'active_tasks');
assert.deepEqual(phases, ['retiring']);
assert.deepEqual(harness.events.map(({ kind }) => kind), ['opened', 'data', 'connected']);
assert.doesNotMatch(JSON.stringify(harness.events), /MAKA_RUNTIME_HOST_SERVICE/u);
await harness.terminal.close();
});

test('keeps a prepared access credential out of the SSH terminal projection', async () => {
const harness = createHarness('pending');
const credential = 'maka_rh_secret-replacement';
Expand Down
Loading