Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6c1485c
feat(desktop): replace native recovery dialogs
M4n5ter Sep 1, 2026
b338701
fix(desktop): harden recovery window lifecycle
M4n5ter Sep 1, 2026
b980132
test(desktop): keep dialog assertions behavioral
M4n5ter Sep 1, 2026
bcaf080
fix(desktop): close remaining recovery gaps
M4n5ter Sep 1, 2026
3441f46
Merge remote-tracking branch 'origin/main' into feat/maka-browser-rec…
M4n5ter Sep 1, 2026
04c64a4
fix(desktop): converge recovery dialog behavior
M4n5ter Sep 1, 2026
3258a39
fix(desktop): keep recovery decisions conservative
M4n5ter Sep 1, 2026
800763b
fix(runtime-host): fence silent replacement connections
M4n5ter Sep 1, 2026
78b8c93
fix(desktop): default Host prompts to cancellation
M4n5ter Sep 1, 2026
58c920c
fix(desktop): keep recovery transitions safe
M4n5ter Sep 1, 2026
b11a555
fix(desktop): harden recovery decision boundaries
M4n5ter Sep 1, 2026
dca3c42
fix(desktop): bind recovery readiness to reload frame
M4n5ter Sep 1, 2026
9d418fc
refactor(desktop): keep renderer frame identity canonical
M4n5ter Sep 1, 2026
611945b
refactor(desktop): unify dialog fallback path
M4n5ter Sep 1, 2026
09c9bbc
Merge remote-tracking branch 'origin/main' into feat/maka-browser-rec…
M4n5ter Sep 1, 2026
76c9ff3
chore(runtime-host): declare idle helper compatible
M4n5ter Sep 1, 2026
8319e4d
chore(desktop): approve dialog document loader
M4n5ter Sep 1, 2026
b7dfba7
fix(desktop): issue live peer routes for shared sessions
M4n5ter Sep 1, 2026
36af5a1
fix(peer): expose live relay routes to shared sessions
M4n5ter Sep 1, 2026
0722acc
fix(desktop): surface pending turn requests
M4n5ter Sep 1, 2026
ae816a3
fix(peer): recover idle shared-session connections
M4n5ter Sep 1, 2026
f060bec
fix(desktop): keep collaboration hook scope observable
M4n5ter Sep 1, 2026
71698b4
fix(desktop): streamline collaboration invitation transfer
M4n5ter Sep 1, 2026
9f90936
fix(desktop): preserve available collaboration inboxes
M4n5ter Sep 1, 2026
7460ebb
fix(peer): retain authenticated guest routes
M4n5ter Sep 1, 2026
7271953
fix(desktop): compact turn approval prompt
M4n5ter Sep 1, 2026
5086daf
docs(pr): refresh turn approval screenshot
M4n5ter Sep 1, 2026
de8dba0
chore: remove PR screenshot asset
M4n5ter Sep 1, 2026
5ce11b3
fix(desktop): preserve Session rail render isolation
M4n5ter Sep 1, 2026
3bf062b
docs(ui): refresh Astryx surface inventory
M4n5ter Sep 1, 2026
2184e74
Merge remote-tracking branch 'origin/main' into feat/maka-browser-rec…
M4n5ter Sep 2, 2026
e582b74
Merge remote-tracking branch 'origin/fix/session-share-peer-route-liv…
M4n5ter Sep 2, 2026
23e811a
Merge remote-tracking branch 'origin/fix/session-share-peer-route-liv…
M4n5ter Sep 2, 2026
ab6de3a
Merge remote-tracking branch 'origin/fix/session-share-peer-route-liv…
M4n5ter Sep 2, 2026
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
100 changes: 100 additions & 0 deletions apps/desktop/src/main/__tests__/browser-message-box.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
buildBrowserMessageBoxHtml,
centeredBounds,
parseBrowserMessageBoxResponse,
} from '../browser-message-box.js';

