@aaarc/handfree-ssh-mcp 1.0.0 → 1.0.1
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/README.md +30 -30
- package/build/config/config-loader.js +8 -0
- package/build/config/server.js +4 -4
- package/build/core/mcp-server.js +0 -5
- package/build/services/ssh-connection-manager.js +64 -159
- package/build/tools/execute-command.js +1 -1
- package/build/tools/help.js +4 -4
- package/build/tools/show-whitelist.js +67 -42
- package/package.json +15 -6
- package/build/tests/command-validation.test.js +0 -876
- package/build/tests/config.test.js +0 -617
- package/build/tests/integration.test.js +0 -348
- package/build/tests/output-collector.test.js +0 -70
- package/build/tests/output-log-writer.test.js +0 -205
- package/build/tests/recent-fixes.test.js +0 -229
- package/build/tests/tool-error.test.js +0 -26
- package/build/tests/tools.test.js +0 -486
|
@@ -1,486 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tools Unit Tests
|
|
3
|
-
*
|
|
4
|
-
* Tests for list-servers, upload, download, show-whitelist tools
|
|
5
|
-
* Uses mock SSHConnectionManager to avoid actual SSH connections.
|
|
6
|
-
*/
|
|
7
|
-
import { describe, it, beforeEach } from "node:test";
|
|
8
|
-
import assert from "node:assert";
|
|
9
|
-
/**
|
|
10
|
-
* Mock SSHConnectionManager for testing tools without real SSH connections
|
|
11
|
-
*/
|
|
12
|
-
class MockSSHConnectionManager {
|
|
13
|
-
configs = {};
|
|
14
|
-
enabledServers = [];
|
|
15
|
-
defaultName = "default";
|
|
16
|
-
setConfig(configs, enabledServers) {
|
|
17
|
-
this.configs = configs;
|
|
18
|
-
this.enabledServers = enabledServers || Object.keys(configs);
|
|
19
|
-
if (this.enabledServers.length > 0) {
|
|
20
|
-
this.defaultName = this.enabledServers[0];
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
getServerConfig(name) {
|
|
24
|
-
const serverName = name || this.defaultName;
|
|
25
|
-
if (!this.enabledServers.includes(serverName)) {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
return this.configs[serverName] || null;
|
|
29
|
-
}
|
|
30
|
-
getAllServerInfos() {
|
|
31
|
-
return this.enabledServers.map(name => {
|
|
32
|
-
const config = this.configs[name];
|
|
33
|
-
return {
|
|
34
|
-
name,
|
|
35
|
-
host: config?.host || "unknown",
|
|
36
|
-
port: config?.port || 22,
|
|
37
|
-
username: config?.username || "unknown",
|
|
38
|
-
connected: false,
|
|
39
|
-
};
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
async upload(localPath, remotePath, connectionName) {
|
|
43
|
-
const serverName = connectionName || this.defaultName;
|
|
44
|
-
if (!this.enabledServers.includes(serverName)) {
|
|
45
|
-
throw new Error(`Server '${serverName}' not enabled`);
|
|
46
|
-
}
|
|
47
|
-
// Mock implementation - just return success message
|
|
48
|
-
return `Uploaded ${localPath} to ${remotePath} on ${serverName}`;
|
|
49
|
-
}
|
|
50
|
-
async download(remotePath, localPath, connectionName) {
|
|
51
|
-
const serverName = connectionName || this.defaultName;
|
|
52
|
-
if (!this.enabledServers.includes(serverName)) {
|
|
53
|
-
throw new Error(`Server '${serverName}' not enabled`);
|
|
54
|
-
}
|
|
55
|
-
// Mock implementation - just return success message
|
|
56
|
-
return `Downloaded ${remotePath} to ${localPath} from ${serverName}`;
|
|
57
|
-
}
|
|
58
|
-
async executeCommand(command, connectionName, options) {
|
|
59
|
-
const serverName = connectionName || this.defaultName;
|
|
60
|
-
if (!this.enabledServers.includes(serverName)) {
|
|
61
|
-
throw new Error(`Server '${serverName}' not enabled`);
|
|
62
|
-
}
|
|
63
|
-
// Mock implementation - simulate command execution
|
|
64
|
-
if (command === "pwd")
|
|
65
|
-
return "/home/testuser";
|
|
66
|
-
if (command.startsWith("ls"))
|
|
67
|
-
return "file1.txt\nfile2.txt\ndir1";
|
|
68
|
-
if (command === "hostname")
|
|
69
|
-
return "test-server";
|
|
70
|
-
if (command === "whoami")
|
|
71
|
-
return "testuser";
|
|
72
|
-
if (command === "date")
|
|
73
|
-
return "Sat Jan 24 12:00:00 UTC 2026";
|
|
74
|
-
return `Executed: ${command}`;
|
|
75
|
-
}
|
|
76
|
-
async executeCommandWithProgress(command, connectionName, options) {
|
|
77
|
-
const serverName = connectionName || this.defaultName;
|
|
78
|
-
if (!this.enabledServers.includes(serverName)) {
|
|
79
|
-
throw new Error(`Server '${serverName}' not enabled`);
|
|
80
|
-
}
|
|
81
|
-
// Simulate streaming output
|
|
82
|
-
if (options?.onProgress) {
|
|
83
|
-
options.onProgress("line 1\n");
|
|
84
|
-
options.onProgress("line 2\n");
|
|
85
|
-
options.onProgress("line 3\n");
|
|
86
|
-
}
|
|
87
|
-
return "line 1\nline 2\nline 3\n";
|
|
88
|
-
}
|
|
89
|
-
disconnect() {
|
|
90
|
-
// No-op for mock
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
// ============================================
|
|
94
|
-
// list-servers Tool Tests
|
|
95
|
-
// ============================================
|
|
96
|
-
describe("list-servers Tool", () => {
|
|
97
|
-
let mockManager;
|
|
98
|
-
beforeEach(() => {
|
|
99
|
-
mockManager = new MockSSHConnectionManager();
|
|
100
|
-
});
|
|
101
|
-
it("should return empty array when no servers configured", () => {
|
|
102
|
-
mockManager.setConfig({}, []);
|
|
103
|
-
const result = mockManager.getAllServerInfos();
|
|
104
|
-
assert.deepStrictEqual(result, []);
|
|
105
|
-
});
|
|
106
|
-
it("should return single server info", () => {
|
|
107
|
-
mockManager.setConfig({
|
|
108
|
-
dev: {
|
|
109
|
-
host: "192.168.1.1",
|
|
110
|
-
port: 22,
|
|
111
|
-
username: "testuser",
|
|
112
|
-
password: "testpass",
|
|
113
|
-
}
|
|
114
|
-
}, ["dev"]);
|
|
115
|
-
const result = mockManager.getAllServerInfos();
|
|
116
|
-
assert.strictEqual(result.length, 1);
|
|
117
|
-
assert.strictEqual(result[0].name, "dev");
|
|
118
|
-
assert.strictEqual(result[0].host, "192.168.1.1");
|
|
119
|
-
assert.strictEqual(result[0].port, 22);
|
|
120
|
-
assert.strictEqual(result[0].username, "testuser");
|
|
121
|
-
assert.strictEqual(result[0].connected, false);
|
|
122
|
-
});
|
|
123
|
-
it("should return multiple servers info", () => {
|
|
124
|
-
mockManager.setConfig({
|
|
125
|
-
dev: { host: "10.0.0.1", port: 22, username: "dev" },
|
|
126
|
-
prod: { host: "10.0.0.2", port: 2222, username: "prod" },
|
|
127
|
-
staging: { host: "10.0.0.3", port: 22, username: "staging" },
|
|
128
|
-
}, ["dev", "prod", "staging"]);
|
|
129
|
-
const result = mockManager.getAllServerInfos();
|
|
130
|
-
assert.strictEqual(result.length, 3);
|
|
131
|
-
assert.strictEqual(result[0].name, "dev");
|
|
132
|
-
assert.strictEqual(result[1].name, "prod");
|
|
133
|
-
assert.strictEqual(result[2].name, "staging");
|
|
134
|
-
assert.strictEqual(result[1].port, 2222);
|
|
135
|
-
});
|
|
136
|
-
it("should only return enabled servers", () => {
|
|
137
|
-
mockManager.setConfig({
|
|
138
|
-
dev: { host: "10.0.0.1", port: 22, username: "dev" },
|
|
139
|
-
prod: { host: "10.0.0.2", port: 22, username: "prod" },
|
|
140
|
-
staging: { host: "10.0.0.3", port: 22, username: "staging" },
|
|
141
|
-
}, ["dev", "staging"]); // prod not enabled
|
|
142
|
-
const result = mockManager.getAllServerInfos();
|
|
143
|
-
assert.strictEqual(result.length, 2);
|
|
144
|
-
const names = result.map(s => s.name);
|
|
145
|
-
assert.ok(names.includes("dev"));
|
|
146
|
-
assert.ok(names.includes("staging"));
|
|
147
|
-
assert.ok(!names.includes("prod"));
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
// ============================================
|
|
151
|
-
// show-whitelist Tool Tests
|
|
152
|
-
// ============================================
|
|
153
|
-
describe("show-whitelist Tool", () => {
|
|
154
|
-
let mockManager;
|
|
155
|
-
beforeEach(() => {
|
|
156
|
-
mockManager = new MockSSHConnectionManager();
|
|
157
|
-
});
|
|
158
|
-
it("should return null for non-existent server", () => {
|
|
159
|
-
mockManager.setConfig({
|
|
160
|
-
dev: { host: "192.168.1.1", username: "test", password: "test" }
|
|
161
|
-
}, ["dev"]);
|
|
162
|
-
const result = mockManager.getServerConfig("nonexistent");
|
|
163
|
-
assert.strictEqual(result, null);
|
|
164
|
-
});
|
|
165
|
-
it("should return config for valid server", () => {
|
|
166
|
-
const devConfig = {
|
|
167
|
-
host: "192.168.1.1",
|
|
168
|
-
port: 22,
|
|
169
|
-
username: "testuser",
|
|
170
|
-
password: "testpass",
|
|
171
|
-
commandWhitelist: ["^ls.*$", "^pwd$"],
|
|
172
|
-
};
|
|
173
|
-
mockManager.setConfig({ dev: devConfig }, ["dev"]);
|
|
174
|
-
const result = mockManager.getServerConfig("dev");
|
|
175
|
-
assert.ok(result);
|
|
176
|
-
assert.strictEqual(result.host, "192.168.1.1");
|
|
177
|
-
assert.deepStrictEqual(result.commandWhitelist, ["^ls.*$", "^pwd$"]);
|
|
178
|
-
});
|
|
179
|
-
it("should return default server config when name not specified", () => {
|
|
180
|
-
const devConfig = {
|
|
181
|
-
host: "192.168.1.1",
|
|
182
|
-
username: "dev",
|
|
183
|
-
password: "test",
|
|
184
|
-
};
|
|
185
|
-
mockManager.setConfig({ dev: devConfig }, ["dev"]);
|
|
186
|
-
const result = mockManager.getServerConfig(); // No name - uses default
|
|
187
|
-
assert.ok(result);
|
|
188
|
-
assert.strictEqual(result.host, "192.168.1.1");
|
|
189
|
-
});
|
|
190
|
-
it("should return null for disabled server", () => {
|
|
191
|
-
mockManager.setConfig({
|
|
192
|
-
dev: { host: "10.0.0.1", username: "dev", password: "test" },
|
|
193
|
-
prod: { host: "10.0.0.2", username: "prod", password: "test" },
|
|
194
|
-
}, ["dev"]); // Only dev enabled
|
|
195
|
-
const result = mockManager.getServerConfig("prod");
|
|
196
|
-
assert.strictEqual(result, null);
|
|
197
|
-
});
|
|
198
|
-
it("should return config with blacklist", () => {
|
|
199
|
-
const devConfig = {
|
|
200
|
-
host: "192.168.1.1",
|
|
201
|
-
username: "test",
|
|
202
|
-
password: "test",
|
|
203
|
-
commandWhitelist: ["^.*$"],
|
|
204
|
-
commandBlacklist: ["^rm.*$", "^shutdown.*$"],
|
|
205
|
-
};
|
|
206
|
-
mockManager.setConfig({ dev: devConfig }, ["dev"]);
|
|
207
|
-
const result = mockManager.getServerConfig("dev");
|
|
208
|
-
assert.ok(result);
|
|
209
|
-
assert.deepStrictEqual(result.commandBlacklist, ["^rm.*$", "^shutdown.*$"]);
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
// ============================================
|
|
213
|
-
// upload Tool Tests
|
|
214
|
-
// ============================================
|
|
215
|
-
describe("upload Tool", () => {
|
|
216
|
-
let mockManager;
|
|
217
|
-
beforeEach(() => {
|
|
218
|
-
mockManager = new MockSSHConnectionManager();
|
|
219
|
-
mockManager.setConfig({
|
|
220
|
-
dev: { host: "192.168.1.1", username: "test", password: "test" }
|
|
221
|
-
}, ["dev"]);
|
|
222
|
-
});
|
|
223
|
-
it("should upload file to default server", async () => {
|
|
224
|
-
const result = await mockManager.upload("/local/file.txt", "/remote/file.txt");
|
|
225
|
-
assert.ok(result.includes("Uploaded"));
|
|
226
|
-
assert.ok(result.includes("/local/file.txt"));
|
|
227
|
-
assert.ok(result.includes("/remote/file.txt"));
|
|
228
|
-
});
|
|
229
|
-
it("should upload file to specified server", async () => {
|
|
230
|
-
const result = await mockManager.upload("/local/file.txt", "/remote/file.txt", "dev");
|
|
231
|
-
assert.ok(result.includes("dev"));
|
|
232
|
-
});
|
|
233
|
-
it("should throw error for disabled server", async () => {
|
|
234
|
-
mockManager.setConfig({
|
|
235
|
-
dev: { host: "192.168.1.1", username: "test", password: "test" },
|
|
236
|
-
prod: { host: "192.168.1.2", username: "test", password: "test" },
|
|
237
|
-
}, ["dev"]); // Only dev enabled
|
|
238
|
-
await assert.rejects(() => mockManager.upload("/local/file.txt", "/remote/file.txt", "prod"), /not enabled/);
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
// ============================================
|
|
242
|
-
// download Tool Tests
|
|
243
|
-
// ============================================
|
|
244
|
-
describe("download Tool", () => {
|
|
245
|
-
let mockManager;
|
|
246
|
-
beforeEach(() => {
|
|
247
|
-
mockManager = new MockSSHConnectionManager();
|
|
248
|
-
mockManager.setConfig({
|
|
249
|
-
dev: { host: "192.168.1.1", username: "test", password: "test" }
|
|
250
|
-
}, ["dev"]);
|
|
251
|
-
});
|
|
252
|
-
it("should download file from default server", async () => {
|
|
253
|
-
const result = await mockManager.download("/remote/file.txt", "/local/file.txt");
|
|
254
|
-
assert.ok(result.includes("Downloaded"));
|
|
255
|
-
assert.ok(result.includes("/remote/file.txt"));
|
|
256
|
-
assert.ok(result.includes("/local/file.txt"));
|
|
257
|
-
});
|
|
258
|
-
it("should download file from specified server", async () => {
|
|
259
|
-
const result = await mockManager.download("/remote/file.txt", "/local/file.txt", "dev");
|
|
260
|
-
assert.ok(result.includes("dev"));
|
|
261
|
-
});
|
|
262
|
-
it("should throw error for disabled server", async () => {
|
|
263
|
-
mockManager.setConfig({
|
|
264
|
-
dev: { host: "192.168.1.1", username: "test", password: "test" },
|
|
265
|
-
prod: { host: "192.168.1.2", username: "test", password: "test" },
|
|
266
|
-
}, ["dev"]); // Only dev enabled
|
|
267
|
-
await assert.rejects(() => mockManager.download("/remote/file.txt", "/local/file.txt", "prod"), /not enabled/);
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
// ============================================
|
|
271
|
-
// execute-command Streaming Mode Tests
|
|
272
|
-
// ============================================
|
|
273
|
-
describe("execute-command Streaming Mode", () => {
|
|
274
|
-
let mockManager;
|
|
275
|
-
beforeEach(() => {
|
|
276
|
-
mockManager = new MockSSHConnectionManager();
|
|
277
|
-
mockManager.setConfig({
|
|
278
|
-
dev: { host: "192.168.1.1", username: "test", password: "test" }
|
|
279
|
-
}, ["dev"]);
|
|
280
|
-
});
|
|
281
|
-
it("should execute command without streaming", async () => {
|
|
282
|
-
const result = await mockManager.executeCommand("pwd", "dev");
|
|
283
|
-
assert.strictEqual(result, "/home/testuser");
|
|
284
|
-
});
|
|
285
|
-
it("should execute command with streaming and receive chunks", async () => {
|
|
286
|
-
const chunks = [];
|
|
287
|
-
const onProgress = (chunk) => chunks.push(chunk);
|
|
288
|
-
const result = await mockManager.executeCommandWithProgress("ls -la", "dev", {
|
|
289
|
-
onProgress,
|
|
290
|
-
});
|
|
291
|
-
assert.ok(chunks.length > 0);
|
|
292
|
-
assert.ok(result.includes("line 1"));
|
|
293
|
-
assert.ok(result.includes("line 2"));
|
|
294
|
-
assert.ok(result.includes("line 3"));
|
|
295
|
-
});
|
|
296
|
-
it("should handle custom timeout", async () => {
|
|
297
|
-
const result = await mockManager.executeCommandWithProgress("long-command", "dev", {
|
|
298
|
-
timeout: 60000,
|
|
299
|
-
});
|
|
300
|
-
assert.ok(result);
|
|
301
|
-
});
|
|
302
|
-
it("should throw error for disabled server in streaming mode", async () => {
|
|
303
|
-
mockManager.setConfig({
|
|
304
|
-
dev: { host: "192.168.1.1", username: "test", password: "test" },
|
|
305
|
-
prod: { host: "192.168.1.2", username: "test", password: "test" },
|
|
306
|
-
}, ["dev"]);
|
|
307
|
-
await assert.rejects(() => mockManager.executeCommandWithProgress("pwd", "prod"), /not enabled/);
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
// ============================================
|
|
311
|
-
// Timeout Behavior Tests (Logic Simulation)
|
|
312
|
-
// ============================================
|
|
313
|
-
/**
|
|
314
|
-
* Helper to calculate effective timeout based on stream mode
|
|
315
|
-
* Matches the logic in execute-command.ts
|
|
316
|
-
*/
|
|
317
|
-
function calculateTimeout(stream, timeout) {
|
|
318
|
-
const useStream = stream !== false;
|
|
319
|
-
if (useStream) {
|
|
320
|
-
return timeout || 300000; // 5 min default for streaming
|
|
321
|
-
}
|
|
322
|
-
else {
|
|
323
|
-
return timeout || 30000; // 30s default for non-streaming
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
describe("Timeout Behavior", () => {
|
|
327
|
-
it("should use default 30s timeout for non-streaming mode", () => {
|
|
328
|
-
const effectiveTimeout = calculateTimeout(false, undefined);
|
|
329
|
-
assert.strictEqual(effectiveTimeout, 30000);
|
|
330
|
-
});
|
|
331
|
-
it("should use default 300s timeout for streaming mode", () => {
|
|
332
|
-
const effectiveTimeout = calculateTimeout(true, undefined);
|
|
333
|
-
assert.strictEqual(effectiveTimeout, 300000);
|
|
334
|
-
});
|
|
335
|
-
it("should use custom timeout when specified in streaming mode", () => {
|
|
336
|
-
const effectiveTimeout = calculateTimeout(true, 60000);
|
|
337
|
-
assert.strictEqual(effectiveTimeout, 60000);
|
|
338
|
-
});
|
|
339
|
-
it("should use custom timeout when specified in non-streaming mode", () => {
|
|
340
|
-
const effectiveTimeout = calculateTimeout(false, 5000);
|
|
341
|
-
assert.strictEqual(effectiveTimeout, 5000);
|
|
342
|
-
});
|
|
343
|
-
it("should default stream to true when not specified (undefined)", () => {
|
|
344
|
-
const effectiveTimeout = calculateTimeout(undefined, undefined);
|
|
345
|
-
// undefined !== false is true, so streaming mode is used
|
|
346
|
-
assert.strictEqual(effectiveTimeout, 300000);
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
// ============================================
|
|
350
|
-
// Pattern Helper Function Tests (show-whitelist)
|
|
351
|
-
// ============================================
|
|
352
|
-
describe("patternToReadable (show-whitelist helper)", () => {
|
|
353
|
-
function patternToReadable(pattern) {
|
|
354
|
-
let readable = pattern;
|
|
355
|
-
readable = readable.replace(/^\^/, "").replace(/\$$/, "");
|
|
356
|
-
if (readable === "ls( .*)?")
|
|
357
|
-
return "ls, ls -la, ls /path, etc.";
|
|
358
|
-
if (readable === "cat .*")
|
|
359
|
-
return "cat <file>";
|
|
360
|
-
if (readable === "pwd")
|
|
361
|
-
return "pwd (current directory)";
|
|
362
|
-
if (readable === "whoami")
|
|
363
|
-
return "whoami (current user)";
|
|
364
|
-
if (readable === "hostname")
|
|
365
|
-
return "hostname";
|
|
366
|
-
if (readable === "date")
|
|
367
|
-
return "date (current time)";
|
|
368
|
-
if (readable === "docker ps.*")
|
|
369
|
-
return "docker ps, docker ps -a, etc.";
|
|
370
|
-
if (readable === "docker logs.*")
|
|
371
|
-
return "docker logs <container>";
|
|
372
|
-
if (readable === "git .*")
|
|
373
|
-
return "git <any subcommand>";
|
|
374
|
-
if (readable.includes(".*"))
|
|
375
|
-
return readable.replace(/\.\*/g, "<any>");
|
|
376
|
-
if (readable.includes(".+"))
|
|
377
|
-
return readable.replace(/\.\+/g, "<required>");
|
|
378
|
-
return readable;
|
|
379
|
-
}
|
|
380
|
-
it("should convert ls pattern to readable", () => {
|
|
381
|
-
assert.strictEqual(patternToReadable("^ls( .*)?$"), "ls, ls -la, ls /path, etc.");
|
|
382
|
-
});
|
|
383
|
-
it("should convert cat pattern to readable", () => {
|
|
384
|
-
assert.strictEqual(patternToReadable("^cat .*$"), "cat <file>");
|
|
385
|
-
});
|
|
386
|
-
it("should convert pwd pattern to readable", () => {
|
|
387
|
-
assert.strictEqual(patternToReadable("^pwd$"), "pwd (current directory)");
|
|
388
|
-
});
|
|
389
|
-
it("should convert docker ps pattern to readable", () => {
|
|
390
|
-
assert.strictEqual(patternToReadable("^docker ps.*$"), "docker ps, docker ps -a, etc.");
|
|
391
|
-
});
|
|
392
|
-
it("should convert git pattern to readable", () => {
|
|
393
|
-
assert.strictEqual(patternToReadable("^git .*$"), "git <any subcommand>");
|
|
394
|
-
});
|
|
395
|
-
it("should replace .* with <any> for unknown patterns", () => {
|
|
396
|
-
assert.strictEqual(patternToReadable("^custom .*$"), "custom <any>");
|
|
397
|
-
});
|
|
398
|
-
it("should replace .+ with <required> for unknown patterns", () => {
|
|
399
|
-
assert.strictEqual(patternToReadable("^custom .+$"), "custom <required>");
|
|
400
|
-
});
|
|
401
|
-
});
|
|
402
|
-
// ============================================
|
|
403
|
-
// Example Generation Tests (show-whitelist)
|
|
404
|
-
// ============================================
|
|
405
|
-
describe("generateExamples (show-whitelist helper)", () => {
|
|
406
|
-
function generateExamples(whitelist) {
|
|
407
|
-
const examples = [];
|
|
408
|
-
for (const pattern of whitelist) {
|
|
409
|
-
if (pattern.includes("^ls"))
|
|
410
|
-
examples.push("ls -la");
|
|
411
|
-
if (pattern.includes("^cat"))
|
|
412
|
-
examples.push("cat /etc/hostname");
|
|
413
|
-
if (pattern.includes("^pwd"))
|
|
414
|
-
examples.push("pwd");
|
|
415
|
-
if (pattern.includes("^whoami"))
|
|
416
|
-
examples.push("whoami");
|
|
417
|
-
if (pattern.includes("^hostname"))
|
|
418
|
-
examples.push("hostname");
|
|
419
|
-
if (pattern.includes("^date"))
|
|
420
|
-
examples.push("date");
|
|
421
|
-
if (pattern.includes("^echo"))
|
|
422
|
-
examples.push("echo 'hello world'");
|
|
423
|
-
if (pattern.includes("^docker ps"))
|
|
424
|
-
examples.push("docker ps -a");
|
|
425
|
-
if (pattern.includes("^docker logs"))
|
|
426
|
-
examples.push("docker logs --tail 50 <container>");
|
|
427
|
-
if (pattern.includes("^git"))
|
|
428
|
-
examples.push("git status");
|
|
429
|
-
}
|
|
430
|
-
return [...new Set(examples)];
|
|
431
|
-
}
|
|
432
|
-
it("should generate ls example", () => {
|
|
433
|
-
const examples = generateExamples(["^ls( .*)?$"]);
|
|
434
|
-
assert.ok(examples.includes("ls -la"));
|
|
435
|
-
});
|
|
436
|
-
it("should generate docker examples", () => {
|
|
437
|
-
const examples = generateExamples(["^docker ps.*$", "^docker logs.*$"]);
|
|
438
|
-
assert.ok(examples.includes("docker ps -a"));
|
|
439
|
-
assert.ok(examples.includes("docker logs --tail 50 <container>"));
|
|
440
|
-
});
|
|
441
|
-
it("should generate multiple examples for mixed whitelist", () => {
|
|
442
|
-
const examples = generateExamples([
|
|
443
|
-
"^ls( .*)?$",
|
|
444
|
-
"^cat .*$",
|
|
445
|
-
"^pwd$",
|
|
446
|
-
"^git .*$",
|
|
447
|
-
]);
|
|
448
|
-
assert.ok(examples.includes("ls -la"));
|
|
449
|
-
assert.ok(examples.includes("cat /etc/hostname"));
|
|
450
|
-
assert.ok(examples.includes("pwd"));
|
|
451
|
-
assert.ok(examples.includes("git status"));
|
|
452
|
-
});
|
|
453
|
-
it("should remove duplicates", () => {
|
|
454
|
-
const examples = generateExamples(["^ls.*$", "^ls -la$"]); // Both match ls
|
|
455
|
-
const lsCount = examples.filter(e => e === "ls -la").length;
|
|
456
|
-
assert.strictEqual(lsCount, 1);
|
|
457
|
-
});
|
|
458
|
-
});
|
|
459
|
-
// ============================================
|
|
460
|
-
// Server Selection Tests
|
|
461
|
-
// ============================================
|
|
462
|
-
describe("Server Selection Logic", () => {
|
|
463
|
-
let mockManager;
|
|
464
|
-
beforeEach(() => {
|
|
465
|
-
mockManager = new MockSSHConnectionManager();
|
|
466
|
-
});
|
|
467
|
-
it("should use first enabled server as default", () => {
|
|
468
|
-
mockManager.setConfig({
|
|
469
|
-
alpha: { host: "10.0.0.1", username: "a", password: "a" },
|
|
470
|
-
beta: { host: "10.0.0.2", username: "b", password: "b" },
|
|
471
|
-
}, ["beta", "alpha"]); // beta is first
|
|
472
|
-
const config = mockManager.getServerConfig();
|
|
473
|
-
assert.ok(config);
|
|
474
|
-
assert.strictEqual(config.host, "10.0.0.2"); // beta's host
|
|
475
|
-
});
|
|
476
|
-
it("should use explicit server name over default", () => {
|
|
477
|
-
mockManager.setConfig({
|
|
478
|
-
dev: { host: "10.0.0.1", username: "dev", password: "dev" },
|
|
479
|
-
prod: { host: "10.0.0.2", username: "prod", password: "prod" },
|
|
480
|
-
}, ["dev", "prod"]);
|
|
481
|
-
const config = mockManager.getServerConfig("prod");
|
|
482
|
-
assert.ok(config);
|
|
483
|
-
assert.strictEqual(config.host, "10.0.0.2");
|
|
484
|
-
});
|
|
485
|
-
});
|
|
486
|
-
console.log("\n🧪 Running tools unit tests...\n");
|