Skip to content
Draft
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
35 changes: 31 additions & 4 deletions apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@
"src/renderer/locales/settings-web-search-copy.ts",
"src/renderer/locales/shell-copy.ts",
"src/renderer/locales/shell-remaining-copy.ts",
"src/renderer/locales/work-board-error-copy.ts",
"src/renderer/locales/workhub-copy.ts",
"src/renderer/main.tsx",
"src/renderer/mcp-brand-contrast.ts",
"src/renderer/mcp-brand-marks.tsx",
Expand Down Expand Up @@ -444,8 +446,8 @@
"@maka/core/text-file-import": 1,
"@maka/core/ui-locale": 1
},
"importSpecifiers": 6,
"nonTriviaTokens": 515
"importSpecifiers": 5,
"nonTriviaTokens": 465
},
"src/renderer/app-shell-detail-panel.tsx": {
"importDeclarations": 1,
Expand Down Expand Up @@ -979,7 +981,7 @@
"react": 1
},
"importSpecifiers": 180,
"nonTriviaTokens": 15620
"nonTriviaTokens": 15616
},
"src/renderer/use-app-shell-composer-quotes.ts": {
"importDeclarations": 3,
Expand Down Expand Up @@ -1980,6 +1982,29 @@
"@maka/core/ui-locale": 1
}
},
"src/renderer/locales/work-board-error-copy.ts": {
"bridgePaths": {},
"environmentCapabilities": {},
"hookCalls": {},
"lifecycleMethods": {},
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {
"../../shared/work-board-ipc.js": 1,
"@maka/core/ui-locale": 1
}
},
"src/renderer/locales/workhub-copy.ts": {
"bridgePaths": {},
"environmentCapabilities": {},
"hookCalls": {},
"lifecycleMethods": {},
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {
"@maka/core/ui-locale": 1
}
},
"src/renderer/mcp-brand-contrast.ts": {
"bridgePaths": {},
"environmentCapabilities": {},
Expand Down Expand Up @@ -4937,8 +4962,8 @@
"actionFactories": [],
"dependencyPaths": {
"../shared/work-board-ipc.js": 1,
"./application/contracts/operation-diagnostics.js": 1,
"@maka/core/work-board": 1,
"@maka/storage/work-board-store": 1,
"react": 1
}
},
Expand All @@ -4957,6 +4982,7 @@
"actionFactories": [],
"dependencyPaths": {
"./locales/conversation-copy.js": 1,
"./locales/work-board-error-copy.js": 1,
"./use-work-board.js": 1,
"@astryxdesign/core": 1,
"@astryxdesign/core/Button": 1,
Expand Down Expand Up @@ -5072,6 +5098,7 @@
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {
"./locales/workhub-copy.js": 2,
"./workhub-controller.js": 1,
"./workhub-coordination-port.js": 1,
"./workhub-send-lease.js": 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { encodeIngestItems } from '../../preload/attachment-ingest-payload.js';
describe('encodeIngestItems', () => {
test('rejects more than 8 items without reading any file bytes', async () => {
const items = Array.from({ length: 9 }, (_, i) => ({ approvalId: `a${i}`, name: `f${i}.txt` }));
await assert.rejects(encodeIngestItems(items as never), /8/);
await assert.rejects(encodeIngestItems(items as never), /attachment_ingest:count_exceeded/);
});

test('rejects a File over 50MB without calling arrayBuffer', async () => {
Expand All @@ -38,7 +38,7 @@ describe('encodeIngestItems', () => {
return new ArrayBuffer(0);
},
} as unknown as File;
await assert.rejects(encodeIngestItems([{ file: bigFile }]), /50/);
await assert.rejects(encodeIngestItems([{ file: bigFile }]), /attachment_ingest:size_exceeded/);
assert.equal(arrayBufferCalls, 0, 'arrayBuffer must not be called for an oversized file');
});

Expand Down Expand Up @@ -67,7 +67,7 @@ describe('encodeIngestItems', () => {
test('rejects a raw base64 item that is neither a File nor an approval token', async () => {
await assert.rejects(
encodeIngestItems([{ name: 'forged', base64: 'AAAA' }] as never),
/无效/,
/attachment_ingest:payload_invalid/,
);
});
});
22 changes: 11 additions & 11 deletions apps/desktop/src/main/__tests__/attachment-ingest-resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
approvals,
stat: async () => (statCalls++, { size: 1 }),
}),
/最多/,
/attachment_ingest:count_limit/,
);
assert.equal(statCalls, 0);
});
Expand All @@ -60,7 +60,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
approvals,
stat: async () => (statCalls++, { size: 1 }),
}),
/过期|无效/,
/attachment_ingest:(source_expired|items_invalid)/,
);
assert.equal(statCalls, 0);
});
Expand All @@ -77,7 +77,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
approvals,
stat: async () => (statCalls++, { size: 1 }),
}),
/过期|无效/,
/attachment_ingest:(source_expired|items_invalid)/,
);
assert.equal(statCalls, 0);
});
Expand All @@ -95,7 +95,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
stat: async () => (statCalls++, { size: 200 }),
maxBytes: 100,
}),
/超出大小限制/,
/attachment_ingest:item_too_large/,
);
assert.equal(statCalls, 1);
});
Expand All @@ -120,7 +120,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
stat: async () => (statCalls++, { size: 1 }),
maxBytes: 100,
}),
/超出大小限制/,
/attachment_ingest:item_too_large/,
);
assert.equal(statCalls, 0);
assert.equal(decodeCalls, 0, 'must reject by base64 string length before Buffer.from');
Expand Down Expand Up @@ -148,7 +148,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
approvals,
stat: async () => ({ size: 10 }),
}),
/过期|无效/,
/attachment_ingest:(source_expired|items_invalid)/,
);
});

