@lobehub/chat 0.108.0 → 0.109.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/docs/Environment-Variable.md +2 -2
  5. package/docs/Environment-Variable.zh-CN.md +2 -2
  6. package/locales/en_US/plugin.json +19 -0
  7. package/locales/en_US/setting.json +26 -0
  8. package/locales/ja_JP/plugin.json +19 -0
  9. package/locales/ja_JP/setting.json +26 -0
  10. package/locales/ko_KR/plugin.json +19 -0
  11. package/locales/ko_KR/setting.json +26 -0
  12. package/locales/ru_RU/plugin.json +19 -0
  13. package/locales/ru_RU/setting.json +26 -0
  14. package/locales/zh_CN/plugin.json +19 -0
  15. package/locales/zh_CN/setting.json +26 -5
  16. package/locales/zh_TW/plugin.json +19 -0
  17. package/locales/zh_TW/setting.json +26 -0
  18. package/package.json +3 -3
  19. package/src/app/chat/features/ChatHeader/PluginTag/PluginStatus.tsx +6 -6
  20. package/src/app/chat/features/ChatHeader/PluginTag/index.tsx +4 -4
  21. package/src/app/chat/features/ChatInput/ActionBar/Tools/ToolItem.tsx +37 -0
  22. package/src/app/chat/features/ChatInput/ActionBar/Tools/index.tsx +116 -0
  23. package/src/app/chat/features/ChatInput/ActionBar/config.ts +3 -1
  24. package/src/app/chat/features/Conversation/ChatList/Error/Plugin/PluginSettings.tsx +4 -4
  25. package/src/app/chat/features/Conversation/ChatList/Plugins/Inspector/Settings.tsx +3 -3
  26. package/src/app/chat/features/Conversation/ChatList/Plugins/Inspector/index.tsx +4 -4
  27. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/DefaultType/index.tsx +2 -2
  28. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/StandaloneType/Iframe.tsx +4 -4
  29. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/StandaloneType/index.tsx +2 -2
  30. package/src/app/chat/features/Conversation/useInitConversation.ts +2 -2
  31. package/src/app/settings/common/Common.tsx +2 -2
  32. package/src/features/AgentSetting/AgentPlugin/AddPluginButton.tsx +56 -0
  33. package/src/features/AgentSetting/AgentPlugin/LoadingList.tsx +48 -0
  34. package/src/features/AgentSetting/AgentPlugin/LocalPluginItem.tsx +5 -5
  35. package/src/features/AgentSetting/AgentPlugin/PluginAction/index.tsx +36 -0
  36. package/src/features/AgentSetting/AgentPlugin/index.tsx +157 -9
  37. package/src/features/AgentSetting/store/action.ts +0 -1
  38. package/src/features/PluginDevModal/MetaForm.tsx +3 -3
  39. package/src/features/PluginDevModal/PluginPreview.tsx +1 -1
  40. package/src/features/PluginDevModal/index.tsx +4 -1
  41. package/src/features/PluginSettings/index.tsx +3 -3
  42. package/src/features/PluginSettingsModal/index.tsx +22 -17
  43. package/src/features/PluginStore/Loading.tsx +13 -0
  44. package/src/features/PluginStore/PluginItem.tsx +81 -0
  45. package/src/features/PluginStore/PluginSettings.tsx +40 -0
  46. package/src/features/{AgentSetting/AgentPlugin/MarketSettingModal.tsx → PluginStore/StoreSetting.tsx} +2 -2
  47. package/src/features/PluginStore/index.tsx +95 -0
  48. package/src/layout/GlobalLayout/StoreHydration.tsx +3 -3
  49. package/src/locales/default/plugin.ts +19 -0
  50. package/src/locales/default/setting.ts +26 -5
  51. package/src/services/__tests__/chat.test.ts +221 -0
  52. package/src/services/__tests__/message.test.ts +333 -0
  53. package/src/services/__tests__/plugin.test.ts +88 -0
  54. package/src/services/__tests__/session.test.ts +227 -0
  55. package/src/services/__tests__/topic.test.ts +220 -0
  56. package/src/services/chat.ts +4 -6
  57. package/src/services/plugin.ts +12 -1
  58. package/src/store/chat/actions/message.test.ts +0 -1
  59. package/src/store/session/slices/agent/action.ts +24 -8
  60. package/src/store/{plugin → tool}/helpers.ts +2 -13
  61. package/src/store/tool/index.ts +2 -0
  62. package/src/store/tool/initialState.ts +11 -0
  63. package/src/store/tool/selectors.ts +3 -0
  64. package/src/store/tool/slices/customPlugin/action.test.ts +164 -0
  65. package/src/store/{plugin → tool}/slices/customPlugin/action.ts +2 -2
  66. package/src/store/tool/slices/customPlugin/selectors.test.ts +59 -0
  67. package/src/store/tool/slices/customPlugin/selectors.ts +9 -0
  68. package/src/store/tool/slices/plugin/action.test.ts +141 -0
  69. package/src/store/tool/slices/plugin/action.ts +95 -0
  70. package/src/store/{plugin → tool}/slices/plugin/initialState.ts +0 -7
  71. package/src/store/{plugin → tool/slices/plugin}/selectors.test.ts +4 -41
  72. package/src/store/{plugin → tool/slices/plugin}/selectors.ts +17 -19
  73. package/src/store/tool/slices/store/action.test.ts +396 -0
  74. package/src/store/tool/slices/store/action.ts +103 -0
  75. package/src/store/tool/slices/store/index.ts +3 -0
  76. package/src/store/tool/slices/store/initialState.ts +17 -0
  77. package/src/store/tool/slices/store/selectors.test.ts +48 -0
  78. package/src/store/tool/slices/store/selectors.ts +21 -0
  79. package/src/store/{plugin → tool}/store.ts +8 -6
  80. package/vitest.config.ts +1 -0
  81. package/src/features/AgentSetting/AgentPlugin/MarketList.tsx +0 -232
  82. package/src/features/AgentSetting/AgentPlugin/PluginSettings.tsx +0 -80
  83. package/src/store/chat/reducers/files.test.ts +0 -38
  84. package/src/store/chat/reducers/files.ts +0 -37
  85. package/src/store/plugin/hooks/useSessionHydrated.ts +0 -21
  86. package/src/store/plugin/index.ts +0 -2
  87. package/src/store/plugin/initialState.ts +0 -9
  88. package/src/store/plugin/slices/plugin/action.ts +0 -173
  89. /package/src/store/{plugin → tool}/slices/customPlugin/index.ts +0 -0
  90. /package/src/store/{plugin → tool}/slices/customPlugin/initialState.ts +0 -0
  91. /package/src/store/{plugin → tool}/slices/customPlugin/reducers/customPluginList.ts +0 -0
  92. /package/src/store/{plugin → tool}/slices/plugin/index.ts +0 -0
  93. /package/src/store/{plugin → tool}/slices/plugin/reducers/manifest.ts +0 -0
