@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.
@@ -1,617 +0,0 @@
1
- /**
2
- * handfree-ssh-mcp Test Suite
3
- *
4
- * Run: npm test
5
- *
6
- * Tests config loading, argument parsing, and tool registration
7
- * without needing an MCP client.
8
- */
9
- import { describe, it, beforeEach, afterEach } from "node:test";
10
- import assert from "node:assert";
11
- import fs from "node:fs";
12
- import path from "node:path";
13
- import os from "node:os";
14
- // Import modules to test
15
- import { getConfigPath, getEnabledServersArg, getLoadUserSshConfigFlag, getNoSshConfigFlag, getSshConfigPathsArg, loadConfigFromSources, loadConfigFromYaml, } from "../config/config-loader.js";
16
- import { loadSshConfigFiles } from "../config/ssh-config-loader.js";
17
- describe("Config Loader", () => {
18
- let tempDir;
19
- let tempConfigPath;
20
- beforeEach(() => {
21
- // Create temp directory for test configs
22
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "handfree-ssh-mcp-test-"));
23
- });
24
- afterEach(() => {
25
- // Cleanup temp files
26
- if (fs.existsSync(tempDir)) {
27
- fs.rmSync(tempDir, { recursive: true, force: true });
28
- }
29
- });
30
- it("should load valid YAML config", () => {
31
- const configContent = `
32
- defaultServer: dev
33
- servers:
34
- dev:
35
- host: 192.168.1.1
36
- port: 22
37
- username: testuser
38
- password: testpass
39
- whitelist:
40
- - "^ls.*$"
41
- - "^pwd$"
42
- `;
43
- tempConfigPath = path.join(tempDir, "servers.yaml");
44
- fs.writeFileSync(tempConfigPath, configContent);
45
- const result = loadConfigFromYaml(tempConfigPath);
46
- assert.ok(result.configs);
47
- assert.ok(result.configs["dev"]);
48
- assert.strictEqual(result.configs["dev"].host, "192.168.1.1");
49
- assert.strictEqual(result.configs["dev"].port, 22);
50
- assert.strictEqual(result.configs["dev"].username, "testuser");
51
- assert.strictEqual(result.configs["dev"].password, "testpass");
52
- assert.deepStrictEqual(result.configs["dev"].commandWhitelist, ["^ls.*$", "^pwd$"]);
53
- });
54
- it("should load multiple servers", () => {
55
- const configContent = `
56
- servers:
57
- dev:
58
- host: 10.0.0.1
59
- username: dev
60
- password: devpass
61
- prod:
62
- host: 10.0.0.2
63
- username: prod
64
- password: prodpass
65
- staging:
66
- host: 10.0.0.3
67
- username: staging
68
- privateKey: ~/.ssh/id_rsa
69
- `;
70
- tempConfigPath = path.join(tempDir, "multi.yaml");
71
- fs.writeFileSync(tempConfigPath, configContent);
72
- const result = loadConfigFromYaml(tempConfigPath);
73
- assert.strictEqual(Object.keys(result.configs).length, 3);
74
- assert.ok(result.configs["dev"]);
75
- assert.ok(result.configs["prod"]);
76
- assert.ok(result.configs["staging"]);
77
- });
78
- it("should throw on missing host", () => {
79
- const configContent = `
80
- servers:
81
- broken:
82
- username: test
83
- password: test
84
- `;
85
- tempConfigPath = path.join(tempDir, "broken.yaml");
86
- fs.writeFileSync(tempConfigPath, configContent);
87
- assert.throws(() => loadConfigFromYaml(tempConfigPath), /host.*required/i);
88
- });
89
- it("should throw on missing username", () => {
90
- const configContent = `
91
- servers:
92
- broken:
93
- host: 192.168.1.1
94
- password: test
95
- `;
96
- tempConfigPath = path.join(tempDir, "broken.yaml");
97
- fs.writeFileSync(tempConfigPath, configContent);
98
- assert.throws(() => loadConfigFromYaml(tempConfigPath), /username.*required/i);
99
- });
100
- it("should throw on missing auth (no password or privateKey)", () => {
101
- const configContent = `
102
- servers:
103
- broken:
104
- host: 192.168.1.1
105
- username: test
106
- `;
107
- tempConfigPath = path.join(tempDir, "broken.yaml");
108
- fs.writeFileSync(tempConfigPath, configContent);
109
- assert.throws(() => loadConfigFromYaml(tempConfigPath), /password.*privateKey.*required/i);
110
- });
111
- it("should expand ~ in privateKey path", () => {
112
- const configContent = `
113
- servers:
114
- dev:
115
- host: 192.168.1.1
116
- username: test
117
- privateKey: ~/.ssh/id_rsa
118
- `;
119
- tempConfigPath = path.join(tempDir, "key.yaml");
120
- fs.writeFileSync(tempConfigPath, configContent);
121
- const result = loadConfigFromYaml(tempConfigPath);
122
- assert.ok(result.configs["dev"].privateKey);
123
- assert.ok(!result.configs["dev"].privateKey.startsWith("~"));
124
- assert.ok(result.configs["dev"].privateKey.includes(os.homedir()));
125
- });
126
- it("should handle preConnect flag", () => {
127
- const configContent = `
128
- preConnect: true
129
- servers:
130
- dev:
131
- host: 192.168.1.1
132
- username: test
133
- password: test
134
- `;
135
- tempConfigPath = path.join(tempDir, "preconnect.yaml");
136
- fs.writeFileSync(tempConfigPath, configContent);
137
- const result = loadConfigFromYaml(tempConfigPath);
138
- assert.strictEqual(result.preConnect, true);
139
- });
140
- it("should throw on non-existent config file", () => {
141
- assert.throws(() => loadConfigFromYaml("/nonexistent/path/servers.yaml"), /not found/i);
142
- });
143
- it("should throw on invalid YAML", () => {
144
- const configContent = `
145
- servers:
146
- dev:
147
- host: 192.168.1.1
148
- username: test
149
- password: test
150
- invalid yaml here [[[
151
- `;
152
- tempConfigPath = path.join(tempDir, "invalid.yaml");
153
- fs.writeFileSync(tempConfigPath, configContent);
154
- assert.throws(() => loadConfigFromYaml(tempConfigPath), /parse|yaml/i);
155
- });
156
- });
157
- describe("Argument Parsing", () => {
158
- it("should extract --config path", () => {
159
- const args = ["--config", "/path/to/servers.yaml", "--enable-servers", "dev"];
160
- const configPath = getConfigPath(args);
161
- assert.strictEqual(configPath, "/path/to/servers.yaml");
162
- });
163
- it("should return null if --config not provided", () => {
164
- const args = ["--enable-servers", "dev"];
165
- const configPath = getConfigPath(args);
166
- assert.strictEqual(configPath, null);
167
- });
168
- it("should extract --enable-servers list", () => {
169
- const args = ["--config", "test.yaml", "--enable-servers", "dev,prod,staging"];
170
- const servers = getEnabledServersArg(args);
171
- assert.deepStrictEqual(servers, ["dev", "prod", "staging"]);
172
- });
173
- it("should return null if --enable-servers not provided", () => {
174
- const args = ["--config", "test.yaml"];
175
- const servers = getEnabledServersArg(args);
176
- assert.strictEqual(servers, null);
177
- });
178
- it("should handle single server in --enable-servers", () => {
179
- const args = ["--enable-servers", "dev"];
180
- const servers = getEnabledServersArg(args);
181
- assert.deepStrictEqual(servers, ["dev"]);
182
- });
183
- it("should trim whitespace from server names", () => {
184
- const args = ["--enable-servers", "dev , prod , staging"];
185
- const servers = getEnabledServersArg(args);
186
- assert.deepStrictEqual(servers, ["dev", "prod", "staging"]);
187
- });
188
- it("should extract repeated --ssh-config paths", () => {
189
- const args = ["--ssh-config", "a,b", "--config", "servers.yaml", "--ssh-config", "c"];
190
- assert.deepStrictEqual(getSshConfigPathsArg(args), ["a", "b", "c"]);
191
- });
192
- it("should detect --no-ssh-config", () => {
193
- assert.strictEqual(getNoSshConfigFlag(["--config", "servers.yaml", "--no-ssh-config"]), true);
194
- assert.strictEqual(getNoSshConfigFlag(["--config", "servers.yaml"]), false);
195
- });
196
- it("should leave OpenSSH config loading to YAML unless CLI disables it", () => {
197
- assert.strictEqual(getLoadUserSshConfigFlag(["--config", "servers.yaml"]), undefined);
198
- assert.strictEqual(getLoadUserSshConfigFlag(["--config", "servers.yaml", "--no-ssh-config"]), false);
199
- });
200
- });
201
- describe("OpenSSH Config Loading", () => {
202
- let tempDir;
203
- beforeEach(() => {
204
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "handfree-ssh-mcp-test-"));
205
- });
206
- afterEach(() => {
207
- if (fs.existsSync(tempDir)) {
208
- fs.rmSync(tempDir, { recursive: true, force: true });
209
- }
210
- });
211
- it("should load concrete Host entries from an OpenSSH config", () => {
212
- const keyPath = path.join(tempDir, "id_ed25519");
213
- const sshConfigPath = path.join(tempDir, "ssh_config");
214
- fs.writeFileSync(keyPath, "fake-key");
215
- fs.writeFileSync(sshConfigPath, `
216
- Host dev
217
- HostName dev.example.com
218
- User deploy
219
- Port 2202
220
- IdentityFile "${keyPath}"
221
-
222
- Host *
223
- User fallback
224
- `);
225
- const result = loadSshConfigFiles([sshConfigPath]);
226
- assert.ok(result.files.includes(fs.realpathSync(sshConfigPath)));
227
- assert.strictEqual(result.configs.dev.host, "dev.example.com");
228
- assert.strictEqual(result.configs.dev.username, "deploy");
229
- assert.strictEqual(result.configs.dev.port, 2202);
230
- assert.strictEqual(result.configs.dev.privateKey, path.normalize(keyPath));
231
- assert.strictEqual(result.configs.dev.authOptional, true);
232
- });
233
- it("should apply wildcard defaults and token replacement", () => {
234
- const sshConfigPath = path.join(tempDir, "ssh_config");
235
- fs.writeFileSync(sshConfigPath, `
236
- Host app
237
- HostName %n.internal
238
-
239
- Host *
240
- User ops
241
- Port 2222
242
- IdentityFile ~/.ssh/%r-%n
243
- `);
244
- const result = loadSshConfigFiles([sshConfigPath]);
245
- assert.strictEqual(result.configs.app.host, "app.internal");
246
- assert.strictEqual(result.configs.app.username, "ops");
247
- assert.strictEqual(result.configs.app.port, 2222);
248
- assert.strictEqual(result.configs.app.privateKey, path.normalize(path.join(os.homedir(), ".ssh", "ops-app")));
249
- });
250
- it("should parse OpenSSH keyword=value syntax", () => {
251
- const keyPath = path.join(tempDir, "id_ed25519");
252
- const sshConfigPath = path.join(tempDir, "ssh_config");
253
- fs.writeFileSync(keyPath, "fake-key");
254
- fs.writeFileSync(sshConfigPath, `
255
- Host eq
256
- HostName=eq.example.com
257
- User = deploy
258
- Port=2202
259
- IdentityFile = "${keyPath}"
260
- `);
261
- const result = loadSshConfigFiles([sshConfigPath]);
262
- assert.strictEqual(result.configs.eq.host, "eq.example.com");
263
- assert.strictEqual(result.configs.eq.username, "deploy");
264
- assert.strictEqual(result.configs.eq.port, 2202);
265
- assert.strictEqual(result.configs.eq.privateKey, path.normalize(keyPath));
266
- });
267
- it("should respect OpenSSH disabled identity and agent settings", () => {
268
- const sshConfigPath = path.join(tempDir, "ssh_config");
269
- fs.writeFileSync(sshConfigPath, `
270
- Host locked
271
- HostName locked.example.com
272
- User deploy
273
- IdentityFile none
274
- IdentityAgent none
275
- IdentitiesOnly yes
276
- `);
277
- const result = loadSshConfigFiles([sshConfigPath]);
278
- assert.strictEqual(result.configs.locked.privateKey, undefined);
279
- assert.strictEqual(result.configs.locked.agent, false);
280
- assert.strictEqual(result.configs.locked.identitiesOnly, true);
281
- });
282
- it("should load Include files", () => {
283
- const includedPath = path.join(tempDir, "included.conf");
284
- const sshConfigPath = path.join(tempDir, "ssh_config");
285
- fs.writeFileSync(includedPath, `
286
- Host inc
287
- HostName inc.example.com
288
- User included
289
- `);
290
- fs.writeFileSync(sshConfigPath, `Include "${includedPath}"`);
291
- const result = loadSshConfigFiles([sshConfigPath]);
292
- assert.strictEqual(result.configs.inc.host, "inc.example.com");
293
- assert.strictEqual(result.configs.inc.username, "included");
294
- assert.ok(result.files.includes(fs.realpathSync(includedPath)));
295
- });
296
- it("should merge YAML server settings over OpenSSH config entries", () => {
297
- const sshConfigPath = path.join(tempDir, "ssh_config");
298
- const yamlPath = path.join(tempDir, "servers.yaml");
299
- fs.writeFileSync(sshConfigPath, `
300
- Host dev
301
- HostName ssh.example.com
302
- User sshuser
303
- Port 22
304
- `);
305
- fs.writeFileSync(yamlPath, `
306
- servers:
307
- dev:
308
- host: yaml.example.com
309
- whitelist:
310
- - "^pwd$"
311
- allowedRemoteDirectories: []
312
- `);
313
- const result = loadConfigFromSources({
314
- yamlConfigPath: yamlPath,
315
- sshConfigPaths: [sshConfigPath],
316
- });
317
- assert.strictEqual(result.configs.dev.host, "yaml.example.com");
318
- assert.strictEqual(result.configs.dev.username, "sshuser");
319
- assert.deepStrictEqual(result.configs.dev.commandWhitelist, ["^pwd$"]);
320
- assert.deepStrictEqual(result.configs.dev.allowedRemoteDirectories, []);
321
- });
322
- it("should still reject YAML-only servers without auth", () => {
323
- const yamlPath = path.join(tempDir, "servers.yaml");
324
- fs.writeFileSync(yamlPath, `
325
- sshConfig: false
326
- servers:
327
- broken:
328
- host: 192.168.1.1
329
- username: test
330
- `);
331
- assert.throws(() => loadConfigFromYaml(yamlPath), /password.*privateKey.*required/i);
332
- });
333
- it("should reject partial YAML servers without a matching OpenSSH host", () => {
334
- const yamlPath = path.join(tempDir, "servers.yaml");
335
- fs.writeFileSync(yamlPath, `
336
- sshConfig: false
337
- servers:
338
- missing-base:
339
- whitelist:
340
- - "^pwd$"
341
- `);
342
- assert.throws(() => loadConfigFromSources({ yamlConfigPath: yamlPath }), /host.*required/i);
343
- });
344
- it("should let explicit --ssh-config paths override YAML sshConfig false", () => {
345
- const sshConfigPath = path.join(tempDir, "ssh_config");
346
- const yamlPath = path.join(tempDir, "servers.yaml");
347
- fs.writeFileSync(sshConfigPath, `
348
- Host explicit
349
- HostName explicit.example.com
350
- User deploy
351
- `);
352
- fs.writeFileSync(yamlPath, `
353
- sshConfig: false
354
- `);
355
- const result = loadConfigFromSources({
356
- yamlConfigPath: yamlPath,
357
- sshConfigPaths: [sshConfigPath],
358
- });
359
- assert.strictEqual(result.configs.explicit.host, "explicit.example.com");
360
- });
361
- });
362
- describe("Whitelist/Blacklist Config", () => {
363
- let tempDir;
364
- beforeEach(() => {
365
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "handfree-ssh-mcp-test-"));
366
- });
367
- afterEach(() => {
368
- if (fs.existsSync(tempDir)) {
369
- fs.rmSync(tempDir, { recursive: true, force: true });
370
- }
371
- });
372
- it("should load whitelist patterns", () => {
373
- const configContent = `
374
- servers:
375
- dev:
376
- host: 192.168.1.1
377
- username: test
378
- password: test
379
- whitelist:
380
- - "^ls( .*)?$"
381
- - "^cat .*$"
382
- - "^docker ps.*$"
383
- `;
384
- const tempConfigPath = path.join(tempDir, "whitelist.yaml");
385
- fs.writeFileSync(tempConfigPath, configContent);
386
- const result = loadConfigFromYaml(tempConfigPath);
387
- assert.deepStrictEqual(result.configs["dev"].commandWhitelist, [
388
- "^ls( .*)?$",
389
- "^cat .*$",
390
- "^docker ps.*$"
391
- ]);
392
- });
393
- it("should load blacklist patterns", () => {
394
- const configContent = `
395
- servers:
396
- dev:
397
- host: 192.168.1.1
398
- username: test
399
- password: test
400
- blacklist:
401
- - "^rm .*$"
402
- - "^shutdown.*$"
403
- `;
404
- const tempConfigPath = path.join(tempDir, "blacklist.yaml");
405
- fs.writeFileSync(tempConfigPath, configContent);
406
- const result = loadConfigFromYaml(tempConfigPath);
407
- assert.deepStrictEqual(result.configs["dev"].commandBlacklist, [
408
- "^rm .*$",
409
- "^shutdown.*$"
410
- ]);
411
- });
412
- it("should load both whitelist and blacklist", () => {
413
- const configContent = `
414
- servers:
415
- dev:
416
- host: 192.168.1.1
417
- username: test
418
- password: test
419
- whitelist:
420
- - "^.*$"
421
- blacklist:
422
- - "^rm -rf.*$"
423
- `;
424
- const tempConfigPath = path.join(tempDir, "both.yaml");
425
- fs.writeFileSync(tempConfigPath, configContent);
426
- const result = loadConfigFromYaml(tempConfigPath);
427
- assert.deepStrictEqual(result.configs["dev"].commandWhitelist, ["^.*$"]);
428
- assert.deepStrictEqual(result.configs["dev"].commandBlacklist, ["^rm -rf.*$"]);
429
- });
430
- });
431
- describe("SFTP Path Allowlist Config", () => {
432
- let tempDir;
433
- beforeEach(() => {
434
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "handfree-ssh-mcp-test-"));
435
- });
436
- afterEach(() => {
437
- if (fs.existsSync(tempDir)) {
438
- fs.rmSync(tempDir, { recursive: true, force: true });
439
- }
440
- });
441
- it("should load allowedRemoteDirectories", () => {
442
- const configContent = `
443
- servers:
444
- dev:
445
- host: 192.168.1.1
446
- username: test
447
- password: test
448
- allowedRemoteDirectories:
449
- - /home/test
450
- - /tmp
451
- `;
452
- const p = path.join(tempDir, "ard.yaml");
453
- fs.writeFileSync(p, configContent);
454
- const result = loadConfigFromYaml(p);
455
- assert.deepStrictEqual(result.configs["dev"].allowedRemoteDirectories, ["/home/test", "/tmp"]);
456
- });
457
- it("should strip trailing slashes from allowedRemoteDirectories", () => {
458
- const configContent = `
459
- servers:
460
- dev:
461
- host: 192.168.1.1
462
- username: test
463
- password: test
464
- allowedRemoteDirectories:
465
- - /home/test/
466
- - /tmp/
467
- - /
468
- `;
469
- const p = path.join(tempDir, "ard-slash.yaml");
470
- fs.writeFileSync(p, configContent);
471
- const result = loadConfigFromYaml(p);
472
- assert.deepStrictEqual(result.configs["dev"].allowedRemoteDirectories, ["/home/test", "/tmp", "/"]);
473
- });
474
- it("should reject relative paths in allowedRemoteDirectories", () => {
475
- const configContent = `
476
- servers:
477
- dev:
478
- host: 192.168.1.1
479
- username: test
480
- password: test
481
- allowedRemoteDirectories:
482
- - home/test
483
- `;
484
- const p = path.join(tempDir, "ard-rel.yaml");
485
- fs.writeFileSync(p, configContent);
486
- assert.throws(() => loadConfigFromYaml(p), /absolute POSIX path/i);
487
- });
488
- it("should reject '..' segments in allowedRemoteDirectories", () => {
489
- const configContent = `
490
- servers:
491
- dev:
492
- host: 192.168.1.1
493
- username: test
494
- password: test
495
- allowedRemoteDirectories:
496
- - /home/test/../etc
497
- `;
498
- const p = path.join(tempDir, "ard-dotdot.yaml");
499
- fs.writeFileSync(p, configContent);
500
- assert.throws(() => loadConfigFromYaml(p), /'\.\.'/);
501
- });
502
- it("should reject empty string entries in allowedRemoteDirectories", () => {
503
- const configContent = `
504
- servers:
505
- dev:
506
- host: 192.168.1.1
507
- username: test
508
- password: test
509
- allowedRemoteDirectories:
510
- - ""
511
- `;
512
- const p = path.join(tempDir, "ard-empty.yaml");
513
- fs.writeFileSync(p, configContent);
514
- assert.throws(() => loadConfigFromYaml(p), /non-empty/i);
515
- });
516
- it("should resolve allowedLocalDirectories to absolute paths", () => {
517
- const configContent = `
518
- servers:
519
- dev:
520
- host: 192.168.1.1
521
- username: test
522
- password: test
523
- allowedLocalDirectories:
524
- - ~/uploads
525
- `;
526
- const p = path.join(tempDir, "ald.yaml");
527
- fs.writeFileSync(p, configContent);
528
- const result = loadConfigFromYaml(p);
529
- const resolved = result.configs["dev"].allowedLocalDirectories;
530
- assert.ok(resolved && resolved.length === 1);
531
- assert.ok(path.isAbsolute(resolved[0]));
532
- assert.ok(resolved[0].includes(os.homedir()));
533
- });
534
- it("should leave allowedRemoteDirectories undefined when not set", () => {
535
- const configContent = `
536
- servers:
537
- dev:
538
- host: 192.168.1.1
539
- username: test
540
- password: test
541
- `;
542
- const p = path.join(tempDir, "no-ard.yaml");
543
- fs.writeFileSync(p, configContent);
544
- const result = loadConfigFromYaml(p);
545
- assert.strictEqual(result.configs["dev"].allowedRemoteDirectories, undefined);
546
- assert.strictEqual(result.configs["dev"].allowedLocalDirectories, undefined);
547
- });
548
- });
549
- describe("Output Log Dir Config", () => {
550
- let tempDir;
551
- beforeEach(() => {
552
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "handfree-ssh-mcp-test-"));
553
- });
554
- afterEach(() => {
555
- if (fs.existsSync(tempDir)) {
556
- fs.rmSync(tempDir, { recursive: true, force: true });
557
- }
558
- });
559
- it("should leave outputLogDir undefined when not set", () => {
560
- const configContent = `
561
- servers:
562
- dev:
563
- host: 192.168.1.1
564
- username: test
565
- password: test
566
- `;
567
- const p = path.join(tempDir, "no-log.yaml");
568
- fs.writeFileSync(p, configContent);
569
- const result = loadConfigFromYaml(p);
570
- assert.strictEqual(result.outputLogDir, undefined);
571
- });
572
- it("should resolve outputLogDir to an absolute path", () => {
573
- const configContent = `
574
- outputLogDir: ./logs
575
- servers:
576
- dev:
577
- host: 192.168.1.1
578
- username: test
579
- password: test
580
- `;
581
- const p = path.join(tempDir, "log.yaml");
582
- fs.writeFileSync(p, configContent);
583
- const result = loadConfigFromYaml(p);
584
- assert.ok(result.outputLogDir);
585
- assert.ok(path.isAbsolute(result.outputLogDir));
586
- assert.ok(result.outputLogDir.endsWith(path.normalize("logs")));
587
- });
588
- it("should expand ~ in outputLogDir", () => {
589
- const configContent = `
590
- outputLogDir: ~/handfree-logs
591
- servers:
592
- dev:
593
- host: 192.168.1.1
594
- username: test
595
- password: test
596
- `;
597
- const p = path.join(tempDir, "log-tilde.yaml");
598
- fs.writeFileSync(p, configContent);
599
- const result = loadConfigFromYaml(p);
600
- assert.ok(result.outputLogDir);
601
- assert.ok(result.outputLogDir.includes(os.homedir()));
602
- });
603
- it("should reject non-string outputLogDir", () => {
604
- const configContent = `
605
- outputLogDir: 42
606
- servers:
607
- dev:
608
- host: 192.168.1.1
609
- username: test
610
- password: test
611
- `;
612
- const p = path.join(tempDir, "log-bad.yaml");
613
- fs.writeFileSync(p, configContent);
614
- assert.throws(() => loadConfigFromYaml(p), /non-empty string/i);
615
- });
616
- });
617
- console.log("\n🧪 Running handfree-ssh-mcp tests...\n");