test('accepts only an in-range response URL produced by the dialog', () => {
assert.equal(parseBrowserMessageBoxResponse('maka-dialog://response/1', 3), 1);
for (const value of [
'https://response/1',
'maka-dialog://other/1',
'maka-dialog://user@response/1',
'maka-dialog://response:123/1',
'maka-dialog://response/01',
'maka-dialog://response/1?',
'maka-dialog://response/1#',
'maka-dialog://response/1?again=true',
'maka-dialog://response/3',
'not a url',
]) {
assert.equal(parseBrowserMessageBoxResponse(value, 3), undefined, value);
}
});

test('centers against the parent while keeping the whole dialog on-screen', () => {
assert.deepEqual(
centeredBounds(
{ x: 900, y: 700, width: 200, height: 100 },
{ x: 0, y: 0, width: 1_000, height: 800 },
520,
300,
),
{ x: 480, y: 500, width: 520, height: 300 },
);
assert.deepEqual(
centeredBounds(undefined, { x: -1_000, y: 40, width: 800, height: 600 }, 400, 280),
{ x: -800, y: 200, width: 400, height: 280 },
);
});

test('renders escaped content with Maka dialog tokens and safe action ordering', () => {
const html = buildBrowserMessageBoxHtml(
{
type: 'warning',
title: '<img src=x onerror=alert(1)>',
message: 'Maka & Runtime Host',
detail: '</div><script>globalThis.pwned = true</script>',
buttons: ['Replace <Host>', 'Cancel', 'Copy & Diagnostics'],
defaultId: 0,
cancelId: 1,
},
{
buttons: ['Replace <Host>', 'Cancel', 'Copy & Diagnostics'],
defaultId: 0,
cancelId: 1,
dark: true,
},
);

assert.match(html, /data-theme="dark"/u);
assert.match(html, /class="wordmark"/u);
assert.match(html, /color: #71a8fd/u);
assert.match(html, /border-radius: 12px/u);
assert.match(html, /height: 32px/u);
assert.match(html, /&lt;img src=x onerror=alert\(1\)&gt;/u);
assert.match(html, /Maka &amp; Runtime Host/u);
assert.match(html, /&lt;\/div&gt;&lt;script&gt;globalThis\.pwned = true&lt;\/script&gt;/u);
assert.match(html, /Replace &lt;Host&gt;/u);
assert.doesNotMatch(html, /<img src=x/u);
assert.doesNotMatch(html, /<script>globalThis\.pwned/u);

const cancelPosition = html.indexOf('>Cancel</button>');
const copyPosition = html.indexOf('>Copy &amp; Diagnostics</button>');
const actionPosition = html.indexOf('>Replace &lt;Host&gt;</button>');
assert.ok(cancelPosition >= 0 && cancelPosition < copyPosition);
assert.ok(copyPosition < actionPosition);
assert.match(html, /class="decision primary"[^>]*data-response="0" autofocus/u);
assert.match(html, /default-src 'none'/u);
});
11 changes: 6 additions & 5 deletions apps/desktop/src/main/__tests__/native-diagnostic-dialog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const diagnosticEnvironment = () => ({
processUptimeSeconds: 3,
});

test('copies diagnostics as an auxiliary native-dialog action', async () => {
test('copies diagnostics as an auxiliary dialog action', async () => {
const shown: MessageBoxOptions[] = [];
const responses = [2, 1];
let copies = 0;
Expand Down Expand Up @@ -80,7 +80,7 @@ test('copies diagnostics as an auxiliary native-dialog action', async () => {
assert.match(shown[1]?.detail ?? '', /Diagnostics copied/);
});

test('fatal startup errors remain copyable without a renderer or BrowserWindow', async () => {
test('fatal startup errors remain copyable before the main Renderer exists', async () => {
const shown: MessageBoxOptions[] = [];
const responses = [1, 0];
let clipboard = '';
Expand Down Expand Up @@ -141,9 +141,10 @@ test('main Renderer loss keeps Copy Diagnostics auxiliary to recovery', async ()
},
});

assert.equal(decision, 'relaunch');
assert.deepEqual(shown[0]?.buttons, ['Relaunch', 'Exit', 'Copy Diagnostics']);
assert.deepEqual(shown[1]?.buttons, ['Relaunch', 'Exit', 'Copy Again']);
assert.equal(decision, 'recover');
assert.deepEqual(shown[0]?.buttons, ['Recover Interface', 'Exit', 'Copy Diagnostics']);
assert.deepEqual(shown[1]?.buttons, ['Recover Interface', 'Exit', 'Copy Again']);
assert.match(shown[0]?.detail ?? '', /without restarting Maka/);
assert.match(clipboard, /Surface: renderer_process_gone/);
assert.match(clipboard, /Reason: oom/);
assert.match(clipboard, /Exit code: 137/);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1235,7 +1235,7 @@ test('waits passively for a Host that cannot be taken over', async () => {
await owner.close();
});

test('replaces a non-restartable Local Host through the supplied authority and retries', async () => {
test('silently replaces an idle non-restartable Local Host and retries', async () => {
const observed = upgradeRequired(false);
const conflict = {
...observed,
Expand All @@ -1244,6 +1244,7 @@ test('replaces a non-restartable Local Host through the supplied authority and r
const replacement = candidateHarness();
let starts = 0;
let replaced: typeof observed.registration | undefined;
const policies: string[] = [];
const owner = await startRuntimeHostDesktopManager(
{} as DesktopRuntimeHostCandidateStartInput,
{
Expand All @@ -1253,23 +1254,95 @@ test('replaces a non-restartable Local Host through the supplied authority and r
},
upgradePrompts: {
restartable: async () => assert.fail('non-restartable conflict used restart prompt'),
nonRestartable: async (_conflict, actions) => {
assert.deepEqual(actions, { canReplace: true, canWait: false });
return 'replace';
},
nonRestartable: async () => assert.fail('idle replaceable Host must not prompt'),
},
resolveLocalHostReplacement: async (registration) => ({
replace: async () => {
replace: async (policy) => {
policies.push(policy);
replaced = registration;
return 'replaced';
},
}),
},
);
assert.equal(starts, 2);
assert.equal(replaced?.hostEpoch, conflict.registration.hostEpoch);
assert.deepEqual(policies, ['refuse_active_work']);
await owner.close();
});

test('prompts only after a non-restartable Local Host reports active tasks', async () => {
const observed = upgradeRequired(false);
const conflict = {
...observed,
registration: { ...observed.registration, lifecycleMode: 'service' as const },
};
const replacement = candidateHarness();
const policies: string[] = [];
let starts = 0;
let prompts = 0;
const owner = await startRuntimeHostDesktopManager(
{} as DesktopRuntimeHostCandidateStartInput,
{
startCandidate: async () => {
starts += 1;
return starts === 1 ? conflict : ready(replacement.candidate);
},
upgradePrompts: {
restartable: async () => assert.fail('non-restartable conflict used restart prompt'),
nonRestartable: async (_conflict, actions) => {
prompts += 1;
assert.deepEqual(actions, {
canReplace: true,
canWait: false,
activeTasksDetected: true,
});
return 'replace';
},
},
resolveLocalHostReplacement: async () => ({
replace: async (policy) => {
policies.push(policy);
return policy === 'refuse_active_work' ? 'active_tasks' : 'replaced';
},
}),
},
);

assert.equal(prompts, 1);
assert.equal(starts, 2);
assert.deepEqual(policies, ['refuse_active_work', 'interrupt_active_work']);
await owner.close();
});

test('does not authorize active-work interruption when replacement is cancelled', async () => {
const observed = upgradeRequired(false);
const conflict = {
...observed,
registration: { ...observed.registration, lifecycleMode: 'service' as const },
};
const policies: string[] = [];

await assert.rejects(
startRuntimeHostDesktopManager({} as DesktopRuntimeHostCandidateStartInput, {
startCandidate: async () => conflict,
upgradePrompts: {
restartable: async () => assert.fail('non-restartable conflict used restart prompt'),
nonRestartable: async () => 'cancel',
},
resolveLocalHostReplacement: async () => ({
replace: async (policy) => {
policies.push(policy);
return 'active_tasks';
},
}),
onFatalError: () => undefined,
}),
RuntimeHostUpgradeCancelledError,
);
assert.deepEqual(policies, ['refuse_active_work']);
});

test('lets the user cancel startup when an incompatible Host owns the root', async () => {
const conflict = incompatibleHost('blocked_by_residency');
let presented: DesktopRuntimeHostCandidateStartResult | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,14 +294,14 @@ test('repairs an existing managed Host with the current setup package and restar
assert.deepEqual(actions, ['update', 'restart']);
});

test('replaces a conflicting supervised Host through canonical authority without a receipt', async (t) => {
test('replaces a conflicting supervised Host with the requested active-work policy', async (t) => {
const base = await mkdtemp(join(tmpdir(), 'maka-local-managed-conflict-'));
t.after(() => rm(base, { recursive: true, force: true }));
const clientDataRoot = join(base, 'client');
const rootPath = join(clientDataRoot, 'workspaces', 'default');
const rootId = 'a'.repeat(64);
await mkdir(rootPath, { recursive: true });
let updated = false;
const policies: Array<boolean | undefined> = [];
const service = createDesktopLocalRuntimeHostRemoteAccess({
ipcMain: { handle() {}, removeHandler() {} },
clientDataRoot,
Expand Down Expand Up @@ -331,8 +331,14 @@ test('replaces a conflicting supervised Host through canonical authority without
assert.equal(input.target.rootId, rootId);
assert.equal(input.target.deploymentId, RECOVERY_DEPLOYMENT_ID);
assert.deepEqual(input.expectedHost, { hostEpoch: 'older-host', pid: 42 });
assert.equal(input.allowInterruptActiveTasks, true);
updated = true;
policies.push(input.allowInterruptActiveTasks);
if (!input.allowInterruptActiveTasks) {
return {
kind: 'error' as const,
action: 'update' as const,
error: { code: 'active_tasks', message: 'active work remains' },
} as never;
}
return {
kind: 'result' as const,
action: 'update' as const,
Expand All @@ -349,8 +355,9 @@ test('replaces a conflicting supervised Host through canonical authority without
new AbortController().signal,
);
assert.ok(replacement);
await replacement.replace();
assert.equal(updated, true);
assert.equal(await replacement.replace('refuse_active_work'), 'active_tasks');
assert.equal(await replacement.replace('interrupt_active_work'), 'replaced');
assert.deepEqual(policies, [undefined, true]);
});

test('does not persist recoverable setup authority before Desktop ownership commits', async (t) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,20 @@ test('does not offer passive waiting for a supervised Host', async () => {
'cancel',
);
});

test('explains when the safe replacement check found active background work', () => {
const conflict = {
kind: 'upgrade_required' as const,
restartable: false as const,
registration: { pid: 42, lifecycleMode: 'service' as const },
} as Parameters<typeof buildRuntimeHostUpgradeDialog>[0];
const dialog = buildRuntimeHostUpgradeDialog(
conflict,
{ action: 'replace', canWait: false, activeTasksDetected: true },
'zh',
);

assert.match(dialog.options.detail ?? '', /仍有后台任务在运行/u);
assert.doesNotMatch(dialog.options.detail ?? '', /无法报告后台活动/u);
assert.equal(dialog.options.defaultId, dialog.options.cancelId);
});
Loading
Loading