Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ test('stops reconnecting when the replacement Host is incompatible', async () =>
await owner.close();
});

test('restarts a generation-aware Host through its exact takeover handshake', async () => {
test('restarts an idle generation-aware Host without prompting', async () => {
const replacement = candidateHarness();
const starts: DesktopRuntimeHostCandidateStartInput[] = [];
const conflict = upgradeRequired(true);
Expand All @@ -613,7 +613,7 @@ test('restarts a generation-aware Host through its exact takeover handshake', as
return starts.length === 1 ? conflict : ready(replacement.candidate);
},
upgradePrompts: {
restartable: async () => 'restart',
restartable: async () => assert.fail('idle Host must not prompt before restart'),
waitOnly: async () => assert.fail('restartable conflict used wait-only prompt'),
},
});
Expand All @@ -623,6 +623,46 @@ test('restarts a generation-aware Host through its exact takeover handshake', as
await owner.close();
});

test('prompts before restarting a generation-aware Host with active work', async () => {
const replacement = candidateHarness();
const conflict = upgradeRequired(true, 1);
let prompts = 0;
const owner = await startRuntimeHostDesktopManager({} as DesktopRuntimeHostCandidateStartInput, {
startCandidate: async (input) =>
input.takeoverHostEpoch ? ready(replacement.candidate) : conflict,
upgradePrompts: {
restartable: async () => {
prompts += 1;
return 'restart';
},
waitOnly: async () => assert.fail('restartable conflict used wait-only prompt'),
},
});

assert.equal(prompts, 1);
await owner.close();
});

test('prompts before restarting a generation-aware Host with a residency', async () => {
const replacement = candidateHarness();
const conflict = upgradeRequired(true, 0, [{ label: 'goal', count: 1 }]);
let prompts = 0;
const owner = await startRuntimeHostDesktopManager({} as DesktopRuntimeHostCandidateStartInput, {
startCandidate: async (input) =>
input.takeoverHostEpoch ? ready(replacement.candidate) : conflict,
upgradePrompts: {
restartable: async () => {
prompts += 1;
return 'restart';
},
waitOnly: async () => assert.fail('restartable conflict used wait-only prompt'),
},
});

assert.equal(prompts, 1);
await owner.close();
});

test('waits passively for a Host that cannot be taken over', async () => {
const conflict = upgradeRequired(false);
let starts = 0;
Expand Down Expand Up @@ -699,6 +739,8 @@ function incompatibleHost(

function upgradeRequired(
restartable: boolean,
activeOperations = 0,
residencies: readonly { readonly label: string; readonly count: number }[] = [],
): Extract<DesktopRuntimeHostCandidateStartResult, { kind: 'upgrade_required' }> {
const registration = hostRegistration(
restartable ? { lifecycleMode: 'ephemeral' } : {},
Expand All @@ -723,9 +765,9 @@ function upgradeRequired(
replacement: 'blocked_by_residency',
activity: {
connections: 0,
activeOperations: 0,
activeOperations,
processUptimeSeconds: 60,
residencies: [],
residencies,
},
},
};
Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src/main/runtime-host-desktop-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,11 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager {
return result.candidate;
}
if (result.kind === 'upgrade_required' && result.restartable) {
const decision = await this.#resolveRestartable(result);
const activity = result.handshake?.activity;
const decision =
activity && activity.activeOperations === 0 && activity.residencies.length === 0
? 'restart'
: await this.#resolveRestartable(result);
if (decision === 'cancel') {
throw new RuntimeHostUpgradeCancelledError();
}
Expand Down