Skip to content

Commit b6ef648

Browse files
fix(desktop): localize expected errors
Raw exception text no longer reaches the UI: expected failures carry machine-readable codes (with params where needed) that the renderer maps through locale catalogs, and unexpected failures show a localized fallback while redacted diagnostics go to the console.
1 parent 49858c4 commit b6ef648

27 files changed

Lines changed: 551 additions & 260 deletions

apps/desktop/renderer-architecture.json

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"src/renderer/desktop-slash-command.ts",
5757
"src/renderer/desktop-transcript-range-store.ts",
5858
"src/renderer/error-boundary.tsx",
59+
"src/renderer/expected-operation-error.ts",
5960
"src/renderer/follow-up-submit-routing.ts",
6061
"src/renderer/interrupted-resume.ts",
6162
"src/renderer/keyboard-help.tsx",
@@ -89,6 +90,7 @@
8990
"src/renderer/locales/settings-web-search-copy.ts",
9091
"src/renderer/locales/shell-copy.ts",
9192
"src/renderer/locales/shell-remaining-copy.ts",
93+
"src/renderer/locales/workhub-copy.ts",
9294
"src/renderer/main.tsx",
9395
"src/renderer/mcp-brand-contrast.ts",
9496
"src/renderer/mcp-brand-marks.tsx",
@@ -278,6 +280,7 @@
278280
"src/renderer/features/module-hub/controller/use-scheduled-tasks-controller.ts -> src/renderer/locales/shell-remaining-copy",
279281
"src/renderer/features/module-hub/controller/use-skills-controller.ts -> src/renderer/locales/shell-copy",
280282
"src/renderer/features/module-hub/ui/module-hub-host.tsx -> src/renderer/mcp-page",
283+
"src/renderer/features/session-collaboration/ui/session-collaboration-join-dialog.tsx -> src/renderer/expected-operation-error",
281284
"src/renderer/features/session-navigation/controller/session-row-actions.ts -> src/renderer/locales/shell-copy",
282285
"src/renderer/features/session-navigation/controller/use-session-navigation-controller.ts -> src/renderer/use-external-store-selector",
283286
"src/renderer/features/session-navigation/controller/use-session-navigation-reads.ts -> src/renderer/use-external-store-selector",
@@ -475,14 +478,14 @@
475478
"unresolvedDependencies": 0,
476479
"actionFactories": [],
477480
"dependencyPaths": {
481+
"./expected-operation-error.js": 1,
478482
"./locales/shell-copy.js": 1,
479483
"@maka/core/llm-connections": 1,
480-
"@maka/core/redaction": 1,
481484
"@maka/core/text-file-import": 1,
482485
"@maka/core/ui-locale": 1
483486
},
484487
"importSpecifiers": 6,
485-
"nonTriviaTokens": 515
488+
"nonTriviaTokens": 372
486489
},
487490
"src/renderer/app-shell-detail-panel.tsx": {
488491
"importDeclarations": 1,
@@ -1664,6 +1667,17 @@
16641667
"react": 1
16651668
}
16661669
},
1670+
"src/renderer/expected-operation-error.ts": {
1671+
"bridgePaths": {},
1672+
"environmentCapabilities": {},
1673+
"hookCalls": {},
1674+
"lifecycleMethods": {},
1675+
"unresolvedDependencies": 0,
1676+
"actionFactories": [],
1677+
"dependencyPaths": {
1678+
"@maka/core/redaction": 1
1679+
}
1680+
},
16671681
"src/renderer/follow-up-submit-routing.ts": {
16681682
"bridgePaths": {},
16691683
"environmentCapabilities": {},
@@ -1791,6 +1805,7 @@
17911805
"unresolvedDependencies": 0,
17921806
"actionFactories": [],
17931807
"dependencyPaths": {
1808+
"../../shared/work-board-ipc.js": 1,
17941809
"@maka/core/connection-readiness": 1,
17951810
"@maka/core/model-call-attempt": 1,
17961811
"@maka/core/session-send-projection": 1,
@@ -2060,10 +2075,10 @@
20602075
"unresolvedDependencies": 0,
20612076
"actionFactories": [],
20622077
"dependencyPaths": {
2078+
"../expected-operation-error.js": 1,
20632079
"@maka/core/goal": 1,
20642080
"@maka/core/model-thinking": 1,
20652081
"@maka/core/permission": 1,
2066-
"@maka/core/redaction": 1,
20672082
"@maka/core/settings": 1,
20682083
"@maka/core/slash-command-catalog": 1,
20692084
"@maka/core/ui-locale": 1
@@ -2080,6 +2095,17 @@
20802095
"@maka/core/ui-locale": 1
20812096
}
20822097
},
2098+
"src/renderer/locales/workhub-copy.ts": {
2099+
"bridgePaths": {},
2100+
"environmentCapabilities": {},
2101+
"hookCalls": {},
2102+
"lifecycleMethods": {},
2103+
"unresolvedDependencies": 0,
2104+
"actionFactories": [],
2105+
"dependencyPaths": {
2106+
"@maka/core/ui-locale": 1
2107+
}
2108+
},
20832109
"src/renderer/mcp-brand-contrast.ts": {
20842110
"bridgePaths": {},
20852111
"environmentCapabilities": {},
@@ -2373,6 +2399,7 @@
23732399
"unresolvedDependencies": 0,
23742400
"actionFactories": [],
23752401
"dependencyPaths": {
2402+
"./expected-operation-error.js": 1,
23762403
"./locales/plan-mode-copy.js": 1,
23772404
"@astryxdesign/core/Banner": 1,
23782405
"@astryxdesign/core/Collapsible": 1,
@@ -2413,6 +2440,7 @@
24132440
"actionFactories": [],
24142441
"dependencyPaths": {
24152442
"../preload/bridge-contract.js": 1,
2443+
"./expected-operation-error.js": 1,
24162444
"./locales/shell-copy.js": 1,
24172445
"@astryxdesign/core/Button": 1,
24182446
"@astryxdesign/core/Dialog": 1,
@@ -2481,6 +2509,7 @@
24812509
"unresolvedDependencies": 0,
24822510
"actionFactories": [],
24832511
"dependencyPaths": {
2512+
"./expected-operation-error.js": 1,
24842513
"./locales/session-collaboration-copy.js": 1,
24852514
"./session-turn-request-composer.js": 1,
24862515
"@astryxdesign/core": 1,
@@ -2618,6 +2647,7 @@
26182647
"unresolvedDependencies": 0,
26192648
"actionFactories": [],
26202649
"dependencyPaths": {
2650+
"./expected-operation-error.js": 1,
26212651
"./locales/session-collaboration-copy.js": 1,
26222652
"@maka/runtime-host/protocol": 1,
26232653
"@maka/ui": 1,
@@ -5262,6 +5292,7 @@
52625292
"actionFactories": [],
52635293
"dependencyPaths": {
52645294
"../shared/work-board-ipc.js": 1,
5295+
"./expected-operation-error.js": 1,
52655296
"@maka/core/work-board": 1,
52665297
"@maka/storage/work-board-store": 1,
52675298
"react": 1
@@ -5281,6 +5312,7 @@
52815312
"unresolvedDependencies": 0,
52825313
"actionFactories": [],
52835314
"dependencyPaths": {
5315+
"./expected-operation-error.js": 1,
52845316
"./locales/conversation-copy.js": 1,
52855317
"./use-work-board.js": 1,
52865318
"@astryxdesign/core": 1,
@@ -5300,6 +5332,7 @@
53005332
"unresolvedDependencies": 0,
53015333
"actionFactories": [],
53025334
"dependencyPaths": {
5335+
"./expected-operation-error.js": 1,
53035336
"./workhub-route-policy.js": 1,
53045337
"@maka/runtime-host/protocol": 1
53055338
}
@@ -5398,6 +5431,8 @@
53985431
"unresolvedDependencies": 0,
53995432
"actionFactories": [],
54005433
"dependencyPaths": {
5434+
"./expected-operation-error.js": 1,
5435+
"./locales/workhub-copy.js": 2,
54015436
"./workhub-controller.js": 1,
54025437
"./workhub-coordination-port.js": 1,
54035438
"./workhub-send-lease.js": 1,
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import assert from 'node:assert/strict';
21+
import test from 'node:test';
22+
import { getDesktopConversationCopy } from '../../renderer/locales/conversation-copy.js';
23+
import { getSessionCollaborationCopy } from '../../renderer/locales/session-collaboration-copy.js';
24+
import {
25+
ExpectedOperationError,
26+
expectedOperationErrorMessage,
27+
} from '../../renderer/expected-operation-error.js';
28+
import { sessionCollaborationImportErrorMessage } from '../../renderer/features/session-collaboration/testing.js';
29+
import { messageReadErrorMessage } from '../../renderer/app-shell-copy.js';
30+
import { localizedShellErrorMessage } from '../../renderer/locales/shell-copy.js';
31+
32+
test('routes Work Board codes through each locale catalog', () => {
33+
for (const locale of ['zh', 'en'] as const) {
34+
const copy = getDesktopConversationCopy(locale).workBoardPanel;
35+
assert.equal(
36+
expectedOperationErrorMessage(
37+
new ExpectedOperationError('not_found'),
38+
copy.errors,
39+
copy.actionFailed,
40+
'test',
41+
),
42+
copy.errors.not_found,
43+
);
44+
}
45+
});
46+
47+
test('routes structured collaboration failures through each locale catalog', () => {
48+
const cases = [
49+
[{ kind: 'error', reason: 'invalid_code' } as const, 'invalidCode'],
50+
[{ kind: 'error', reason: 'peer_path_unavailable' } as const, 'directPathUnavailable'],
51+
[{ kind: 'error', reason: 'connection_failed' } as const, 'connectionFailed'],
52+
] as const;
53+
for (const locale of ['zh', 'en'] as const) {
54+
const copy = getSessionCollaborationCopy(locale);
55+
for (const [result, key] of cases) {
56+
assert.equal(sessionCollaborationImportErrorMessage(copy, result), copy[key]);
57+
}
58+
assert.equal(
59+
sessionCollaborationImportErrorMessage(copy, {
60+
kind: 'error',
61+
reason: 'mount_limit_reached',
62+
params: { max: 12 },
63+
}),
64+
copy.mountLimit(12),
65+
);
66+
}
67+
});
68+
69+
test('uses localized fallbacks instead of classifying raw exception text', (context) => {
70+
context.mock.method(console, 'error', () => undefined);
71+
const raw = new Error('timeout 401 网络失败 MAKA_SESSION_READ_MESSAGES_ERROR: 后端中文');
72+
assert.equal(
73+
messageReadErrorMessage(raw, 'en'),
74+
'Task content is temporarily unavailable. Try again later.',
75+
);
76+
assert.equal(messageReadErrorMessage(raw, 'zh'), '任务内容暂时无法读取,请稍后重试。');
77+
assert.equal(localizedShellErrorMessage(raw, 'English fallback', 'en'), 'English fallback');
78+
assert.equal(localizedShellErrorMessage(raw, '中文兜底', 'zh'), '中文兜底');
79+
});

apps/desktop/src/main/__tests__/plan-mode-copy.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ test('localizes Plan Mode chrome and abandon confirmation without rewriting plan
2929
assert.equal(en.proposal.statuses.approved, 'Approved');
3030
assert.equal(zh.execution.stepCount(2, 3), '2/3 步');
3131
assert.equal(en.execution.stepCount(1, 1), '1/1 step');
32+
assert.equal(zh.operationFailed, '计划操作失败,请稍后重试。');
33+
assert.equal(en.operationFailed, 'The plan action failed. Try again later.');
3234
assert.deepEqual(
3335
{
3436
title: en.abandonConfirmation.title,

apps/desktop/src/main/__tests__/runtime-host-guest-session-mounts.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,30 @@ test('removes failed activation desire instead of creating recoverable profile s
6969
});
7070

7171
const result = await mounts.importInvitation(invitation('guest-two'), false, 'import-two');
72-
assert.deepEqual(result.kind === 'error' ? result.reason : result.kind, 'peer_path_unavailable');
72+
assert.deepEqual(result, { kind: 'error', reason: 'peer_path_unavailable' });
7373
assert.deepEqual(await store.read(), []);
7474
assert.equal(unmounted.length, 1);
7575
});
7676

77+
test('logs unexpected activation failures without returning Host details', async (context) => {
78+
const diagnostics: string[] = [];
79+
context.mock.method(console, 'error', (...values: unknown[]) => {
80+
diagnostics.push(values.map(String).join(' '));
81+
});
82+
const mounts = service(memoryStore(), {
83+
mount: async () => {
84+
throw new Error('Authorization: Bearer very-secret-token');
85+
},
86+
});
87+
88+
const result = await mounts.importInvitation(invitation('guest-failed'), false, 'import-failed');
89+
90+
assert.deepEqual(result, { kind: 'error', reason: 'connection_failed' });
91+
assert.equal(diagnostics.length, 1);
92+
assert.match(diagnostics[0]!, /session-collaboration.*import failed/u);
93+
assert.doesNotMatch(diagnostics[0]!, /very-secret-token/u);
94+
});
95+
7796
test('settles admitted finalization before committing unmount desire', async () => {
7897
const store = memoryStore();
7998
let started!: () => void;

apps/desktop/src/main/__tests__/work-board-ipc-main.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,16 +164,18 @@ describe('Work Board IPC', () => {
164164
{ title: 'stale write' },
165165
{ expectedRevision: 1 },
166166
);
167-
assert.equal(staleRename.ok, false);
168-
if (!staleRename.ok) assert.equal(staleRename.code, 'operation_conflict');
167+
assert.deepEqual(staleRename, {
168+
ok: false,
169+
error: { code: 'operation_conflict' },
170+
});
169171

170172
const removedBeforeArchive = await ipc.invoke<WorkBoardIpcResult<null>>(
171173
'workBoard:remove',
172174
id,
173175
);
174176
assert.equal(removedBeforeArchive.ok, false);
175177
if (!removedBeforeArchive.ok) {
176-
assert.equal(removedBeforeArchive.code, 'must_archive_first');
178+
assert.equal(removedBeforeArchive.error.code, 'must_archive_first');
177179
}
178180

179181
const archived = await ipc.invoke<
@@ -194,14 +196,14 @@ describe('Work Board IPC', () => {
194196
{ titel: 'x' },
195197
);
196198
assert.equal(invalidPatch.ok, false);
197-
if (!invalidPatch.ok) assert.equal(invalidPatch.code, 'invalid_input');
199+
if (!invalidPatch.ok) assert.equal(invalidPatch.error.code, 'invalid_input');
198200

199201
const invalidCreate = await ipc.invoke<WorkBoardIpcResult<unknown>>(
200202
'workBoard:create',
201203
{ ...itemInput(), notes: null },
202204
);
203205
assert.equal(invalidCreate.ok, false);
204-
if (!invalidCreate.ok) assert.equal(invalidCreate.code, 'invalid_input');
206+
if (!invalidCreate.ok) assert.equal(invalidCreate.error.code, 'invalid_input');
205207

206208
await ipc.invoke('workBoard:archive', id);
207209
const removed = await ipc.invoke<WorkBoardIpcResult<null>>('workBoard:remove', id);

apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,30 +43,36 @@ import {
4343
type WorkHubDelegationExecutionState,
4444
type WorkHubSubmitInput,
4545
} from '../../renderer/workhub-controller.js';
46+
import { ExpectedOperationError } from '../../renderer/expected-operation-error.js';
47+
import { WorkHubCoordinationFailure } from '../../renderer/workhub-coordination-port.js';
4648
import { WorkHubSendLease } from '../../renderer/workhub-send-lease.js';
4749
import {
4850
createDesktopWorkHubSessionPort,
4951
type WorkHubDesktopSession,
5052
} from '../../renderer/workhub-session-port.js';
5153

52-
test('surface turns Action Gate rejections into safe actionable failures', () => {
54+
test('surface turns Action Gate rejections into safe actionable failures', (context) => {
55+
context.mock.method(console, 'error', () => undefined);
5356
assert.equal(
54-
workHubSurfaceFailure(
55-
new Error('WorkHub Session candidates changed; refresh before delegating'),
56-
),
57+
workHubSurfaceFailure(new ExpectedOperationError('candidates_changed')),
5758
'candidates_changed',
5859
);
5960
assert.equal(
60-
workHubSurfaceFailure(
61-
new Error('WorkHub linked correction requires an active durable delegation'),
62-
),
61+
workHubSurfaceFailure(new ExpectedOperationError('linked_correction_unavailable')),
6362
'linked_correction_unavailable',
6463
);
6564
assert.equal(
66-
workHubSurfaceFailure(new Error('Target Session is waiting for user input')),
65+
workHubSurfaceFailure(new WorkHubCoordinationFailure('session_busy', 'Host diagnostic')),
6766
'target_waiting',
6867
);
69-
assert.equal(workHubSurfaceFailure(new Error('private transport detail')), 'delivery_failed');
68+
assert.equal(
69+
workHubSurfaceFailure(new WorkHubCoordinationFailure('operation_conflict', 'Host diagnostic')),
70+
'action_changed',
71+
);
72+
assert.equal(
73+
workHubSurfaceFailure(new Error('WorkHub Session candidates changed; private detail')),
74+
'delivery_failed',
75+
);
7076
});
7177

7278
test('surface route gate rejects same-frame duplicate operations and reopens after settle', async () => {

0 commit comments

Comments
 (0)