@@ -0,0 +1,221 @@
1
+ import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
2
+ import { act } from '@testing-library/react';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { VISION_MODEL_WHITE_LIST } from '@/const/llm';
6
+ import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
7
+ import { useFileStore } from '@/store/file';
8
+ import { useToolStore } from '@/store/tool';
9
+ import { ChatMessage } from '@/types/chatMessage';
10
+ import { OpenAIChatStreamPayload } from '@/types/openai/chat';
11
+ import { fetchAIFactory } from '@/utils/fetch';
12
+
13
+ import { chatService } from '../chat';
14
+
15
+ // Mocking external dependencies
16
+ vi.stubGlobal(
17
+ 'fetch',
18
+ vi.fn(() => Promise.resolve(new Response(JSON.stringify({ some: 'data' })))),
19
+ ); // 用你的模拟响应替换这里的内容
20
+
21
+ vi.mock('@/utils/fetch', () => ({
22
+ fetchAIFactory: vi.fn(),
23
+ getMessageError: vi.fn(),
24
+ }));
25
+
26
+ describe('ChatService', () => {
27
+ describe('createAssistantMessage', () => {
28
+ it('should process messages and call getChatCompletion with the right parameters', async () => {
29
+ const getChatCompletionSpy = vi.spyOn(chatService, 'getChatCompletion');
30
+ const messages = [{ content: 'Hello', role: 'user' }] as ChatMessage[];
31
+ const enabledPlugins = ['plugin1'];
32
+ await act(async () => {
33
+ useToolStore.setState({
34
+ pluginManifestMap: {
35
+ plugin1: {
36
+ identifier: 'plugin1',
37
+ api: [{ name: 'api1' }],
38
+ type: 'default',
39
+ } as LobeChatPluginManifest,
40
+ plugin2: {
41
+ identifier: 'plugin2',
42
+ api: [{ name: 'api2' }],
43
+ type: 'standalone',
44
+ } as LobeChatPluginManifest,
45
+ },
46
+ });
47
+ });
48
+ await chatService.createAssistantMessage({ messages, plugins: enabledPlugins });
49
+
50
+ expect(getChatCompletionSpy).toHaveBeenCalledWith(
51
+ expect.objectContaining({
52
+ functions: expect.arrayContaining([{ name: 'plugin1____api1____default' }]),
53
+ messages: expect.anything(),
54
+ }),
55
+ undefined,
56
+ );
57
+ });
58
+
59
+ it('should not use tools for models in the vision model whitelist', async () => {
60
+ const getChatCompletionSpy = vi.spyOn(chatService, 'getChatCompletion');
61
+ const messages = [{ content: 'Hello', role: 'user' }] as ChatMessage[];
62
+ const modelInWhitelist = VISION_MODEL_WHITE_LIST[0];
63
+
64
+ await chatService.createAssistantMessage({
65
+ messages,
66
+ model: modelInWhitelist,
67
+ plugins: ['plugin1'],
68
+ });
69
+
70
+ expect(getChatCompletionSpy).toHaveBeenCalledWith(
71
+ expect.objectContaining({
72
+ functions: undefined,
73
+ model: modelInWhitelist,
74
+ }),
75
+ undefined,
76
+ );
77
+ });
78
+
79
+ // New test case for processMessages
80
+ it('should correctly process messages and handle content for vision models', async () => {
81
+ const messages = [
82
+ { content: 'Hello', role: 'user', files: ['file1'] }, // Message with files
83
+ { content: 'Hi', role: 'function', plugin: { identifier: 'plugin1' } }, // Message with function role
84
+ { content: 'Hey', role: 'assistant' }, // Regular user message
85
+ ] as ChatMessage[];
86
+
87
+ // Mock file store state to return a specific image URL or Base64 for the given files
88
+ act(() => {
89
+ useFileStore.setState({
90
+ imagesMap: {
91
+ file1: {
92
+ name: 'abc.png',
93
+ saveMode: 'url',
94
+ fileType: 'image/png',
95
+ url: 'http://example.com/image.jpg',
96
+ },
97
+ },
98
+ });
99
+ });
100
+
101
+ const getChatCompletionSpy = vi.spyOn(chatService, 'getChatCompletion');
102
+ await chatService.createAssistantMessage({ messages, plugins: [] });
103
+
104
+ expect(getChatCompletionSpy).toHaveBeenCalledWith(
105
+ {
106
+ messages: [
107
+ {
108
+ content: [
109
+ { text: 'Hello', type: 'text' },
110
+ {
111
+ image_url: { detail: 'auto', url: 'http://example.com/image.jpg' },
112
+ type: 'image_url',
113
+ },
114
+ ],
115
+ role: 'user',
116
+ },
117
+ {
118
+ content: 'Hi',
119
+ name: 'plugin1',
120
+ role: 'function',
121
+ },
122
+ {
123
+ content: 'Hey',
124
+ role: 'assistant',
125
+ },
126
+ ],
127
+ },
128
+ undefined,
129
+ );
130
+ });
131
+
132
+ it('should correctly process messages and handle content for vision models', async () => {
133
+ const messages = [
134
+ { content: 'Hello', role: 'user', files: ['file2'] }, // Message with files
135
+ { content: 'Hi', role: 'function', plugin: { identifier: 'plugin1' } }, // Message with function role
136
+ { content: 'Hey', role: 'assistant' }, // Regular user message
137
+ ] as ChatMessage[];
138
+
139
+ // Mock file store state to return a specific image URL or Base64 for the given files
140
+ act(() => {
141
+ useFileStore.setState({
142
+ imagesMap: {
143
+ file1: {
144
+ name: 'abc.png',
145
+ saveMode: 'url',
146
+ fileType: 'image/png',
147
+ url: 'http://example.com/image.jpg',
148
+ },
149
+ },
150
+ });
151
+ });
152
+
153
+ const getChatCompletionSpy = vi.spyOn(chatService, 'getChatCompletion');
154
+ await chatService.createAssistantMessage({ messages, plugins: [] });
155
+
156
+ expect(getChatCompletionSpy).toHaveBeenCalledWith(
157
+ {
158
+ messages: [
159
+ {
160
+ content: 'Hello',
161
+ role: 'user',
162
+ },
163
+ {
164
+ content: 'Hi',
165
+ name: 'plugin1',
166
+ role: 'function',
167
+ },
168
+ {
169
+ content: 'Hey',
170
+ role: 'assistant',
171
+ },
172
+ ],
173
+ },
174
+ undefined,
175
+ );
176
+ });
177
+ });
178
+ describe('getChatCompletion', () => {
179
+ it('should make a POST request with the correct payload', async () => {
180
+ const params: Partial<OpenAIChatStreamPayload> = {
181
+ model: 'test-model',
182
+ messages: [],
183
+ };
184
+ const options = {};
185
+ const expectedPayload = {
186
+ model: DEFAULT_AGENT_CONFIG.model,
187
+ stream: true,
188
+ ...DEFAULT_AGENT_CONFIG.params,
189
+ ...params,
190
+ };
191
+
192
+ await chatService.getChatCompletion(params, options);
193
+
194
+ expect(global.fetch).toHaveBeenCalledWith(expect.any(String), {
195
+ body: JSON.stringify(expectedPayload),
196
+ headers: expect.any(Object),
197
+ method: 'POST',
198
+ signal: undefined,
199
+ });
200
+ });
201
+
202
+ // Add more test cases to cover different scenarios and edge cases
203
+ });
204
+
205
+ describe('runPluginApi', () => {
206
+ it('should make a POST request and return the result text', async () => {
207
+ const params = { identifier: 'test-plugin', apiName: '1' }; // Add more properties if needed
208
+ const options = {};
209
+ const mockResponse = new Response('Plugin Result', { status: 200 });
210
+
211
+ global.fetch = vi.fn(() => Promise.resolve(mockResponse));
212
+
213
+ const result = await chatService.runPluginApi(params, options);
214
+
215
+ expect(global.fetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object));
216
+ expect(result).toBe('Plugin Result');
217
+ });
218
+
219
+ // Add more test cases to cover different scenarios and edge cases
220
+ });
221
+ });
@@ -0,0 +1,333 @@
1
+ import { Mock, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { CreateMessageParams, MessageModel } from '@/database/models/message';
4
+ import {
5
+ ChatMessage,
6
+ ChatMessageError,
7
+ ChatPluginPayload,
8
+ ChatTTS,
9
+ ChatTranslate,
10
+ } from '@/types/chatMessage';
11
+
12
+ import { messageService } from '../message';
13
+
14
+ // Mock the MessageModel
15
+ vi.mock('@/database/models/message', () => {
16
+ return {
17
+ MessageModel: {
18
+ create: vi.fn(),
19
+ batchCreate: vi.fn(),
20
+ isEmpty: vi.fn(),
21
+ query: vi.fn(),
22
+ delete: vi.fn(),
23
+ queryBySessionId: vi.fn(),
24
+ update: vi.fn(),
25
+ batchDelete: vi.fn(),
26
+ clearTable: vi.fn(),
27
+ batchUpdate: vi.fn(),
28
+ queryAll: vi.fn(),
29
+ },
30
+ };
31
+ });
32
+
33
+ describe('MessageService', () => {
34
+ // Mock data
35
+ const mockMessageId = 'mock-message-id';
36
+ const mockMessage = {
37
+ id: mockMessageId,
38
+ content: 'Mock message content',
39
+ sessionId: 'mock-session-id',
40
+ createdAt: 100,
41
+ updatedAt: 100,
42
+ role: 'user',
43
+ // ... other properties
44
+ } as ChatMessage;
45
+ const mockMessages = [mockMessage];
46
+
47
+ beforeEach(() => {
48
+ // Reset all mocks before running each test case
49
+ vi.resetAllMocks();
50
+ });
51
+
52
+ describe('create', () => {
53
+ it('should create a message and return its id', async () => {
54
+ // Setup
55
+ const createParams = {
56
+ content: 'New message content',
57
+ sessionId: '1',
58
+ // ... other properties
59
+ } as CreateMessageParams;
60
+ (MessageModel.create as Mock).mockResolvedValue({ id: mockMessageId });
61
+
62
+ // Execute
63
+ const messageId = await messageService.create(createParams);
64
+
65
+ // Assert
66
+ expect(MessageModel.create).toHaveBeenCalledWith(createParams);
67
+ expect(messageId).toBe(mockMessageId);
68
+ });
69
+ });
70
+
71
+ describe('batchCreate', () => {
72
+ it('should batch create messages', async () => {
73
+ // Setup
74
+ (MessageModel.batchCreate as Mock).mockResolvedValue(mockMessages);
75
+
76
+ // Execute
77
+ const result = await messageService.batchCreate(mockMessages);
78
+
79
+ // Assert
80
+ expect(MessageModel.batchCreate).toHaveBeenCalledWith(mockMessages);
81
+ expect(result).toBe(mockMessages);
82
+ });
83
+ });
84
+
85
+ describe('hasMessages', () => {
86
+ it('should return true if there are messages', async () => {
87
+ // Setup
88
+ (MessageModel.isEmpty as Mock).mockResolvedValue(false);
89
+
90
+ // Execute
91
+ const hasMessages = await messageService.hasMessages();
92
+
93
+ // Assert
94
+ expect(MessageModel.isEmpty).toHaveBeenCalled();
95
+ expect(hasMessages).toBe(true);
96
+ });
97
+
98
+ it('should return false if there are no messages', async () => {
99
+ // Setup
100
+ (MessageModel.isEmpty as Mock).mockResolvedValue(true);
101
+
102
+ // Execute
103
+ const hasMessages = await messageService.hasMessages();
104
+
105
+ // Assert
106
+ expect(MessageModel.isEmpty).toHaveBeenCalled();
107
+ expect(hasMessages).toBe(false);
108
+ });
109
+ });
110
+
111
+ describe('removeMessage', () => {
112
+ it('should remove a message by id', async () => {
113
+ // Setup
114
+ (MessageModel.delete as Mock).mockResolvedValue(true);
115
+
116
+ // Execute
117
+ const result = await messageService.removeMessage(mockMessageId);
118
+
119
+ // Assert
120
+ expect(MessageModel.delete).toHaveBeenCalledWith(mockMessageId);
121
+ expect(result).toBe(true);
122
+ });
123
+ });
124
+
125
+ describe('getMessages', () => {
126
+ it('should retrieve messages by sessionId and topicId', async () => {
127
+ // Setup
128
+ const sessionId = 'session-id';
129
+ const topicId = 'topic-id';
130
+ (MessageModel.query as Mock).mockResolvedValue(mockMessages);
131
+
132
+ // Execute
133
+ const messages = await messageService.getMessages(sessionId, topicId);
134
+
135
+ // Assert
136
+ expect(MessageModel.query).toHaveBeenCalledWith({ sessionId, topicId });
137
+ expect(messages).toBe(mockMessages);
138
+ });
139
+ });
140
+
141
+ describe('getAllMessagesInSession', () => {
142
+ it('should retrieve all messages in a session', async () => {
143
+ // Setup
144
+ const sessionId = 'session-id';
145
+ (MessageModel.queryBySessionId as Mock).mockResolvedValue(mockMessages);
146
+
147
+ // Execute
148
+ const messages = await messageService.getAllMessagesInSession(sessionId);
149
+
150
+ // Assert
151
+ expect(MessageModel.queryBySessionId).toHaveBeenCalledWith(sessionId);
152
+ expect(messages).toBe(mockMessages);
153
+ });
154
+ });
155
+
156
+ describe('updateMessageContent', () => {
157
+ it('should update the content of a message', async () => {
158
+ // Setup
159
+ const newContent = 'Updated message content';
160
+ (MessageModel.update as Mock).mockResolvedValue({ ...mockMessage, content: newContent });
161
+
162
+ // Execute
163
+ const result = await messageService.updateMessageContent(mockMessageId, newContent);
164
+
165
+ // Assert
166
+ expect(MessageModel.update).toHaveBeenCalledWith(mockMessageId, { content: newContent });
167
+ expect(result).toEqual({ ...mockMessage, content: newContent });
168
+ });
169
+ });
170
+
171
+ describe('removeMessages', () => {
172
+ it('should batch remove messages by assistantId and topicId', async () => {
173
+ // Setup
174
+ const assistantId = 'assistant-id';
175
+ const topicId = 'topic-id';
176
+ (MessageModel.batchDelete as Mock).mockResolvedValue(true);
177
+
178
+ // Execute
179
+ const result = await messageService.removeMessages(assistantId, topicId);
180
+
181
+ // Assert
182
+ expect(MessageModel.batchDelete).toHaveBeenCalledWith(assistantId, topicId);
183
+ expect(result).toBe(true);
184
+ });
185
+ });
186
+
187
+ describe('clearAllMessage', () => {
188
+ it('should clear all messages from the table', async () => {
189
+ // Setup
190
+ (MessageModel.clearTable as Mock).mockResolvedValue(true);
191
+
192
+ // Execute
193
+ const result = await messageService.clearAllMessage();
194
+
195
+ // Assert
196
+ expect(MessageModel.clearTable).toHaveBeenCalled();
197
+ expect(result).toBe(true);
198
+ });
199
+ });
200
+
201
+ describe('bindMessagesToTopic', () => {
202
+ it('should batch update messages to bind them to a topic', async () => {
203
+ // Setup
204
+ const topicId = 'topic-id';
205
+ const messageIds = [mockMessageId];
206
+ (MessageModel.batchUpdate as Mock).mockResolvedValue(mockMessages);
207
+
208
+ // Execute
209
+ const result = await messageService.bindMessagesToTopic(topicId, messageIds);
210
+
211
+ // Assert
212
+ expect(MessageModel.batchUpdate).toHaveBeenCalledWith(messageIds, { topicId });
213
+ expect(result).toBe(mockMessages);
214
+ });
215
+ });
216
+
217
+ describe('getAllMessages', () => {
218
+ it('should retrieve all messages', async () => {
219
+ // Setup
220
+ (MessageModel.queryAll as Mock).mockResolvedValue(mockMessages);
221
+
222
+ // Execute
223
+ const messages = await messageService.getAllMessages();
224
+
225
+ // Assert
226
+ expect(MessageModel.queryAll).toHaveBeenCalled();
227
+ expect(messages).toBe(mockMessages);
228
+ });
229
+ });
230
+
231
+ describe('updateMessageError', () => {
232
+ it('should update the error field of a message', async () => {
233
+ // Setup
234
+ const newError = { type: 'NoAPIKey', message: 'Error occurred' } as ChatMessageError;
235
+ (MessageModel.update as Mock).mockResolvedValue({ ...mockMessage, error: newError });
236
+
237
+ // Execute
238
+ const result = await messageService.updateMessageError(mockMessageId, newError);
239
+
240
+ // Assert
241
+ expect(MessageModel.update).toHaveBeenCalledWith(mockMessageId, { error: newError });
242
+ expect(result).toEqual({ ...mockMessage, error: newError });
243
+ });
244
+ });
245
+
246
+ describe('updateMessageTranslate', () => {
247
+ it('should update the translate field of a message', async () => {
248
+ // Setup
249
+ const newTranslate = { content: 'Translated text', to: 'es' } as ChatTranslate;
250
+ (MessageModel.update as Mock).mockResolvedValue({ ...mockMessage, translate: newTranslate });
251
+
252
+ // Execute
253
+ const result = await messageService.updateMessageTranslate(mockMessageId, newTranslate);
254
+
255
+ // Assert
256
+ expect(MessageModel.update).toHaveBeenCalledWith(mockMessageId, { translate: newTranslate });
257
+ expect(result).toEqual({ ...mockMessage, translate: newTranslate });
258
+ });
259
+ });
260
+
261
+ describe('updateMessageTTS', () => {
262
+ it('should update the tts field of a message', async () => {
263
+ // Setup
264
+ const newTTS = { init: false } as ChatTTS;
265
+ (MessageModel.update as Mock).mockResolvedValue({ ...mockMessage, tts: newTTS });
266
+
267
+ // Execute
268
+ const result = await messageService.updateMessageTTS(mockMessageId, newTTS);
269
+
270
+ // Assert
271
+ expect(MessageModel.update).toHaveBeenCalledWith(mockMessageId, { tts: newTTS });
272
+ expect(result).toEqual({ ...mockMessage, tts: newTTS });
273
+ });
274
+ });
275
+
276
+ describe('updateMessageRole', () => {
277
+ it('should update the role of a message', async () => {
278
+ // Setup
279
+ const newRole = 'user';
280
+ (MessageModel.update as Mock).mockResolvedValue({ ...mockMessage, role: newRole });
281
+
282
+ // Execute
283
+ const result = await messageService.updateMessageRole(mockMessageId, newRole);
284
+
285
+ // Assert
286
+ expect(MessageModel.update).toHaveBeenCalledWith(mockMessageId, { role: newRole });
287
+ expect(result).toEqual({ ...mockMessage, role: newRole });
288
+ });
289
+ });
290
+
291
+ describe('updateMessagePlugin', () => {
292
+ it('should update the plugin payload of a message', async () => {
293
+ // Setup
294
+ const newPlugin = {
295
+ type: 'default',
296
+ apiName: 'abc',
297
+ arguments: '',
298
+ identifier: 'plugin1',
299
+ } as ChatPluginPayload;
300
+
301
+ (MessageModel.update as Mock).mockResolvedValue({ ...mockMessage, plugin: newPlugin });
302
+
303
+ // Execute
304
+ const result = await messageService.updateMessagePlugin(mockMessageId, newPlugin);
305
+
306
+ // Assert
307
+ expect(MessageModel.update).toHaveBeenCalledWith(mockMessageId, { plugin: newPlugin });
308
+ expect(result).toEqual({ ...mockMessage, plugin: newPlugin });
309
+ });
310
+ });
311
+
312
+ describe('updateMessagePluginState', () => {
313
+ it('should update the plugin state of a message', async () => {
314
+ // Setup
315
+ const key = 'stateKey';
316
+ const value = 'stateValue';
317
+ const newPluginState = { [key]: value };
318
+ (MessageModel.update as Mock).mockResolvedValue({
319
+ ...mockMessage,
320
+ pluginState: newPluginState,
321
+ });
322
+
323
+ // Execute
324
+ const result = await messageService.updateMessagePluginState(mockMessageId, key, value);
325
+
326
+ // Assert
327
+ expect(MessageModel.update).toHaveBeenCalledWith(mockMessageId, {
328
+ pluginState: newPluginState,
329
+ });
330
+ expect(result).toEqual({ ...mockMessage, pluginState: newPluginState });
331
+ });
332
+ });
333
+ });
@@ -0,0 +1,88 @@
1
+ import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { getPluginIndexJSON } from '@/const/url';
4
+ import { getCurrentLanguage } from '@/store/global/helpers';
5
+
6
+ import { pluginService } from '../plugin';
7
+
8
+ // Mocking modules and functions
9
+ vi.mock('@/const/url', () => ({
10
+ getPluginIndexJSON: vi.fn(),
11
+ }));
12
+ vi.mock('@/store/global/helpers', () => ({
13
+ getCurrentLanguage: vi.fn(),
14
+ }));
15
+
16
+ describe('PluginService', () => {
17
+ beforeEach(() => {
18
+ vi.resetAllMocks();
19
+ });
20
+
21
+ describe('getPluginList', () => {
22
+ it('should fetch and return the plugin list', async () => {
23
+ // Arrange
24
+ const fakeResponse = { plugins: [{ name: 'TestPlugin' }] };
25
+ const fakeUrl = 'http://fake-url.com/plugins.json';
26
+ (getCurrentLanguage as Mock).mockReturnValue('en');
27
+ (getPluginIndexJSON as Mock).mockReturnValue(fakeUrl);
28
+ global.fetch = vi.fn(() =>
29
+ Promise.resolve({
30
+ json: () => Promise.resolve(fakeResponse),
31
+ }),
32
+ ) as any;
33
+
34
+ // Act
35
+ const pluginList = await pluginService.getPluginList();
36
+
37
+ // Assert
38
+ expect(getCurrentLanguage).toHaveBeenCalled();
39
+ expect(getPluginIndexJSON).toHaveBeenCalledWith('en');
40
+ expect(fetch).toHaveBeenCalledWith(fakeUrl);
41
+ expect(pluginList).toEqual(fakeResponse);
42
+ });
43
+
44
+ it('should handle fetch error', async () => {
45
+ // Arrange
46
+ const fakeUrl = 'http://fake-url.com/plugins.json';
47
+ (getCurrentLanguage as Mock).mockReturnValue('en');
48
+ (getPluginIndexJSON as Mock).mockReturnValue(fakeUrl);
49
+ global.fetch = vi.fn(() => Promise.reject(new Error('Network error')));
50
+
51
+ // Act & Assert
52
+ await expect(pluginService.getPluginList()).rejects.toThrow('Network error');
53
+ });
54
+ });
55
+
56
+ describe('fetchManifest', () => {
57
+ it('should fetch and return the plugin manifest', async () => {
58
+ // Arrange
59
+ const fakeManifest = { name: 'TestPlugin', version: '1.0.0' };
60
+ const manifestUrl = 'http://fake-url.com/manifest.json';
61
+ global.fetch = vi.fn(() =>
62
+ Promise.resolve({
63
+ json: () => Promise.resolve(fakeManifest),
64
+ }),
65
+ ) as any;
66
+
67
+ // Act
68
+ const manifest = await pluginService.fetchManifest(manifestUrl);
69
+
70
+ // Assert
71
+ expect(fetch).toHaveBeenCalledWith(manifestUrl);
72
+ expect(manifest).toEqual(fakeManifest);
73
+ });
74
+
75
+ it('should return null on fetch error', async () => {
76
+ // Arrange
77
+ const manifestUrl = 'http://fake-url.com/manifest.json';
78
+ global.fetch = vi.fn(() => Promise.reject(new Error('Network error')));
79
+
80
+ // Act
81
+ const manifest = await pluginService.fetchManifest(manifestUrl);
82
+
83
+ // Assert
84
+ expect(fetch).toHaveBeenCalledWith(manifestUrl);
85
+ expect(manifest).toBeNull();
86
+ });
87
+ });
88
+ });