Expand Down Expand Up @@ -184,7 +184,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
approvals,
stat: async () => (statCalls++, { size: 10 }),
}),
/无效/,
/attachment_ingest:items_invalid/,
);
assert.notEqual(
approvals.consumeApproval(1, issued.approvalId),
Expand All @@ -207,7 +207,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
approvals,
stat: async () => (statCalls++, { size: 10 }),
}),
/重复/,
/attachment_ingest:duplicate_source/,
);
assert.notEqual(
approvals.consumeApproval(1, issued.approvalId),
Expand All @@ -226,7 +226,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
approvals,
stat: async () => ({ size: 1 }),
}),
/无效/,
/attachment_ingest:items_invalid/,
);
await assert.rejects(
() =>
Expand All @@ -236,7 +236,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
approvals,
stat: async () => ({ size: 1 }),
}),
/无效/,
/attachment_ingest:items_invalid/,
);
});
});
Expand Down Expand Up @@ -420,7 +420,7 @@ describe('resolveAttachmentRefs', () => {
throw new Error('snapshot must not run');
},
}),
/超出大小限制/,
/attachment_ingest:item_too_large/,
);
assert.equal(snapshots, 0);
} finally {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* 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 { getDesktopConversationCopy } from '../../renderer/locales/conversation-copy.js';
import { getWorkBoardErrorCopy } from '../../renderer/locales/work-board-error-copy.js';
import { getSessionCollaborationCopy } from '../../renderer/locales/session-collaboration-copy.js';
import { ExpectedOperationError } from '../../renderer/application/contracts/operation-diagnostics.js';
import { sessionCollaborationImportErrorMessage } from '../../renderer/features/session-collaboration/testing.js';
import { messageReadErrorMessage } from '../../renderer/app-shell-copy.js';
import { localizedShellErrorMessage } from '../../renderer/locales/shell-copy.js';

test('routes Work Board codes through each locale catalog', () => {
for (const locale of ['zh', 'en'] as const) {
const copy = getDesktopConversationCopy(locale).workBoardPanel;
const errorCopy = getWorkBoardErrorCopy(locale);
const error: unknown = new ExpectedOperationError<'not_found'>('not_found');
assert.equal(
error instanceof ExpectedOperationError && Object.hasOwn(errorCopy, error.code)
? errorCopy[error.code as keyof typeof errorCopy]
: copy.actionFailed,
errorCopy.not_found,
);
}
});

test('maps blocked session-control tokens per locale at the shared entry', () => {
const blocked = new Error('session_control_blocked:permission_turn_running');
assert.equal(
localizedShellErrorMessage(blocked, 'fallback', 'zh'),
'当前任务正在运行,等结束后再切换权限模式。',
);
assert.equal(
localizedShellErrorMessage(blocked, 'fallback', 'en'),
'A task is still running. Change the permission mode after it finishes.',
);
});

test('maps attachment-ingest tokens per locale at the shared entry', () => {
const blocked = new Error("Error invoking remote method 'attachments': Error: attachment_ingest:count_limit");
assert.equal(localizedShellErrorMessage(blocked, 'fallback', 'zh'), '一次最多添加 8 个附件。');
assert.equal(
localizedShellErrorMessage(blocked, 'fallback', 'en'),
'At most 8 attachments per message.',
);
});

test('routes structured collaboration failures through each locale catalog', () => {
const cases = [
[{ kind: 'error', reason: 'invalid_code' } as const, 'invalidCode'],
[{ kind: 'error', reason: 'peer_path_unavailable' } as const, 'directPathUnavailable'],
[{ kind: 'error', reason: 'connection_failed' } as const, 'connectionFailed'],
] as const;
for (const locale of ['zh', 'en'] as const) {
const copy = getSessionCollaborationCopy(locale);
for (const [result, key] of cases) {
assert.equal(sessionCollaborationImportErrorMessage(copy, result), copy[key]);
}
assert.equal(
sessionCollaborationImportErrorMessage(copy, {
kind: 'error',
reason: 'mount_limit_reached',
params: { max: 12 },
}),
copy.mountLimit(12),
);
}
});

test('uses localized fallbacks instead of classifying raw exception text', (context) => {
context.mock.method(console, 'error', () => undefined);
const raw = new Error('timeout 401 网络失败 MAKA_SESSION_READ_MESSAGES_ERROR: 后端中文');
assert.equal(
messageReadErrorMessage(raw, 'en'),
'Task content is temporarily unavailable. Try again later.',
);
assert.equal(messageReadErrorMessage(raw, 'zh'), '任务内容暂时无法读取,请稍后重试。');
assert.equal(localizedShellErrorMessage(raw, 'English fallback', 'en'), 'English fallback');
assert.equal(localizedShellErrorMessage(raw, '中文兜底', 'zh'), '中文兜底');
});
2 changes: 2 additions & 0 deletions apps/desktop/src/main/__tests__/plan-mode-copy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ test('localizes Plan Mode chrome and abandon confirmation without rewriting plan
assert.equal(en.proposal.statuses.approved, 'Approved');
assert.equal(zh.execution.stepCount(2, 3), '2/3 步');
assert.equal(en.execution.stepCount(1, 1), '1/1 step');
assert.equal(zh.operationFailed, '计划操作失败,请稍后重试。');
assert.equal(en.operationFailed, 'The plan action failed. Try again later.');
assert.deepEqual(
{
title: en.abandonConfirmation.title,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ test('removes failed activation desire instead of creating recoverable profile s
});

const result = await mounts.importInvitation(invitation('guest-two'), false, 'import-two');
assert.deepEqual(result.kind === 'error' ? result.reason : result.kind, 'peer_path_unavailable');
assert.deepEqual(result, { kind: 'error', reason: 'peer_path_unavailable' });
assert.deepEqual(await store.read(), []);
assert.equal(unmounted.length, 1);
});
Expand Down Expand Up @@ -672,6 +672,25 @@ test('does not lose a catalog invalidation that races Guest activation', async (
await mounts.close();
});

test('logs unexpected activation failures without returning Host details', async (context) => {
const diagnostics: string[] = [];
context.mock.method(console, 'error', (...values: unknown[]) => {
diagnostics.push(values.map(String).join(' '));
});
const mounts = service(memoryStore(), {
mount: async () => {
throw new Error('Authorization: Bearer very-secret-token');
},
});

const result = await mounts.importInvitation(invitation('guest-failed'), false, 'import-failed');

assert.deepEqual(result, { kind: 'error', reason: 'connection_failed' });
assert.equal(diagnostics.length, 1);
assert.match(diagnostics[0]!, /session-collaboration.*import failed/u);
assert.doesNotMatch(diagnostics[0]!, /very-secret-token/u);
});

test('settles admitted finalization before committing unmount desire', async () => {
const store = memoryStore();
let started!: () => void;
Expand Down
12 changes: 7 additions & 5 deletions apps/desktop/src/main/__tests__/work-board-ipc-main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,16 +164,18 @@ describe('Work Board IPC', () => {
{ title: 'stale write' },
{ expectedRevision: 1 },
);
assert.equal(staleRename.ok, false);
if (!staleRename.ok) assert.equal(staleRename.code, 'operation_conflict');
assert.deepEqual(staleRename, {
ok: false,
error: { code: 'operation_conflict' },
});

const removedBeforeArchive = await ipc.invoke<WorkBoardIpcResult<null>>(
'workBoard:remove',
id,
);
assert.equal(removedBeforeArchive.ok, false);
if (!removedBeforeArchive.ok) {
assert.equal(removedBeforeArchive.code, 'must_archive_first');
assert.equal(removedBeforeArchive.error.code, 'must_archive_first');
}

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

const invalidCreate = await ipc.invoke<WorkBoardIpcResult<unknown>>(
'workBoard:create',
{ ...itemInput(), notes: null },
);
assert.equal(invalidCreate.ok, false);
if (!invalidCreate.ok) assert.equal(invalidCreate.code, 'invalid_input');
if (!invalidCreate.ok) assert.equal(invalidCreate.error.code, 'invalid_input');

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