@lobehub/lobehub 2.0.0-next.22 → 2.0.0-next.23
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/CHANGELOG.md +25 -0
- package/apps/desktop/package.json +1 -1
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/packages/memory-extract/package.json +1 -1
- package/packages/types/src/message/ui/params.ts +3 -3
- package/renovate.json +49 -13
- package/src/server/services/mcp/deps/MCPSystemDepsCheckService.test.ts +541 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.23](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.22...v2.0.0-next.23)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2025-11-04**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Fix send message.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Fix send message, closes [#10041](https://github.com/lobehub/lobe-chat/issues/10041) [#9984](https://github.com/lobehub/lobe-chat/issues/9984) ([7cca60f](https://github.com/lobehub/lobe-chat/commit/7cca60f))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
## [Version 2.0.0-next.22](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.21...v2.0.0-next.22)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2025-11-04**</sup>
|
package/changelog/v1.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.23",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -130,10 +130,10 @@ export const CreateMessageParamsSchema = z
|
|
|
130
130
|
files: z.array(z.string()).optional(),
|
|
131
131
|
fromModel: z.string().optional(),
|
|
132
132
|
fromProvider: z.string().optional(),
|
|
133
|
-
groupId: z.string().optional(),
|
|
133
|
+
groupId: z.string().nullable().optional(),
|
|
134
134
|
targetId: z.string().nullable().optional(),
|
|
135
135
|
threadId: z.string().nullable().optional(),
|
|
136
|
-
topicId: z.string().optional(),
|
|
136
|
+
topicId: z.string().nullable().optional(),
|
|
137
137
|
traceId: z.string().optional(),
|
|
138
138
|
// Allow additional fields from UIChatMessage (many can be null)
|
|
139
139
|
agentId: z.string().optional(),
|
|
@@ -182,7 +182,7 @@ export const CreateNewMessageParamsSchema = z
|
|
|
182
182
|
parentId: z.string().optional(),
|
|
183
183
|
groupId: z.string().nullable().optional(),
|
|
184
184
|
// Context
|
|
185
|
-
topicId: z.string().optional(),
|
|
185
|
+
topicId: z.string().nullable().optional(),
|
|
186
186
|
threadId: z.string().nullable().optional(),
|
|
187
187
|
targetId: z.string().nullable().optional(),
|
|
188
188
|
// Model info
|
package/renovate.json
CHANGED
|
@@ -15,28 +15,64 @@
|
|
|
15
15
|
"workarounds:all"
|
|
16
16
|
],
|
|
17
17
|
"ignoreDeps": [],
|
|
18
|
-
"labels": [
|
|
19
|
-
"dependencies"
|
|
20
|
-
],
|
|
18
|
+
"labels": ["dependencies"],
|
|
21
19
|
"packageRules": [
|
|
20
|
+
// 1) Pinned deps: isolate (OK to use separate* here because there's no matchUpdateTypes)
|
|
21
|
+
{
|
|
22
|
+
"description": "Isolate PRs for pinned deps (exact x.y.z)",
|
|
23
|
+
"matchManagers": ["npm", "pnpm", "yarn", "bun"],
|
|
24
|
+
"matchDepTypes": [
|
|
25
|
+
"dependencies",
|
|
26
|
+
"devDependencies",
|
|
27
|
+
"optionalDependencies",
|
|
28
|
+
"peerDependencies"
|
|
29
|
+
],
|
|
30
|
+
"matchCurrentValue": "^\\d+\\.\\d+\\.\\d+([+-][0-9A-Za-z.-]+)?$",
|
|
31
|
+
"groupName": null,
|
|
32
|
+
"separateMinorPatch": true,
|
|
33
|
+
"separateMajorMinor": true,
|
|
34
|
+
"separateMultipleMinor": true,
|
|
35
|
+
"separateMultipleMajor": true
|
|
36
|
+
},
|
|
37
|
+
// 2a) Non-pinned deps: override splitting so patch+minor can be combined
|
|
22
38
|
{
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
39
|
+
"description": "Non-pinned deps: allow patch+minor to group; keep majors separate",
|
|
40
|
+
"matchManagers": ["npm", "pnpm", "yarn", "bun"],
|
|
41
|
+
"matchDepTypes": [
|
|
42
|
+
"dependencies",
|
|
43
|
+
"devDependencies",
|
|
44
|
+
"optionalDependencies",
|
|
45
|
+
"peerDependencies"
|
|
27
46
|
],
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
"matchCurrentValue": "/(^[~^]|[<>=| -])/", // anything that looks like a range
|
|
48
|
+
"separateMinorPatch": false,
|
|
49
|
+
"separateMajorMinor": true
|
|
50
|
+
},
|
|
51
|
+
// 2b) Non-pinned deps: actually group patch+minor together
|
|
52
|
+
{
|
|
53
|
+
"description": "Non-pinned deps: group non-major updates",
|
|
54
|
+
"matchManagers": ["npm", "pnpm", "yarn", "bun"],
|
|
55
|
+
"matchDepTypes": [
|
|
56
|
+
"dependencies",
|
|
57
|
+
"devDependencies",
|
|
58
|
+
"optionalDependencies",
|
|
59
|
+
"peerDependencies"
|
|
60
|
+
],
|
|
61
|
+
"matchCurrentValue": "/(^[~^]|[<>=| -])/",
|
|
62
|
+
"matchUpdateTypes": ["minor", "patch"], // only non-majors
|
|
63
|
+
"groupName": "deps (non-major)"
|
|
31
64
|
}
|
|
32
65
|
],
|
|
33
|
-
"postUpdateOptions": [
|
|
34
|
-
"yarnDedupeHighest"
|
|
35
|
-
],
|
|
66
|
+
"postUpdateOptions": ["yarnDedupeHighest"],
|
|
36
67
|
"prConcurrentLimit": 30,
|
|
37
68
|
"prHourlyLimit": 0,
|
|
38
69
|
"rangeStrategy": "bump",
|
|
39
70
|
"rebaseWhen": "conflicted",
|
|
40
71
|
"schedule": "on sunday before 6:00am",
|
|
72
|
+
"separateMajorMinor": true,
|
|
73
|
+
// Global defaults are fine; rule 2a overrides minor/patch splitting for ranged deps
|
|
74
|
+
"separateMinorPatch": true,
|
|
75
|
+
"separateMultipleMajor": true,
|
|
76
|
+
"separateMultipleMinor": true,
|
|
41
77
|
"timezone": "UTC"
|
|
42
78
|
}
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { DeploymentOption, SystemDependency } from '@lobehub/market-sdk';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
// Import after mock setup
|
|
5
|
+
import { mcpSystemDepsCheckService } from './MCPSystemDepsCheckService';
|
|
6
|
+
import { InstallationChecker } from './types';
|
|
7
|
+
|
|
8
|
+
// Hoist the mock to ensure it's available in the factory
|
|
9
|
+
const { mockExecPromise } = vi.hoisted(() => {
|
|
10
|
+
return {
|
|
11
|
+
mockExecPromise: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Mock node:child_process
|
|
16
|
+
vi.mock('node:child_process');
|
|
17
|
+
|
|
18
|
+
// Mock node:util to return our hoisted mock when promisify is called
|
|
19
|
+
vi.mock('node:util', () => ({
|
|
20
|
+
default: {
|
|
21
|
+
promisify: () => mockExecPromise,
|
|
22
|
+
},
|
|
23
|
+
promisify: () => mockExecPromise,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
describe('MCPSystemDepsCheckService', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('registerChecker', () => {
|
|
32
|
+
it('should register an installation checker', () => {
|
|
33
|
+
const mockChecker: InstallationChecker = {
|
|
34
|
+
checkPackageInstalled: vi.fn(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
mcpSystemDepsCheckService.registerChecker('npm', mockChecker);
|
|
38
|
+
|
|
39
|
+
// Verify checker is registered by using it in checkDeployOption
|
|
40
|
+
expect(() => mcpSystemDepsCheckService.registerChecker('npm', mockChecker)).not.toThrow();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('checkSystemDependency', () => {
|
|
45
|
+
it('should successfully check installed dependency without version requirement', async () => {
|
|
46
|
+
const mockDependency: SystemDependency = {
|
|
47
|
+
name: 'node',
|
|
48
|
+
checkCommand: 'node --version',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
mockExecPromise.mockResolvedValue({ stdout: 'v18.16.0\n', stderr: '' });
|
|
52
|
+
|
|
53
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
54
|
+
|
|
55
|
+
expect(result).toEqual({
|
|
56
|
+
installed: true,
|
|
57
|
+
meetRequirement: true,
|
|
58
|
+
name: 'node',
|
|
59
|
+
requiredVersion: undefined,
|
|
60
|
+
version: 'v18.16.0',
|
|
61
|
+
installInstructions: undefined,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should parse version when versionParsingRequired is true', async () => {
|
|
66
|
+
const mockDependency: SystemDependency = {
|
|
67
|
+
name: 'python',
|
|
68
|
+
checkCommand: 'python --version',
|
|
69
|
+
versionParsingRequired: true,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
mockExecPromise.mockResolvedValue({ stdout: 'Python 3.10.5\n', stderr: '' });
|
|
73
|
+
|
|
74
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
75
|
+
|
|
76
|
+
expect(result.version).toBe('3.10.5');
|
|
77
|
+
expect(result.installed).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should handle version with v prefix in parsing', async () => {
|
|
81
|
+
const mockDependency: SystemDependency = {
|
|
82
|
+
name: 'node',
|
|
83
|
+
checkCommand: 'node --version',
|
|
84
|
+
versionParsingRequired: true,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
mockExecPromise.mockResolvedValue({ stdout: 'v20.1.0\n', stderr: '' });
|
|
88
|
+
|
|
89
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
90
|
+
|
|
91
|
+
expect(result.version).toBe('v20.1.0');
|
|
92
|
+
expect(result.installed).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should check version with >= operator', async () => {
|
|
96
|
+
const mockDependency: SystemDependency = {
|
|
97
|
+
name: 'node',
|
|
98
|
+
checkCommand: 'node --version',
|
|
99
|
+
requiredVersion: '>=18.0.0',
|
|
100
|
+
versionParsingRequired: true,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
mockExecPromise.mockResolvedValue({ stdout: 'v20.1.0\n', stderr: '' });
|
|
104
|
+
|
|
105
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
106
|
+
|
|
107
|
+
expect(result.meetRequirement).toBe(true);
|
|
108
|
+
expect(result.installed).toBe(true);
|
|
109
|
+
expect(result.version).toBe('v20.1.0');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should fail version check with >= operator when version is lower', async () => {
|
|
113
|
+
const mockDependency: SystemDependency = {
|
|
114
|
+
name: 'node',
|
|
115
|
+
checkCommand: 'node --version',
|
|
116
|
+
requiredVersion: '>=20.0.0',
|
|
117
|
+
versionParsingRequired: true,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
mockExecPromise.mockResolvedValue({ stdout: 'v18.16.0\n', stderr: '' });
|
|
121
|
+
|
|
122
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
123
|
+
|
|
124
|
+
expect(result.meetRequirement).toBe(false);
|
|
125
|
+
expect(result.installed).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should check version with > operator', async () => {
|
|
129
|
+
const mockDependency: SystemDependency = {
|
|
130
|
+
name: 'python',
|
|
131
|
+
checkCommand: 'python --version',
|
|
132
|
+
requiredVersion: '>3.0.0',
|
|
133
|
+
versionParsingRequired: true,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
mockExecPromise.mockResolvedValue({ stdout: 'Python 3.10.5\n', stderr: '' });
|
|
137
|
+
|
|
138
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
139
|
+
|
|
140
|
+
expect(result.meetRequirement).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should check version with <= operator', async () => {
|
|
144
|
+
const mockDependency: SystemDependency = {
|
|
145
|
+
name: 'tool',
|
|
146
|
+
checkCommand: 'tool --version',
|
|
147
|
+
requiredVersion: '<=2.0.0',
|
|
148
|
+
versionParsingRequired: true,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
mockExecPromise.mockResolvedValue({ stdout: '1.5.0\n', stderr: '' });
|
|
152
|
+
|
|
153
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
154
|
+
|
|
155
|
+
expect(result.meetRequirement).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should check version with < operator', async () => {
|
|
159
|
+
const mockDependency: SystemDependency = {
|
|
160
|
+
name: 'tool',
|
|
161
|
+
checkCommand: 'tool --version',
|
|
162
|
+
requiredVersion: '<2.0.0',
|
|
163
|
+
versionParsingRequired: true,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
mockExecPromise.mockResolvedValue({ stdout: '2.5.0\n', stderr: '' });
|
|
167
|
+
|
|
168
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
169
|
+
|
|
170
|
+
expect(result.meetRequirement).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should check version with = operator (default)', async () => {
|
|
174
|
+
const mockDependency: SystemDependency = {
|
|
175
|
+
name: 'tool',
|
|
176
|
+
checkCommand: 'tool --version',
|
|
177
|
+
requiredVersion: '3.0.0',
|
|
178
|
+
versionParsingRequired: true,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
mockExecPromise.mockResolvedValue({ stdout: '3.0.0\n', stderr: '' });
|
|
182
|
+
|
|
183
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
184
|
+
|
|
185
|
+
expect(result.meetRequirement).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should use default check command when not provided', async () => {
|
|
189
|
+
const mockDependency: SystemDependency = {
|
|
190
|
+
name: 'git',
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
mockExecPromise.mockResolvedValue({ stdout: 'git version 2.39.0\n', stderr: '' });
|
|
194
|
+
|
|
195
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
196
|
+
|
|
197
|
+
expect(result.installed).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should handle stderr without stdout as error', async () => {
|
|
201
|
+
const mockDependency: SystemDependency = {
|
|
202
|
+
name: 'invalid-tool',
|
|
203
|
+
checkCommand: 'invalid-tool --version',
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
mockExecPromise.mockResolvedValue({ stdout: '', stderr: 'command not found' });
|
|
207
|
+
|
|
208
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
209
|
+
|
|
210
|
+
expect(result.installed).toBe(false);
|
|
211
|
+
expect(result.meetRequirement).toBe(false);
|
|
212
|
+
expect(result.error).toBe('command not found');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should handle command execution error', async () => {
|
|
216
|
+
const mockDependency: SystemDependency = {
|
|
217
|
+
name: 'missing-tool',
|
|
218
|
+
checkCommand: 'missing-tool --version',
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
mockExecPromise.mockRejectedValue(new Error('command not found'));
|
|
222
|
+
|
|
223
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
224
|
+
|
|
225
|
+
expect(result.installed).toBe(false);
|
|
226
|
+
expect(result.meetRequirement).toBe(false);
|
|
227
|
+
expect(result.error).toBe('command not found');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should handle unknown error types', async () => {
|
|
231
|
+
const mockDependency: SystemDependency = {
|
|
232
|
+
name: 'tool',
|
|
233
|
+
checkCommand: 'tool --version',
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
mockExecPromise.mockRejectedValue('string error');
|
|
237
|
+
|
|
238
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
239
|
+
|
|
240
|
+
expect(result.installed).toBe(false);
|
|
241
|
+
expect(result.error).toBe('Unknown error');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should include install instructions on macOS', async () => {
|
|
245
|
+
const originalPlatform = process.platform;
|
|
246
|
+
Object.defineProperty(process, 'platform', {
|
|
247
|
+
value: 'darwin',
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const mockDependency: SystemDependency = {
|
|
251
|
+
name: 'brew',
|
|
252
|
+
checkCommand: 'brew --version',
|
|
253
|
+
installInstructions: {
|
|
254
|
+
macos: 'Install via Homebrew',
|
|
255
|
+
linux: 'Use apt-get',
|
|
256
|
+
manual: 'Download from website',
|
|
257
|
+
},
|
|
258
|
+
} as any;
|
|
259
|
+
|
|
260
|
+
mockExecPromise.mockRejectedValue(new Error('not found'));
|
|
261
|
+
|
|
262
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
263
|
+
|
|
264
|
+
expect(result.installInstructions).toEqual({
|
|
265
|
+
current: 'Install via Homebrew',
|
|
266
|
+
manual: 'Download from website',
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
Object.defineProperty(process, 'platform', {
|
|
270
|
+
value: originalPlatform,
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should include install instructions on Linux', async () => {
|
|
275
|
+
const originalPlatform = process.platform;
|
|
276
|
+
Object.defineProperty(process, 'platform', {
|
|
277
|
+
value: 'linux',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const mockDependency: SystemDependency = {
|
|
281
|
+
name: 'tool',
|
|
282
|
+
checkCommand: 'tool --version',
|
|
283
|
+
installInstructions: {
|
|
284
|
+
linux_debian: 'apt-get install tool',
|
|
285
|
+
manual: 'Manual install',
|
|
286
|
+
},
|
|
287
|
+
} as any;
|
|
288
|
+
|
|
289
|
+
mockExecPromise.mockRejectedValue(new Error('not found'));
|
|
290
|
+
|
|
291
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
292
|
+
|
|
293
|
+
expect(result.installInstructions).toEqual({
|
|
294
|
+
current: 'apt-get install tool',
|
|
295
|
+
manual: 'Manual install',
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
Object.defineProperty(process, 'platform', {
|
|
299
|
+
value: originalPlatform,
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should fallback to linux instruction when linux_debian is not available', async () => {
|
|
304
|
+
const originalPlatform = process.platform;
|
|
305
|
+
Object.defineProperty(process, 'platform', {
|
|
306
|
+
value: 'linux',
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const mockDependency: SystemDependency = {
|
|
310
|
+
name: 'tool',
|
|
311
|
+
checkCommand: 'tool --version',
|
|
312
|
+
installInstructions: {
|
|
313
|
+
linux: 'Generic linux install',
|
|
314
|
+
manual: 'Manual install',
|
|
315
|
+
},
|
|
316
|
+
} as any;
|
|
317
|
+
|
|
318
|
+
mockExecPromise.mockRejectedValue(new Error('not found'));
|
|
319
|
+
|
|
320
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
321
|
+
|
|
322
|
+
expect(result.installInstructions?.current).toBe('Generic linux install');
|
|
323
|
+
|
|
324
|
+
Object.defineProperty(process, 'platform', {
|
|
325
|
+
value: originalPlatform,
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should include install instructions on Windows', async () => {
|
|
330
|
+
const originalPlatform = process.platform;
|
|
331
|
+
Object.defineProperty(process, 'platform', {
|
|
332
|
+
value: 'win32',
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const mockDependency: SystemDependency = {
|
|
336
|
+
name: 'tool',
|
|
337
|
+
checkCommand: 'tool --version',
|
|
338
|
+
installInstructions: {
|
|
339
|
+
windows: 'Install via Chocolatey',
|
|
340
|
+
manual: 'Download from website',
|
|
341
|
+
},
|
|
342
|
+
} as any;
|
|
343
|
+
|
|
344
|
+
mockExecPromise.mockRejectedValue(new Error('not found'));
|
|
345
|
+
|
|
346
|
+
const result = await mcpSystemDepsCheckService.checkSystemDependency(mockDependency);
|
|
347
|
+
|
|
348
|
+
expect(result.installInstructions).toEqual({
|
|
349
|
+
current: 'Install via Chocolatey',
|
|
350
|
+
manual: 'Download from website',
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
Object.defineProperty(process, 'platform', {
|
|
354
|
+
value: originalPlatform,
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('checkDeployOption', () => {
|
|
360
|
+
it('should use installation checker when available', async () => {
|
|
361
|
+
const mockChecker: InstallationChecker = {
|
|
362
|
+
checkPackageInstalled: vi.fn().mockResolvedValue({
|
|
363
|
+
installed: true,
|
|
364
|
+
packageName: 'test-package',
|
|
365
|
+
}),
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
mcpSystemDepsCheckService.registerChecker('npm', mockChecker);
|
|
369
|
+
|
|
370
|
+
const mockOption: DeploymentOption = {
|
|
371
|
+
installationMethod: 'npm',
|
|
372
|
+
installationDetails: { packageName: 'test-package' },
|
|
373
|
+
connection: {
|
|
374
|
+
command: 'node',
|
|
375
|
+
args: ['index.js'],
|
|
376
|
+
},
|
|
377
|
+
} as any;
|
|
378
|
+
|
|
379
|
+
const result = await mcpSystemDepsCheckService.checkDeployOption(mockOption);
|
|
380
|
+
|
|
381
|
+
expect(mockChecker.checkPackageInstalled).toHaveBeenCalledWith({
|
|
382
|
+
packageName: 'test-package',
|
|
383
|
+
});
|
|
384
|
+
expect(result.packageInstalled).toBe(true);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should set connection type to http when url is provided', async () => {
|
|
388
|
+
const mockOption: DeploymentOption = {
|
|
389
|
+
installationMethod: 'manual',
|
|
390
|
+
installationDetails: {},
|
|
391
|
+
connection: {
|
|
392
|
+
url: 'http://localhost:3000',
|
|
393
|
+
},
|
|
394
|
+
} as any;
|
|
395
|
+
|
|
396
|
+
const result = await mcpSystemDepsCheckService.checkDeployOption(mockOption);
|
|
397
|
+
|
|
398
|
+
expect(result.connection.type).toBe('http');
|
|
399
|
+
expect(result.connection.url).toBe('http://localhost:3000');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should detect configuration requirements from required array', async () => {
|
|
403
|
+
const mockOption: DeploymentOption = {
|
|
404
|
+
installationMethod: 'npm',
|
|
405
|
+
installationDetails: { packageName: 'test-package' },
|
|
406
|
+
connection: {
|
|
407
|
+
command: 'node',
|
|
408
|
+
args: ['index.js'],
|
|
409
|
+
configSchema: {
|
|
410
|
+
type: 'object',
|
|
411
|
+
required: ['apiKey'],
|
|
412
|
+
properties: {
|
|
413
|
+
apiKey: { type: 'string' },
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
} as any;
|
|
418
|
+
|
|
419
|
+
const result = await mcpSystemDepsCheckService.checkDeployOption(mockOption);
|
|
420
|
+
|
|
421
|
+
expect(result.needsConfig).toBe(true);
|
|
422
|
+
expect(result.configSchema).toBeDefined();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should detect configuration requirements from property-level required flag', async () => {
|
|
426
|
+
const mockOption: DeploymentOption = {
|
|
427
|
+
installationMethod: 'npm',
|
|
428
|
+
installationDetails: { packageName: 'test-package' },
|
|
429
|
+
connection: {
|
|
430
|
+
command: 'node',
|
|
431
|
+
args: ['index.js'],
|
|
432
|
+
configSchema: {
|
|
433
|
+
type: 'object',
|
|
434
|
+
properties: {
|
|
435
|
+
apiKey: { type: 'string', required: true },
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
} as any;
|
|
440
|
+
|
|
441
|
+
const result = await mcpSystemDepsCheckService.checkDeployOption(mockOption);
|
|
442
|
+
|
|
443
|
+
expect(result.needsConfig).toBe(true);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('should not require config when schema has no required fields', async () => {
|
|
447
|
+
const mockOption: DeploymentOption = {
|
|
448
|
+
installationMethod: 'npm',
|
|
449
|
+
installationDetails: { packageName: 'test-package' },
|
|
450
|
+
connection: {
|
|
451
|
+
command: 'node',
|
|
452
|
+
args: ['index.js'],
|
|
453
|
+
configSchema: {
|
|
454
|
+
type: 'object',
|
|
455
|
+
properties: {
|
|
456
|
+
optional: { type: 'string' },
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
} as any;
|
|
461
|
+
|
|
462
|
+
const result = await mcpSystemDepsCheckService.checkDeployOption(mockOption);
|
|
463
|
+
|
|
464
|
+
expect(result.needsConfig).toBe(false);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should not require config when schema has empty required array', async () => {
|
|
468
|
+
const mockOption: DeploymentOption = {
|
|
469
|
+
installationMethod: 'npm',
|
|
470
|
+
installationDetails: { packageName: 'test-package' },
|
|
471
|
+
connection: {
|
|
472
|
+
command: 'node',
|
|
473
|
+
args: ['index.js'],
|
|
474
|
+
configSchema: {
|
|
475
|
+
type: 'object',
|
|
476
|
+
required: [],
|
|
477
|
+
properties: {
|
|
478
|
+
optional: { type: 'string' },
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
} as any;
|
|
483
|
+
|
|
484
|
+
const result = await mcpSystemDepsCheckService.checkDeployOption(mockOption);
|
|
485
|
+
|
|
486
|
+
expect(result.needsConfig).toBe(false);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should include isRecommended flag from deployment option', async () => {
|
|
490
|
+
const mockOption: DeploymentOption = {
|
|
491
|
+
installationMethod: 'npm',
|
|
492
|
+
installationDetails: { packageName: 'test-package' },
|
|
493
|
+
isRecommended: true,
|
|
494
|
+
connection: {
|
|
495
|
+
command: 'node',
|
|
496
|
+
args: ['index.js'],
|
|
497
|
+
},
|
|
498
|
+
} as any;
|
|
499
|
+
|
|
500
|
+
const result = await mcpSystemDepsCheckService.checkDeployOption(mockOption);
|
|
501
|
+
|
|
502
|
+
expect(result.isRecommended).toBe(true);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('should handle multiple system dependencies', async () => {
|
|
506
|
+
const mockOption: DeploymentOption = {
|
|
507
|
+
installationMethod: 'npm',
|
|
508
|
+
installationDetails: { packageName: 'test-package' },
|
|
509
|
+
systemDependencies: [
|
|
510
|
+
{
|
|
511
|
+
name: 'node',
|
|
512
|
+
checkCommand: 'node --version',
|
|
513
|
+
requiredVersion: '>=18.0.0',
|
|
514
|
+
versionParsingRequired: true,
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: 'python',
|
|
518
|
+
checkCommand: 'python --version',
|
|
519
|
+
requiredVersion: '>=3.0.0',
|
|
520
|
+
versionParsingRequired: true,
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
connection: {
|
|
524
|
+
command: 'node',
|
|
525
|
+
args: ['index.js'],
|
|
526
|
+
},
|
|
527
|
+
} as any;
|
|
528
|
+
|
|
529
|
+
mockExecPromise
|
|
530
|
+
.mockResolvedValueOnce({ stdout: 'v20.1.0\n', stderr: '' })
|
|
531
|
+
.mockResolvedValueOnce({ stdout: 'Python 3.10.5\n', stderr: '' });
|
|
532
|
+
|
|
533
|
+
const result = await mcpSystemDepsCheckService.checkDeployOption(mockOption);
|
|
534
|
+
|
|
535
|
+
expect(result.systemDependencies).toHaveLength(2);
|
|
536
|
+
expect(result.allDependenciesMet).toBe(true);
|
|
537
|
+
expect(result.systemDependencies[0]!.name).toBe('node');
|
|
538
|
+
expect(result.systemDependencies[1]!.name).toBe('python');
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
});
|