Files
zn-ai/tests/browser-shortcut.test.ts
duanshuwen df600272d6 feat: add tool status management and localization for skill installation
- Updated chat message types to include tool statuses.
- Enhanced localization files for English, Thai, and Chinese to support new tool status messages.
- Modified HomePage and SkillsPage components to handle tool statuses in chat messages.
- Implemented tool status merging and updating logic in the chat store.
- Added handling for tool status events in the gateway event processing.
- Created tests for chat message rendering with tool statuses and skill installation shortcuts.
- Improved gateway event dispatching for tool lifecycle events.
2026-04-23 20:27:54 +08:00

207 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @vitest-environment node
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
appendMessage: vi.fn(),
setActiveRun: vi.fn(),
clearActiveRun: vi.fn(),
extractBrowserOpenIntent: vi.fn(),
openUrlInBrowser: vi.fn(),
appendTranscriptLine: vi.fn(),
logger: {
error: vi.fn(),
},
}));
vi.mock('../electron/gateway/session-store', () => ({
sessionStore: {
appendMessage: mocks.appendMessage,
setActiveRun: mocks.setActiveRun,
clearActiveRun: mocks.clearActiveRun,
},
}));
vi.mock('@electron/service/browser-open-service', () => ({
extractBrowserOpenIntent: mocks.extractBrowserOpenIntent,
openUrlInBrowser: mocks.openUrlInBrowser,
}));
vi.mock('@electron/utils/token-usage-writer', () => ({
appendTranscriptLine: mocks.appendTranscriptLine,
}));
vi.mock('@electron/service/logger', () => ({
default: mocks.logger,
}));
function flushAsyncTasks(): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
describe('gateway browser shortcut', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
mocks.extractBrowserOpenIntent.mockImplementation((text: string) => (
text === '打开 http://www.baidu.com'
? { url: 'http://www.baidu.com/' }
: null
));
});
it('starts a browser-open run and emits a final assistant message on success', async () => {
mocks.openUrlInBrowser.mockResolvedValue({
url: 'http://www.baidu.com/',
pageUrl: 'http://www.baidu.com/',
title: '百度一下,你就知道',
});
const { maybeHandleBrowserOpenMessage } = await import('../electron/gateway/browser-shortcut');
const broadcast = vi.fn();
const handled = maybeHandleBrowserOpenMessage(
'agent:test:main',
'run-1',
{ role: 'user', content: '打开 http://www.baidu.com' },
broadcast,
);
expect(handled).toBe(true);
expect(mocks.setActiveRun).toHaveBeenCalledWith(
'agent:test:main',
'run-1',
expect.any(AbortController),
);
await flushAsyncTasks();
expect(mocks.openUrlInBrowser).toHaveBeenCalledWith(
'http://www.baidu.com/',
expect.objectContaining({ signal: expect.any(AbortSignal) }),
);
expect(broadcast).toHaveBeenNthCalledWith(1, expect.objectContaining({
type: 'tool:status',
sessionKey: 'agent:test:main',
runId: 'run-1',
toolName: 'browser.open_url',
status: 'running',
}));
expect(broadcast).toHaveBeenNthCalledWith(2, expect.objectContaining({
type: 'tool:status',
sessionKey: 'agent:test:main',
runId: 'run-1',
toolName: 'browser.open_url',
status: 'completed',
}));
expect(mocks.appendMessage).toHaveBeenCalledWith(
'agent:test:main',
expect.objectContaining({
role: 'assistant',
content: '已为你打开 http://www.baidu.com/(百度一下,你就知道)',
_toolStatuses: [
expect.objectContaining({
name: 'browser.open_url',
status: 'completed',
input: { url: 'http://www.baidu.com/' },
result: expect.objectContaining({
pageUrl: 'http://www.baidu.com/',
title: '百度一下,你就知道',
}),
}),
],
}),
);
expect(broadcast).toHaveBeenNthCalledWith(3, expect.objectContaining({
type: 'chat:final',
sessionKey: 'agent:test:main',
runId: 'run-1',
message: expect.objectContaining({
_toolStatuses: [
expect.objectContaining({
name: 'browser.open_url',
status: 'completed',
}),
],
}),
}));
});
it('emits a failure assistant message when browser open fails', async () => {
mocks.openUrlInBrowser.mockRejectedValue(new Error('No browser context available'));
const { maybeHandleBrowserOpenMessage } = await import('../electron/gateway/browser-shortcut');
const broadcast = vi.fn();
const handled = maybeHandleBrowserOpenMessage(
'agent:test:main',
'run-2',
{ role: 'user', content: '打开 http://www.baidu.com' },
broadcast,
);
expect(handled).toBe(true);
await flushAsyncTasks();
expect(broadcast).toHaveBeenNthCalledWith(1, expect.objectContaining({
type: 'tool:status',
runId: 'run-2',
toolName: 'browser.open_url',
status: 'running',
}));
expect(broadcast).toHaveBeenNthCalledWith(2, expect.objectContaining({
type: 'tool:status',
runId: 'run-2',
toolName: 'browser.open_url',
status: 'error',
}));
expect(mocks.appendMessage).toHaveBeenCalledWith(
'agent:test:main',
expect.objectContaining({
role: 'assistant',
content: '打开失败No browser context available',
_toolStatuses: [
expect.objectContaining({
name: 'browser.open_url',
status: 'error',
result: {
error: 'No browser context available',
},
}),
],
}),
);
expect(broadcast).toHaveBeenNthCalledWith(3, expect.objectContaining({
type: 'chat:final',
runId: 'run-2',
message: expect.objectContaining({
_toolStatuses: [
expect.objectContaining({
name: 'browser.open_url',
status: 'error',
}),
],
}),
}));
});
it('returns false when the user message is not an explicit browser-open command', async () => {
mocks.extractBrowserOpenIntent.mockReturnValue(null);
const { maybeHandleBrowserOpenMessage } = await import('../electron/gateway/browser-shortcut');
expect(
maybeHandleBrowserOpenMessage(
'agent:test:main',
'run-3',
{ role: 'user', content: '帮我总结一下百度首页' },
vi.fn(),
),
).toBe(false);
expect(mocks.openUrlInBrowser).not.toHaveBeenCalled();
});
});