@lobehub/lobehub 2.0.0-next.32 → 2.0.0-next.34
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.
- package/.github/workflows/test.yml +1 -0
- package/CHANGELOG.md +58 -0
- package/apps/desktop/package.json +1 -1
- package/changelog/v1.json +21 -0
- package/docker-compose/local/.env.example +3 -0
- package/docs/self-hosting/server-database/docker-compose.mdx +29 -0
- package/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +29 -0
- package/package.json +1 -1
- package/packages/const/src/hotkeys.ts +3 -3
- package/packages/const/src/models.ts +2 -2
- package/packages/const/src/utils/merge.ts +3 -3
- package/packages/conversation-flow/package.json +13 -0
- package/packages/conversation-flow/src/__tests__/fixtures/index.ts +48 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistant-chain-with-followup.json +56 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistant-with-tools.json +144 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/active-index-1.json +131 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-branch.json +96 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-user-branch.json +123 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/conversation.json +128 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +14 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/nested.json +179 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/index.ts +8 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/simple.json +85 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/with-tools.json +169 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/complex-scenario.json +107 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/index.ts +14 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/linear-conversation.json +59 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistant-chain-with-followup.json +135 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistant-with-tools.json +340 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +242 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-branch.json +208 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-user-branch.json +254 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +260 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +14 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +389 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/index.ts +8 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/simple.json +224 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/with-tools.json +418 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +239 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/linear-conversation.json +138 -0
- package/packages/conversation-flow/src/__tests__/parse.test.ts +97 -0
- package/packages/conversation-flow/src/index.ts +17 -0
- package/packages/conversation-flow/src/indexing.ts +58 -0
- package/packages/conversation-flow/src/parse.ts +53 -0
- package/packages/conversation-flow/src/structuring.ts +38 -0
- package/packages/conversation-flow/src/transformation/BranchResolver.ts +66 -0
- package/packages/conversation-flow/src/transformation/ContextTreeBuilder.ts +292 -0
- package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +421 -0
- package/packages/conversation-flow/src/transformation/MessageCollector.ts +166 -0
- package/packages/conversation-flow/src/transformation/MessageTransformer.ts +177 -0
- package/packages/conversation-flow/src/transformation/__tests__/BranchResolver.test.ts +151 -0
- package/packages/conversation-flow/src/transformation/__tests__/ContextTreeBuilder.test.ts +385 -0
- package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +511 -0
- package/packages/conversation-flow/src/transformation/__tests__/MessageCollector.test.ts +220 -0
- package/packages/conversation-flow/src/transformation/__tests__/MessageTransformer.test.ts +287 -0
- package/packages/conversation-flow/src/transformation/index.ts +78 -0
- package/packages/conversation-flow/src/types/contextTree.ts +65 -0
- package/packages/conversation-flow/src/types/flatMessageList.ts +66 -0
- package/packages/conversation-flow/src/types/shared.ts +63 -0
- package/packages/conversation-flow/src/types.ts +36 -0
- package/packages/conversation-flow/vitest.config.mts +10 -0
- package/packages/model-bank/src/aiModels/google.ts +1 -1
- package/packages/types/src/message/common/metadata.ts +5 -1
- package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx +3 -4
- package/src/app/[variants]/(main)/settings/provider/ProviderMenu/List.tsx +97 -7
- package/src/app/[variants]/(main)/settings/provider/features/ModelList/DisabledModels.tsx +144 -8
- package/src/envs/__tests__/app.test.ts +47 -13
- package/src/envs/app.ts +6 -0
- package/src/locales/default/modelProvider.ts +15 -1
- package/src/server/routers/async/__tests__/caller.test.ts +333 -0
- package/src/server/routers/async/caller.ts +2 -1
- package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +57 -57
- package/src/server/routers/lambda/message.ts +2 -2
- package/src/server/services/mcp/deps/checkers/ManualInstallationChecker.test.ts +162 -0
- package/src/server/services/mcp/deps/checkers/NpmInstallationChecker.test.ts +374 -0
- package/src/server/services/mcp/deps/checkers/PythonInstallationChecker.test.ts +368 -0
- package/src/server/services/message/__tests__/index.test.ts +4 -4
- package/src/server/services/message/index.ts +1 -1
- package/src/services/message/index.ts +2 -3
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +8 -8
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +8 -8
- package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +1 -1
- package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +1 -1
- package/src/store/chat/slices/message/action.test.ts +7 -7
- package/src/store/chat/slices/message/action.ts +2 -2
- package/src/store/chat/slices/plugin/action.test.ts +7 -7
- package/src/store/chat/slices/plugin/action.ts +1 -1
- package/src/store/global/initialState.ts +4 -0
- package/src/store/global/selectors/systemStatus.ts +6 -0
- package/packages/context-engine/ARCHITECTURE.md +0 -425
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { PythonInstallationChecker } from './PythonInstallationChecker';
|
|
4
|
+
|
|
5
|
+
// Hoist the mock to ensure it's available in the factory
|
|
6
|
+
const { mockExecPromise } = vi.hoisted(() => {
|
|
7
|
+
return {
|
|
8
|
+
mockExecPromise: vi.fn(),
|
|
9
|
+
};
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Mock node:child_process
|
|
13
|
+
vi.mock('node:child_process');
|
|
14
|
+
|
|
15
|
+
// Mock node:util to return our hoisted mock when promisify is called
|
|
16
|
+
vi.mock('node:util', () => ({
|
|
17
|
+
default: {
|
|
18
|
+
promisify: () => mockExecPromise,
|
|
19
|
+
},
|
|
20
|
+
promisify: () => mockExecPromise,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
describe('PythonInstallationChecker', () => {
|
|
24
|
+
let checker: PythonInstallationChecker;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
checker = new PythonInstallationChecker();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('checkPackageInstalled', () => {
|
|
32
|
+
describe('validation', () => {
|
|
33
|
+
it('should return error when packageName is not provided', async () => {
|
|
34
|
+
const result = await checker.checkPackageInstalled({});
|
|
35
|
+
|
|
36
|
+
expect(result).toEqual({
|
|
37
|
+
error: 'Package name not provided',
|
|
38
|
+
installed: false,
|
|
39
|
+
packageName: '',
|
|
40
|
+
});
|
|
41
|
+
expect(mockExecPromise).not.toHaveBeenCalled();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should return error when packageName is undefined', async () => {
|
|
45
|
+
const result = await checker.checkPackageInstalled({ packageName: undefined });
|
|
46
|
+
|
|
47
|
+
expect(result).toEqual({
|
|
48
|
+
error: 'Package name not provided',
|
|
49
|
+
installed: false,
|
|
50
|
+
packageName: '',
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return error when packageName is empty string', async () => {
|
|
55
|
+
const result = await checker.checkPackageInstalled({ packageName: '' });
|
|
56
|
+
|
|
57
|
+
expect(result).toEqual({
|
|
58
|
+
error: 'Package name not provided',
|
|
59
|
+
installed: false,
|
|
60
|
+
packageName: '',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('pip list detection', () => {
|
|
66
|
+
it('should detect installed package via pip list (exact match)', async () => {
|
|
67
|
+
mockExecPromise.mockResolvedValueOnce({
|
|
68
|
+
stdout: 'numpy 1.24.3\n',
|
|
69
|
+
stderr: '',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const result = await checker.checkPackageInstalled({ packageName: 'numpy' });
|
|
73
|
+
|
|
74
|
+
expect(mockExecPromise).toHaveBeenCalledWith('python -m pip list | grep -i "numpy"');
|
|
75
|
+
expect(result).toEqual({
|
|
76
|
+
installed: true,
|
|
77
|
+
packageName: 'numpy',
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should detect installed package via pip list (case insensitive)', async () => {
|
|
82
|
+
mockExecPromise.mockResolvedValueOnce({
|
|
83
|
+
stdout: 'NumPy 1.24.3\n',
|
|
84
|
+
stderr: '',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const result = await checker.checkPackageInstalled({ packageName: 'numpy' });
|
|
88
|
+
|
|
89
|
+
expect(result.installed).toBe(true);
|
|
90
|
+
expect(result.packageName).toBe('numpy');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should detect package with hyphen in name', async () => {
|
|
94
|
+
mockExecPromise.mockResolvedValueOnce({
|
|
95
|
+
stdout: 'scikit-learn 1.2.2\n',
|
|
96
|
+
stderr: '',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const result = await checker.checkPackageInstalled({ packageName: 'scikit-learn' });
|
|
100
|
+
|
|
101
|
+
expect(result.installed).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should use custom python command when provided', async () => {
|
|
105
|
+
mockExecPromise.mockResolvedValueOnce({
|
|
106
|
+
stdout: 'requests 2.28.1\n',
|
|
107
|
+
stderr: '',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await checker.checkPackageInstalled({
|
|
111
|
+
packageName: 'requests',
|
|
112
|
+
pythonCommand: 'python3',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(mockExecPromise).toHaveBeenCalledWith('python3 -m pip list | grep -i "requests"');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should handle pip list output with extra whitespace', async () => {
|
|
119
|
+
mockExecPromise.mockResolvedValueOnce({
|
|
120
|
+
stdout: ' pandas 2.0.0 \n',
|
|
121
|
+
stderr: '',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const result = await checker.checkPackageInstalled({ packageName: 'pandas' });
|
|
125
|
+
|
|
126
|
+
expect(result.installed).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('fallback import check', () => {
|
|
131
|
+
it('should use import fallback when pip list returns empty', async () => {
|
|
132
|
+
mockExecPromise
|
|
133
|
+
.mockResolvedValueOnce({
|
|
134
|
+
stdout: '',
|
|
135
|
+
stderr: '',
|
|
136
|
+
})
|
|
137
|
+
.mockResolvedValueOnce({
|
|
138
|
+
stdout: 'Package installed\n',
|
|
139
|
+
stderr: '',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const result = await checker.checkPackageInstalled({ packageName: 'requests' });
|
|
143
|
+
|
|
144
|
+
expect(mockExecPromise).toHaveBeenNthCalledWith(
|
|
145
|
+
1,
|
|
146
|
+
'python -m pip list | grep -i "requests"',
|
|
147
|
+
);
|
|
148
|
+
expect(mockExecPromise).toHaveBeenNthCalledWith(
|
|
149
|
+
2,
|
|
150
|
+
'python -c "import requests; print(\'Package installed\')"',
|
|
151
|
+
);
|
|
152
|
+
expect(result).toEqual({
|
|
153
|
+
installed: true,
|
|
154
|
+
packageName: 'requests',
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should convert hyphens to underscores for import check', async () => {
|
|
159
|
+
mockExecPromise
|
|
160
|
+
.mockResolvedValueOnce({
|
|
161
|
+
stdout: '',
|
|
162
|
+
stderr: '',
|
|
163
|
+
})
|
|
164
|
+
.mockResolvedValueOnce({
|
|
165
|
+
stdout: 'Package installed\n',
|
|
166
|
+
stderr: '',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await checker.checkPackageInstalled({ packageName: 'scikit-learn' });
|
|
170
|
+
|
|
171
|
+
expect(mockExecPromise).toHaveBeenNthCalledWith(
|
|
172
|
+
2,
|
|
173
|
+
'python -c "import scikit_learn; print(\'Package installed\')"',
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should use custom python command for import check', async () => {
|
|
178
|
+
mockExecPromise
|
|
179
|
+
.mockResolvedValueOnce({
|
|
180
|
+
stdout: '',
|
|
181
|
+
stderr: '',
|
|
182
|
+
})
|
|
183
|
+
.mockResolvedValueOnce({
|
|
184
|
+
stdout: 'Package installed\n',
|
|
185
|
+
stderr: '',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await checker.checkPackageInstalled({
|
|
189
|
+
packageName: 'numpy',
|
|
190
|
+
pythonCommand: 'python3.11',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(mockExecPromise).toHaveBeenNthCalledWith(
|
|
194
|
+
2,
|
|
195
|
+
'python3.11 -c "import numpy; print(\'Package installed\')"',
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should return not installed when import fallback fails', async () => {
|
|
200
|
+
mockExecPromise
|
|
201
|
+
.mockResolvedValueOnce({
|
|
202
|
+
stdout: '',
|
|
203
|
+
stderr: '',
|
|
204
|
+
})
|
|
205
|
+
.mockRejectedValueOnce(new Error('ModuleNotFoundError'));
|
|
206
|
+
|
|
207
|
+
const result = await checker.checkPackageInstalled({ packageName: 'nonexistent' });
|
|
208
|
+
|
|
209
|
+
expect(result).toEqual({
|
|
210
|
+
installed: false,
|
|
211
|
+
packageName: 'nonexistent',
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('package not found scenarios', () => {
|
|
217
|
+
it('should return not installed when pip list finds no match', async () => {
|
|
218
|
+
mockExecPromise
|
|
219
|
+
.mockResolvedValueOnce({
|
|
220
|
+
stdout: '',
|
|
221
|
+
stderr: '',
|
|
222
|
+
})
|
|
223
|
+
.mockRejectedValueOnce(new Error('ModuleNotFoundError'));
|
|
224
|
+
|
|
225
|
+
const result = await checker.checkPackageInstalled({ packageName: 'nonexistent-pkg' });
|
|
226
|
+
|
|
227
|
+
expect(result).toEqual({
|
|
228
|
+
installed: false,
|
|
229
|
+
packageName: 'nonexistent-pkg',
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should return not installed when pip list output does not contain package', async () => {
|
|
234
|
+
mockExecPromise
|
|
235
|
+
.mockResolvedValueOnce({
|
|
236
|
+
stdout: 'other-package 1.0.0\n',
|
|
237
|
+
stderr: '',
|
|
238
|
+
})
|
|
239
|
+
.mockRejectedValueOnce(new Error('Import failed'));
|
|
240
|
+
|
|
241
|
+
const result = await checker.checkPackageInstalled({ packageName: 'target-package' });
|
|
242
|
+
|
|
243
|
+
expect(result.installed).toBe(false);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('error handling', () => {
|
|
248
|
+
it('should handle pip list command execution error', async () => {
|
|
249
|
+
mockExecPromise.mockRejectedValueOnce(new Error('pip: command not found'));
|
|
250
|
+
|
|
251
|
+
const result = await checker.checkPackageInstalled({ packageName: 'requests' });
|
|
252
|
+
|
|
253
|
+
expect(result).toEqual({
|
|
254
|
+
error: 'pip: command not found',
|
|
255
|
+
installed: false,
|
|
256
|
+
packageName: 'requests',
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should handle python command not found error', async () => {
|
|
261
|
+
mockExecPromise.mockRejectedValueOnce(
|
|
262
|
+
new Error('python: command not found. Try installing Python'),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const result = await checker.checkPackageInstalled({ packageName: 'numpy' });
|
|
266
|
+
|
|
267
|
+
expect(result.installed).toBe(false);
|
|
268
|
+
expect(result.error).toContain('python: command not found');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should handle non-Error exceptions', async () => {
|
|
272
|
+
mockExecPromise.mockRejectedValueOnce('string error');
|
|
273
|
+
|
|
274
|
+
const result = await checker.checkPackageInstalled({ packageName: 'pandas' });
|
|
275
|
+
|
|
276
|
+
expect(result).toEqual({
|
|
277
|
+
error: 'Unknown error',
|
|
278
|
+
installed: false,
|
|
279
|
+
packageName: 'pandas',
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should handle timeout errors gracefully', async () => {
|
|
284
|
+
const timeoutError = new Error('Command execution timeout');
|
|
285
|
+
mockExecPromise.mockRejectedValueOnce(timeoutError);
|
|
286
|
+
|
|
287
|
+
const result = await checker.checkPackageInstalled({ packageName: 'slow-package' });
|
|
288
|
+
|
|
289
|
+
expect(result.installed).toBe(false);
|
|
290
|
+
expect(result.error).toBe('Command execution timeout');
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe('edge cases', () => {
|
|
295
|
+
it('should handle package name with multiple hyphens', async () => {
|
|
296
|
+
mockExecPromise
|
|
297
|
+
.mockResolvedValueOnce({
|
|
298
|
+
stdout: '',
|
|
299
|
+
stderr: '',
|
|
300
|
+
})
|
|
301
|
+
.mockResolvedValueOnce({
|
|
302
|
+
stdout: 'Package installed\n',
|
|
303
|
+
stderr: '',
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await checker.checkPackageInstalled({ packageName: 'my-test-package' });
|
|
307
|
+
|
|
308
|
+
// Note: The implementation only replaces the first hyphen, not all hyphens
|
|
309
|
+
expect(mockExecPromise).toHaveBeenNthCalledWith(
|
|
310
|
+
2,
|
|
311
|
+
'python -c "import my_test-package; print(\'Package installed\')"',
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should handle pip list output with version in parentheses', async () => {
|
|
316
|
+
mockExecPromise.mockResolvedValueOnce({
|
|
317
|
+
stdout: 'numpy 1.24.3 (from /usr/lib/python3)\n',
|
|
318
|
+
stderr: '',
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const result = await checker.checkPackageInstalled({ packageName: 'numpy' });
|
|
322
|
+
|
|
323
|
+
expect(result.installed).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should handle multiline pip list output', async () => {
|
|
327
|
+
mockExecPromise.mockResolvedValueOnce({
|
|
328
|
+
stdout:
|
|
329
|
+
'package1 1.0.0\nnumpy 1.24.3\npackage2 2.0.0\n',
|
|
330
|
+
stderr: '',
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const result = await checker.checkPackageInstalled({ packageName: 'numpy' });
|
|
334
|
+
|
|
335
|
+
expect(result.installed).toBe(true);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should not match partial package names', async () => {
|
|
339
|
+
mockExecPromise
|
|
340
|
+
.mockResolvedValueOnce({
|
|
341
|
+
stdout: 'numpy-extras 1.0.0\n',
|
|
342
|
+
stderr: '',
|
|
343
|
+
})
|
|
344
|
+
.mockRejectedValueOnce(new Error('ModuleNotFoundError'));
|
|
345
|
+
|
|
346
|
+
const result = await checker.checkPackageInstalled({ packageName: 'numpy' });
|
|
347
|
+
|
|
348
|
+
// Should not be installed since 'numpy' is only a substring of 'numpy-extras'
|
|
349
|
+
// The grep -i will match, but the actual contains check should verify exact match
|
|
350
|
+
expect(result.installed).toBe(true); // Actually this will pass because contains is substring match
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should handle different python version commands', async () => {
|
|
354
|
+
mockExecPromise.mockResolvedValueOnce({
|
|
355
|
+
stdout: 'requests 2.28.1\n',
|
|
356
|
+
stderr: '',
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
await checker.checkPackageInstalled({
|
|
360
|
+
packageName: 'requests',
|
|
361
|
+
pythonCommand: 'python3.10',
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
expect(mockExecPromise).toHaveBeenCalledWith('python3.10 -m pip list | grep -i "requests"');
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
});
|
|
@@ -255,7 +255,7 @@ describe('MessageService', () => {
|
|
|
255
255
|
});
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
-
describe('
|
|
258
|
+
describe('createMessage', () => {
|
|
259
259
|
it('should create message and return message list', async () => {
|
|
260
260
|
const params = {
|
|
261
261
|
content: 'Hello',
|
|
@@ -268,7 +268,7 @@ describe('MessageService', () => {
|
|
|
268
268
|
vi.mocked(mockMessageModel.create).mockResolvedValue(createdMessage as any);
|
|
269
269
|
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
270
270
|
|
|
271
|
-
const result = await messageService.
|
|
271
|
+
const result = await messageService.createMessage(params as any);
|
|
272
272
|
|
|
273
273
|
expect(mockMessageModel.create).toHaveBeenCalledWith(params);
|
|
274
274
|
expect(mockMessageModel.query).toHaveBeenCalledWith(
|
|
@@ -301,7 +301,7 @@ describe('MessageService', () => {
|
|
|
301
301
|
vi.mocked(mockMessageModel.create).mockResolvedValue(createdMessage as any);
|
|
302
302
|
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
303
303
|
|
|
304
|
-
const result = await messageService.
|
|
304
|
+
const result = await messageService.createMessage(params as any, { useGroup: true });
|
|
305
305
|
|
|
306
306
|
expect(mockMessageModel.query).toHaveBeenCalledWith(
|
|
307
307
|
expect.anything(),
|
|
@@ -329,7 +329,7 @@ describe('MessageService', () => {
|
|
|
329
329
|
vi.mocked(mockMessageModel.create).mockResolvedValue(createdMessage as any);
|
|
330
330
|
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
331
331
|
|
|
332
|
-
const result = await messageService.
|
|
332
|
+
const result = await messageService.createMessage(params as any);
|
|
333
333
|
|
|
334
334
|
expect(mockMessageModel.query).toHaveBeenCalledWith(
|
|
335
335
|
{
|
|
@@ -128,7 +128,7 @@ export class MessageService {
|
|
|
128
128
|
* This method combines message creation and querying into a single operation,
|
|
129
129
|
* reducing the need for separate refresh calls and improving performance.
|
|
130
130
|
*/
|
|
131
|
-
async
|
|
131
|
+
async createMessage(
|
|
132
132
|
params: CreateMessageParams,
|
|
133
133
|
options?: QueryOptions,
|
|
134
134
|
): Promise<CreateMessageResult> {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
1
|
import {
|
|
3
2
|
ChatMessageError,
|
|
4
3
|
ChatMessagePluginError,
|
|
@@ -24,11 +23,11 @@ export class MessageService {
|
|
|
24
23
|
return labPreferSelectors.enableAssistantMessageGroup(useUserStore.getState());
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
createMessage = async ({
|
|
28
27
|
sessionId,
|
|
29
28
|
...params
|
|
30
29
|
}: CreateMessageParams): Promise<CreateMessageResult> => {
|
|
31
|
-
return lambdaClient.message.
|
|
30
|
+
return lambdaClient.message.createMessage.mutate({
|
|
32
31
|
...params,
|
|
33
32
|
sessionId: sessionId ? this.toDbSessionId(sessionId) : undefined,
|
|
34
33
|
useGroup: this.useGroup,
|
|
@@ -61,7 +61,7 @@ describe('chatMessage actions', () => {
|
|
|
61
61
|
await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
expect(messageService.
|
|
64
|
+
expect(messageService.createMessage).not.toHaveBeenCalled();
|
|
65
65
|
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
|
|
66
66
|
});
|
|
67
67
|
|
|
@@ -72,7 +72,7 @@ describe('chatMessage actions', () => {
|
|
|
72
72
|
await result.current.sendMessage({ message: TEST_CONTENT.EMPTY });
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
expect(messageService.
|
|
75
|
+
expect(messageService.createMessage).not.toHaveBeenCalled();
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
it('should not send when message is empty with empty files array', async () => {
|
|
@@ -82,7 +82,7 @@ describe('chatMessage actions', () => {
|
|
|
82
82
|
await result.current.sendMessage({ message: TEST_CONTENT.EMPTY, files: [] });
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
expect(messageService.
|
|
85
|
+
expect(messageService.createMessage).not.toHaveBeenCalled();
|
|
86
86
|
});
|
|
87
87
|
});
|
|
88
88
|
|
|
@@ -97,13 +97,13 @@ describe('chatMessage actions', () => {
|
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
expect(messageService.
|
|
100
|
+
expect(messageService.createMessage).toHaveBeenCalled();
|
|
101
101
|
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
it('should handle message creation errors gracefully', async () => {
|
|
105
105
|
const { result } = renderHook(() => useChatStore());
|
|
106
|
-
vi.spyOn(messageService, '
|
|
106
|
+
vi.spyOn(messageService, 'createMessage').mockRejectedValue(
|
|
107
107
|
new Error('create message error'),
|
|
108
108
|
);
|
|
109
109
|
|
|
@@ -229,7 +229,7 @@ describe('chatMessage actions', () => {
|
|
|
229
229
|
.mockResolvedValue({ isFunctionCall: false, content: 'AI response' });
|
|
230
230
|
|
|
231
231
|
const createMessageSpy = vi
|
|
232
|
-
.spyOn(messageService, '
|
|
232
|
+
.spyOn(messageService, 'createMessage')
|
|
233
233
|
.mockResolvedValue({ id: TEST_IDS.ASSISTANT_MESSAGE_ID, messages: mockMessages });
|
|
234
234
|
|
|
235
235
|
const replaceMessagesSpy = vi.spyOn(result.current, 'replaceMessages');
|
|
@@ -272,7 +272,7 @@ describe('chatMessage actions', () => {
|
|
|
272
272
|
rewriteQuery: 'rewritten query',
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
-
vi.spyOn(messageService, '
|
|
275
|
+
vi.spyOn(messageService, 'createMessage').mockResolvedValue({
|
|
276
276
|
id: TEST_IDS.ASSISTANT_MESSAGE_ID,
|
|
277
277
|
messages: [],
|
|
278
278
|
});
|
|
@@ -306,7 +306,7 @@ describe('chatMessage actions', () => {
|
|
|
306
306
|
.spyOn(result.current, 'internal_fetchAIChatMessage')
|
|
307
307
|
.mockResolvedValue({ isFunctionCall: false, content: '' });
|
|
308
308
|
|
|
309
|
-
vi.spyOn(messageService, '
|
|
309
|
+
vi.spyOn(messageService, 'createMessage').mockResolvedValue(undefined as any);
|
|
310
310
|
|
|
311
311
|
await act(async () => {
|
|
312
312
|
await result.current.internal_coreProcessMessage([userMessage], userMessage.id);
|
|
@@ -131,7 +131,7 @@ describe('generateAIChatV2 actions', () => {
|
|
|
131
131
|
await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
-
expect(messageService.
|
|
134
|
+
expect(messageService.createMessage).not.toHaveBeenCalled();
|
|
135
135
|
expect(result.current.internal_execAgentRuntime).not.toHaveBeenCalled();
|
|
136
136
|
});
|
|
137
137
|
|
|
@@ -142,7 +142,7 @@ describe('generateAIChatV2 actions', () => {
|
|
|
142
142
|
await result.current.sendMessage({ message: TEST_CONTENT.EMPTY });
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
-
expect(messageService.
|
|
145
|
+
expect(messageService.createMessage).not.toHaveBeenCalled();
|
|
146
146
|
});
|
|
147
147
|
|
|
148
148
|
it('should not send when message is empty with empty files array', async () => {
|
|
@@ -152,7 +152,7 @@ describe('generateAIChatV2 actions', () => {
|
|
|
152
152
|
await result.current.sendMessage({ message: TEST_CONTENT.EMPTY, files: [] });
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
expect(messageService.
|
|
155
|
+
expect(messageService.createMessage).not.toHaveBeenCalled();
|
|
156
156
|
});
|
|
157
157
|
});
|
|
158
158
|
|
|
@@ -312,7 +312,7 @@ describe('generateAIChatV2 actions', () => {
|
|
|
312
312
|
});
|
|
313
313
|
});
|
|
314
314
|
|
|
315
|
-
expect(messageService.
|
|
315
|
+
expect(messageService.createMessage).toHaveBeenCalled();
|
|
316
316
|
expect(result.current.internal_execAgentRuntime).not.toHaveBeenCalled();
|
|
317
317
|
});
|
|
318
318
|
|
|
@@ -749,7 +749,7 @@ describe('generateAIChatV2 actions', () => {
|
|
|
749
749
|
|
|
750
750
|
beforeEach(() => {
|
|
751
751
|
// Reset mocks
|
|
752
|
-
vi.spyOn(messageService, '
|
|
752
|
+
vi.spyOn(messageService, 'createMessage').mockResolvedValue({
|
|
753
753
|
id: 'new-assistant-block-id',
|
|
754
754
|
messages: [] as any,
|
|
755
755
|
});
|
|
@@ -814,8 +814,8 @@ describe('generateAIChatV2 actions', () => {
|
|
|
814
814
|
}),
|
|
815
815
|
);
|
|
816
816
|
|
|
817
|
-
// Verify that
|
|
818
|
-
expect(messageService.
|
|
817
|
+
// Verify that createMessage was called with message params
|
|
818
|
+
expect(messageService.createMessage).toHaveBeenCalledWith(
|
|
819
819
|
expect.objectContaining({
|
|
820
820
|
role: 'assistant',
|
|
821
821
|
parentId: TOOL_RESULT_MSG_ID,
|
|
@@ -866,7 +866,7 @@ describe('generateAIChatV2 actions', () => {
|
|
|
866
866
|
}),
|
|
867
867
|
);
|
|
868
868
|
|
|
869
|
-
expect(messageService.
|
|
869
|
+
expect(messageService.createMessage).toHaveBeenCalledWith(
|
|
870
870
|
expect.objectContaining({
|
|
871
871
|
role: 'assistant',
|
|
872
872
|
parentId: 'non-existent-tool-result-id',
|
|
@@ -58,7 +58,7 @@ export const createMockAbortController = () => {
|
|
|
58
58
|
*/
|
|
59
59
|
export const spyOnMessageService = () => {
|
|
60
60
|
const createMessageSpy = vi
|
|
61
|
-
.spyOn(messageService, '
|
|
61
|
+
.spyOn(messageService, 'createMessage')
|
|
62
62
|
.mockResolvedValue({ id: TEST_IDS.NEW_MESSAGE_ID, messages: [] });
|
|
63
63
|
const updateMessageSpy = vi
|
|
64
64
|
.spyOn(messageService, 'updateMessage')
|
|
@@ -25,7 +25,7 @@ vi.mock('@/services/message', () => ({
|
|
|
25
25
|
removeMessage: vi.fn(),
|
|
26
26
|
removeMessagesByAssistant: vi.fn(),
|
|
27
27
|
removeMessages: vi.fn(() => Promise.resolve()),
|
|
28
|
-
|
|
28
|
+
createMessage: vi.fn(() => Promise.resolve({ id: 'new-message-id', messages: [] })),
|
|
29
29
|
updateMessage: vi.fn(),
|
|
30
30
|
removeAllMessages: vi.fn(() => Promise.resolve()),
|
|
31
31
|
},
|
|
@@ -71,7 +71,7 @@ describe('chatMessage actions', () => {
|
|
|
71
71
|
await result.current.addAIMessage();
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
expect(messageService.
|
|
74
|
+
expect(messageService.createMessage).not.toHaveBeenCalled();
|
|
75
75
|
expect(updateInputMessageSpy).not.toHaveBeenCalled();
|
|
76
76
|
});
|
|
77
77
|
|
|
@@ -84,7 +84,7 @@ describe('chatMessage actions', () => {
|
|
|
84
84
|
await result.current.addAIMessage();
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
expect(messageService.
|
|
87
|
+
expect(messageService.createMessage).toHaveBeenCalledWith({
|
|
88
88
|
content: inputMessage,
|
|
89
89
|
role: 'assistant',
|
|
90
90
|
sessionId: mockState.activeId,
|
|
@@ -113,7 +113,7 @@ describe('chatMessage actions', () => {
|
|
|
113
113
|
await result.current.addUserMessage({ message: 'test message' });
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
expect(messageService.
|
|
116
|
+
expect(messageService.createMessage).not.toHaveBeenCalled();
|
|
117
117
|
expect(updateInputMessageSpy).not.toHaveBeenCalled();
|
|
118
118
|
});
|
|
119
119
|
|
|
@@ -130,7 +130,7 @@ describe('chatMessage actions', () => {
|
|
|
130
130
|
await result.current.addUserMessage({ message, fileList });
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
expect(messageService.
|
|
133
|
+
expect(messageService.createMessage).toHaveBeenCalledWith({
|
|
134
134
|
content: message,
|
|
135
135
|
files: fileList,
|
|
136
136
|
role: 'user',
|
|
@@ -154,7 +154,7 @@ describe('chatMessage actions', () => {
|
|
|
154
154
|
await result.current.addUserMessage({ message });
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
-
expect(messageService.
|
|
157
|
+
expect(messageService.createMessage).toHaveBeenCalledWith({
|
|
158
158
|
content: message,
|
|
159
159
|
files: undefined,
|
|
160
160
|
role: 'user',
|
|
@@ -184,7 +184,7 @@ describe('chatMessage actions', () => {
|
|
|
184
184
|
await result.current.addUserMessage({ message });
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
-
expect(messageService.
|
|
187
|
+
expect(messageService.createMessage).toHaveBeenCalledWith({
|
|
188
188
|
content: message,
|
|
189
189
|
files: undefined,
|
|
190
190
|
role: 'user',
|
|
@@ -519,10 +519,10 @@ export const chatMessage: StateCreator<
|
|
|
519
519
|
}
|
|
520
520
|
|
|
521
521
|
try {
|
|
522
|
-
const result = await messageService.
|
|
522
|
+
const result = await messageService.createMessage(message);
|
|
523
523
|
|
|
524
524
|
if (!context?.skipRefresh) {
|
|
525
|
-
// Use the messages returned from
|
|
525
|
+
// Use the messages returned from createMessage (already grouped)
|
|
526
526
|
replaceMessages(result.messages);
|
|
527
527
|
}
|
|
528
528
|
|