@marcusrbrown/infra 0.11.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,652 @@
1
+ import {afterEach, beforeEach, describe, expect, it} from 'bun:test'
2
+
3
+ import {createCapturedCtx, MockProcessExit} from '../../__test__/mcp-ctx-fixture'
4
+ import {
5
+ dashboardStatusAction,
6
+ getDashboardComposeStatus,
7
+ getDashboardStatusSummary,
8
+ parseComposePs,
9
+ parseComposePsOutput,
10
+ type ComposePsEntry,
11
+ type ServiceRow,
12
+ } from './status'
13
+
14
+ // ─── parseComposePsOutput ─────────────────────────────────────────────────────
15
+
16
+ describe('parseComposePsOutput', () => {
17
+ it('parses NDJSON output (one JSON object per line)', () => {
18
+ const ndjson = [
19
+ JSON.stringify({Name: 'dashboard', State: 'running', Health: 'healthy'}),
20
+ JSON.stringify({Name: 'db', State: 'running', Health: ''}),
21
+ ].join('\n')
22
+
23
+ const entries = parseComposePsOutput(ndjson)
24
+
25
+ expect(entries).toHaveLength(2)
26
+ expect(entries[0]).toEqual({Name: 'dashboard', State: 'running', Health: 'healthy'})
27
+ expect(entries[1]).toEqual({Name: 'db', State: 'running', Health: ''})
28
+ })
29
+
30
+ it('parses JSON-array output (legacy compose format)', () => {
31
+ const jsonArray = JSON.stringify([
32
+ {Name: 'dashboard', State: 'running', Health: 'healthy'},
33
+ {Name: 'db', State: 'running', Health: ''},
34
+ ])
35
+
36
+ const entries = parseComposePsOutput(jsonArray)
37
+
38
+ expect(entries).toHaveLength(2)
39
+ expect(entries[0]).toEqual({Name: 'dashboard', State: 'running', Health: 'healthy'})
40
+ })
41
+
42
+ it('returns empty array for empty string', () => {
43
+ expect(parseComposePsOutput('')).toEqual([])
44
+ })
45
+
46
+ it('returns empty array for whitespace-only string', () => {
47
+ expect(parseComposePsOutput(' \n ')).toEqual([])
48
+ })
49
+ })
50
+
51
+ // ─── parseComposePs ──────────────────────────────────────────────────────────
52
+
53
+ describe('parseComposePs', () => {
54
+ it('maps running services with healthy status', () => {
55
+ const raw: ComposePsEntry[] = [
56
+ {Name: 'dashboard', State: 'running', Health: 'healthy'},
57
+ {Name: 'db', State: 'running', Health: ''},
58
+ ]
59
+
60
+ const rows = parseComposePs(raw)
61
+
62
+ expect(rows).toHaveLength(2)
63
+ expect(rows[0]).toEqual({service: 'dashboard', state: 'running', health: 'healthy'} satisfies ServiceRow)
64
+ expect(rows[1]).toEqual({service: 'db', state: 'running', health: 'n-a'} satisfies ServiceRow)
65
+ })
66
+
67
+ it('maps exited state', () => {
68
+ const raw: ComposePsEntry[] = [{Name: 'dashboard', State: 'exited', Health: ''}]
69
+
70
+ const rows = parseComposePs(raw)
71
+
72
+ expect(rows[0]).toEqual({service: 'dashboard', state: 'exited', health: 'n-a'} satisfies ServiceRow)
73
+ })
74
+
75
+ it('maps unhealthy health status', () => {
76
+ const raw: ComposePsEntry[] = [{Name: 'dashboard', State: 'running', Health: 'unhealthy'}]
77
+
78
+ const rows = parseComposePs(raw)
79
+
80
+ expect(rows[0]?.health).toBe('unhealthy')
81
+ })
82
+
83
+ it('maps starting health status', () => {
84
+ const raw: ComposePsEntry[] = [{Name: 'dashboard', State: 'running', Health: 'starting'}]
85
+
86
+ const rows = parseComposePs(raw)
87
+
88
+ expect(rows[0]?.health).toBe('starting')
89
+ })
90
+
91
+ it('maps unknown health values to n-a', () => {
92
+ const raw: ComposePsEntry[] = [{Name: 'dashboard', State: 'running', Health: 'something-weird'}]
93
+
94
+ const rows = parseComposePs(raw)
95
+
96
+ expect(rows[0]?.health).toBe('n-a')
97
+ })
98
+
99
+ it('returns empty array for empty input', () => {
100
+ expect(parseComposePs([])).toEqual([])
101
+ })
102
+ })
103
+
104
+ // ─── getDashboardComposeStatus (SSH mocked via SpawnFn) ──────────────────────
105
+
106
+ type SpawnFn = (
107
+ cmd: string[],
108
+ opts: {env: Record<string, string>; stdout: 'pipe'; stderr: 'pipe'},
109
+ ) => {
110
+ stdout: ReadableStream<Uint8Array>
111
+ stderr: ReadableStream<Uint8Array>
112
+ exited: Promise<number>
113
+ }
114
+
115
+ function makeSpawn(stdout: string, stderr = '', exitCode = 0): SpawnFn {
116
+ return (_cmd, _opts) => {
117
+ const enc = new TextEncoder()
118
+ return {
119
+ stdout: new ReadableStream({
120
+ start(controller) {
121
+ controller.enqueue(enc.encode(stdout))
122
+ controller.close()
123
+ },
124
+ }),
125
+ stderr: new ReadableStream({
126
+ start(controller) {
127
+ controller.enqueue(enc.encode(stderr))
128
+ controller.close()
129
+ },
130
+ }),
131
+ exited: Promise.resolve(exitCode),
132
+ }
133
+ }
134
+ }
135
+
136
+ // ─── SSH command includes repo-pinned UserKnownHostsFile ─────────────────────
137
+
138
+ describe('getDashboardComposeStatus — SSH command includes UserKnownHostsFile', () => {
139
+ it('passes -o UserKnownHostsFile=<repo>/.github/known_hosts to ssh when the file exists', async () => {
140
+ let capturedCmd: string[] = []
141
+
142
+ const capturingSpawn: SpawnFn = (cmd, _opts) => {
143
+ capturedCmd = cmd
144
+ const enc = new TextEncoder()
145
+ return {
146
+ stdout: new ReadableStream({
147
+ start(c) {
148
+ c.enqueue(enc.encode(JSON.stringify([{Name: 'dashboard', State: 'running', Health: 'healthy'}])))
149
+ c.close()
150
+ },
151
+ }),
152
+ stderr: new ReadableStream({
153
+ start(c) {
154
+ c.close()
155
+ },
156
+ }),
157
+ exited: Promise.resolve(0),
158
+ }
159
+ }
160
+
161
+ await getDashboardComposeStatus('dashboard.fro.bot', capturingSpawn)
162
+
163
+ // Find the UserKnownHostsFile option in the ssh command
164
+ const knownHostsIdx = capturedCmd.findIndex(arg => arg.startsWith('UserKnownHostsFile='))
165
+ expect(knownHostsIdx).toBeGreaterThan(-1)
166
+ expect(capturedCmd[knownHostsIdx - 1]).toBe('-o')
167
+ expect(capturedCmd[knownHostsIdx]).toMatch(/\.github[/\\]known_hosts$/)
168
+ })
169
+
170
+ it('does not weaken StrictHostKeyChecking when UserKnownHostsFile is added', async () => {
171
+ let capturedCmd: string[] = []
172
+
173
+ const capturingSpawn: SpawnFn = (cmd, _opts) => {
174
+ capturedCmd = cmd
175
+ const enc = new TextEncoder()
176
+ return {
177
+ stdout: new ReadableStream({
178
+ start(c) {
179
+ c.enqueue(enc.encode(JSON.stringify([{Name: 'dashboard', State: 'running', Health: 'healthy'}])))
180
+ c.close()
181
+ },
182
+ }),
183
+ stderr: new ReadableStream({
184
+ start(c) {
185
+ c.close()
186
+ },
187
+ }),
188
+ exited: Promise.resolve(0),
189
+ }
190
+ }
191
+
192
+ await getDashboardComposeStatus('dashboard.fro.bot', capturingSpawn)
193
+
194
+ const strictIdx = capturedCmd.findIndex(arg => arg.startsWith('StrictHostKeyChecking='))
195
+ expect(strictIdx).toBeGreaterThan(-1)
196
+ expect(capturedCmd[strictIdx]).toBe('StrictHostKeyChecking=yes')
197
+ })
198
+ })
199
+
200
+ describe('getDashboardComposeStatus', () => {
201
+ it('returns service rows from NDJSON output', async () => {
202
+ const ndjson = [
203
+ JSON.stringify({Name: 'dashboard', State: 'running', Health: 'healthy'}),
204
+ JSON.stringify({Name: 'db', State: 'running', Health: ''}),
205
+ ].join('\n')
206
+
207
+ const result = await getDashboardComposeStatus('dashboard.fro.bot', makeSpawn(ndjson))
208
+
209
+ expect(result.ok).toBe(true)
210
+ expect(result.services).toHaveLength(2)
211
+ expect(result.services[0]?.service).toBe('dashboard')
212
+ })
213
+
214
+ it('returns service rows from JSON-array output', async () => {
215
+ const jsonArray = JSON.stringify([
216
+ {Name: 'dashboard', State: 'running', Health: 'healthy'},
217
+ {Name: 'db', State: 'running', Health: ''},
218
+ ])
219
+
220
+ const result = await getDashboardComposeStatus('dashboard.fro.bot', makeSpawn(jsonArray))
221
+
222
+ expect(result.ok).toBe(true)
223
+ expect(result.services).toHaveLength(2)
224
+ })
225
+
226
+ it('returns ok=false when SSH exits non-zero', async () => {
227
+ const result = await getDashboardComposeStatus('dashboard.fro.bot', makeSpawn('', 'connection refused', 1))
228
+
229
+ expect(result.ok).toBe(false)
230
+ expect(result.error).toContain('SSH command failed')
231
+ })
232
+
233
+ it('rejects invalid host before spawning SSH', async () => {
234
+ let spawnCalled = false
235
+ const spy: SpawnFn = (cmd, opts) => {
236
+ spawnCalled = true
237
+ return makeSpawn('')(cmd, opts)
238
+ }
239
+
240
+ await expect(getDashboardComposeStatus('-oProxyCommand=evil', spy)).rejects.toThrow('Invalid DASHBOARD_DOMAIN')
241
+ expect(spawnCalled).toBe(false)
242
+ })
243
+ })
244
+
245
+ // ─── getDashboardStatusSummary ────────────────────────────────────────────────
246
+
247
+ describe('getDashboardStatusSummary', () => {
248
+ it('shows service rows with DEGRADED marker when services present but unhealthy', async () => {
249
+ const ndjson = [
250
+ JSON.stringify({Name: 'dashboard', State: 'running', Health: 'unhealthy'}),
251
+ JSON.stringify({Name: 'db', State: 'running', Health: 'healthy'}),
252
+ ].join('\n')
253
+
254
+ const summary = await getDashboardStatusSummary('dashboard.fro.bot', makeSpawn(ndjson))
255
+
256
+ expect(summary.http).toContain('DEGRADED')
257
+ expect(summary.http).toContain('dashboard')
258
+ expect(summary.http).not.toContain('No services reported')
259
+ })
260
+
261
+ it('shows ERROR with no-services message when compose ps returns empty output', async () => {
262
+ const summary = await getDashboardStatusSummary('dashboard.fro.bot', makeSpawn(''))
263
+
264
+ expect(summary.http).toContain('ERROR')
265
+ expect(summary.http).toContain('No services')
266
+ expect(summary.http).not.toContain('DEGRADED')
267
+ })
268
+
269
+ it('shows OK with service rows when all services are healthy', async () => {
270
+ const ndjson = [
271
+ JSON.stringify({Name: 'dashboard', State: 'running', Health: 'healthy'}),
272
+ JSON.stringify({Name: 'db', State: 'running', Health: 'healthy'}),
273
+ ].join('\n')
274
+
275
+ const summary = await getDashboardStatusSummary('dashboard.fro.bot', makeSpawn(ndjson))
276
+
277
+ expect(summary.http).toContain('OK')
278
+ expect(summary.http).toContain('dashboard')
279
+ expect(summary.http).not.toContain('DEGRADED')
280
+ expect(summary.http).not.toContain('ERROR')
281
+ })
282
+
283
+ it('returns a summary with app: dashboard', async () => {
284
+ const ndjson = JSON.stringify([{Name: 'dashboard', State: 'running', Health: 'healthy'}])
285
+ const summary = await getDashboardStatusSummary('dashboard.fro.bot', makeSpawn(ndjson))
286
+ expect(summary.app).toBe('dashboard')
287
+ })
288
+ })
289
+
290
+ // ─── dashboardStatusAction ───────────────────────────────────────────────────
291
+
292
+ describe('status command', () => {
293
+ let originalEnv: Record<string, string | undefined>
294
+
295
+ beforeEach(() => {
296
+ originalEnv = {DASHBOARD_DOMAIN: process.env.DASHBOARD_DOMAIN, MY_DASHBOARD_HOST: process.env.MY_DASHBOARD_HOST}
297
+ })
298
+
299
+ afterEach(() => {
300
+ if (originalEnv.DASHBOARD_DOMAIN === undefined) {
301
+ delete process.env.DASHBOARD_DOMAIN
302
+ } else {
303
+ process.env.DASHBOARD_DOMAIN = originalEnv.DASHBOARD_DOMAIN
304
+ }
305
+
306
+ if (originalEnv.MY_DASHBOARD_HOST === undefined) {
307
+ delete process.env.MY_DASHBOARD_HOST
308
+ } else {
309
+ process.env.MY_DASHBOARD_HOST = originalEnv.MY_DASHBOARD_HOST
310
+ }
311
+ })
312
+
313
+ it('outputs service rows through ctx when services are running', async () => {
314
+ process.env.DASHBOARD_DOMAIN = 'dashboard.fro.bot'
315
+
316
+ const ndjson = [
317
+ JSON.stringify({Name: 'dashboard', State: 'running', Health: 'healthy'}),
318
+ JSON.stringify({Name: 'db', State: 'running', Health: ''}),
319
+ ].join('\n')
320
+
321
+ const {ctx, captured} = createCapturedCtx()
322
+
323
+ await dashboardStatusAction({}, ctx, makeSpawn(ndjson))
324
+
325
+ const output = captured.stdout.join('\n')
326
+ expect(output).toContain('dashboard')
327
+ expect(output).toContain('running')
328
+ expect(captured.exit).toBeNull()
329
+ })
330
+
331
+ it('uses ctx.console NOT global console (output is captured via ctx)', async () => {
332
+ process.env.DASHBOARD_DOMAIN = 'dashboard.fro.bot'
333
+
334
+ const ndjson = JSON.stringify([{Name: 'dashboard', State: 'running', Health: 'healthy'}])
335
+ const {ctx, captured} = createCapturedCtx()
336
+
337
+ await dashboardStatusAction({}, ctx, makeSpawn(ndjson))
338
+
339
+ // Output must appear in captured ctx, not global console
340
+ expect(captured.stdout.length).toBeGreaterThan(0)
341
+ expect(captured.stdout.join('')).toContain('dashboard')
342
+ })
343
+
344
+ it('calls ctx.console.error and ctx.process.exit(1) on SSH failure without throwing', async () => {
345
+ process.env.DASHBOARD_DOMAIN = 'dashboard.fro.bot'
346
+
347
+ const {ctx, captured} = createCapturedCtx()
348
+
349
+ let threw = false
350
+ try {
351
+ await dashboardStatusAction({}, ctx, makeSpawn('', 'connection refused', 1))
352
+ } catch (error) {
353
+ if (error instanceof MockProcessExit) {
354
+ // expected — MockProcessExit is thrown by ctx.process.exit
355
+ } else {
356
+ threw = true
357
+ }
358
+ }
359
+
360
+ expect(threw).toBe(false)
361
+ expect(captured.stderr.join('')).toContain('Error')
362
+ expect(captured.exit?.code).toBe(1)
363
+ })
364
+
365
+ it('calls ctx.console.error and exits when DASHBOARD_DOMAIN is not set', async () => {
366
+ delete process.env.DASHBOARD_DOMAIN
367
+
368
+ const {ctx, captured} = createCapturedCtx()
369
+
370
+ try {
371
+ await dashboardStatusAction({}, ctx)
372
+ } catch (error) {
373
+ if (!(error instanceof MockProcessExit)) throw error
374
+ }
375
+
376
+ expect(captured.stderr.join('')).toContain('DASHBOARD_DOMAIN')
377
+ expect(captured.exit?.code).toBe(1)
378
+ })
379
+
380
+ it('rejects bad host via ctx.console.error without throwing unhandled', async () => {
381
+ process.env.DASHBOARD_DOMAIN = '-oProxyCommand=evil'
382
+
383
+ const {ctx, captured} = createCapturedCtx()
384
+
385
+ try {
386
+ await dashboardStatusAction({}, ctx, makeSpawn(''))
387
+ } catch (error) {
388
+ if (!(error instanceof MockProcessExit)) throw error
389
+ }
390
+
391
+ expect(captured.stderr.join('')).toContain('Invalid DASHBOARD_DOMAIN')
392
+ expect(captured.exit?.code).toBe(1)
393
+ })
394
+
395
+ it('reads host from the env var named by --key instead of DASHBOARD_DOMAIN', async () => {
396
+ delete process.env.DASHBOARD_DOMAIN
397
+ process.env.MY_DASHBOARD_HOST = 'dashboard.fro.bot'
398
+
399
+ const ndjson = [
400
+ JSON.stringify({Name: 'dashboard', State: 'running', Health: 'healthy'}),
401
+ JSON.stringify({Name: 'db', State: 'running', Health: ''}),
402
+ ].join('\n')
403
+
404
+ const {ctx, captured} = createCapturedCtx()
405
+
406
+ try {
407
+ await dashboardStatusAction({key: 'MY_DASHBOARD_HOST'}, ctx, makeSpawn(ndjson))
408
+ } catch (error) {
409
+ if (!(error instanceof MockProcessExit)) throw error
410
+ }
411
+
412
+ const output = captured.stdout.join('\n')
413
+ expect(output).toContain('dashboard')
414
+ expect(captured.exit).toBeNull()
415
+ })
416
+
417
+ it('exits 1 with a clear message when docker compose ps returns empty output', async () => {
418
+ process.env.DASHBOARD_DOMAIN = 'dashboard.fro.bot'
419
+
420
+ const {ctx, captured} = createCapturedCtx()
421
+
422
+ try {
423
+ await dashboardStatusAction({}, ctx, makeSpawn(''))
424
+ } catch (error) {
425
+ if (!(error instanceof MockProcessExit)) throw error
426
+ }
427
+
428
+ expect(captured.stderr.join('')).toMatch(/no services|empty/i)
429
+ expect(captured.exit?.code).toBe(1)
430
+ })
431
+
432
+ it('reports degraded when a service is running but health is unhealthy', async () => {
433
+ process.env.DASHBOARD_DOMAIN = 'dashboard.fro.bot'
434
+
435
+ const ndjson = [
436
+ JSON.stringify({Name: 'dashboard', State: 'running', Health: 'unhealthy'}),
437
+ JSON.stringify({Name: 'db', State: 'running', Health: 'healthy'}),
438
+ ].join('\n')
439
+
440
+ const {ctx, captured} = createCapturedCtx()
441
+
442
+ try {
443
+ await dashboardStatusAction({}, ctx, makeSpawn(ndjson))
444
+ } catch (error) {
445
+ if (!(error instanceof MockProcessExit)) throw error
446
+ }
447
+
448
+ const output = captured.stdout.join('\n')
449
+ expect(output).toContain('DEGRADED')
450
+ expect(captured.exit?.code).toBe(1)
451
+ })
452
+
453
+ it('reports OK when a service is running with health n-a (e.g. caddy has no healthcheck)', async () => {
454
+ process.env.DASHBOARD_DOMAIN = 'dashboard.fro.bot'
455
+
456
+ const ndjson = [
457
+ JSON.stringify({Name: 'dashboard', State: 'running', Health: 'healthy'}),
458
+ JSON.stringify({Name: 'db', State: 'running', Health: 'healthy'}),
459
+ JSON.stringify({Name: 'caddy', State: 'running', Health: ''}),
460
+ ].join('\n')
461
+
462
+ const {ctx, captured} = createCapturedCtx()
463
+
464
+ await dashboardStatusAction({}, ctx, makeSpawn(ndjson))
465
+
466
+ const output = captured.stdout.join('\n')
467
+ expect(output).toContain('Status: OK')
468
+ expect(captured.exit).toBeNull()
469
+ })
470
+ })
471
+
472
+ // ─── SSH error redaction — host value must not leak in returned error strings ──
473
+
474
+ describe('getDashboardComposeStatus — SSH error redaction', () => {
475
+ it('does not include the resolved host value in the error when SSH fails', async () => {
476
+ const {getDashboardComposeStatus} = await import('./status')
477
+ const secretLookingHost = 'TOPSECRETHOSTNAME'
478
+
479
+ const mockSpawn = (_cmd: string[], _opts: {env: Record<string, string>; stdout: 'pipe'; stderr: 'pipe'}) => {
480
+ const encoder = new TextEncoder()
481
+ return {
482
+ stdout: new ReadableStream({
483
+ start(c) {
484
+ c.enqueue(encoder.encode(''))
485
+ c.close()
486
+ },
487
+ }),
488
+ stderr: new ReadableStream({
489
+ start(c) {
490
+ c.enqueue(encoder.encode(`ssh: Could not resolve hostname ${secretLookingHost}: Name or service not known`))
491
+ c.close()
492
+ },
493
+ }),
494
+ exited: Promise.resolve(255),
495
+ }
496
+ }
497
+
498
+ const result = await getDashboardComposeStatus(secretLookingHost, mockSpawn)
499
+
500
+ expect(result.ok).toBe(false)
501
+ expect(result.error).toBeDefined()
502
+ expect(result.error).not.toContain(secretLookingHost)
503
+ })
504
+
505
+ it('does not leak the host even when OpenSSH lowercases it in stderr', async () => {
506
+ const {getDashboardComposeStatus} = await import('./status')
507
+ const secretHost = 'AKIAIOSFODNN7EXAMPLE'
508
+
509
+ const mockSpawn = (_cmd: string[], _opts: {env: Record<string, string>; stdout: 'pipe'; stderr: 'pipe'}) => {
510
+ const encoder = new TextEncoder()
511
+ return {
512
+ stdout: new ReadableStream({
513
+ start(c) {
514
+ c.close()
515
+ },
516
+ }),
517
+ stderr: new ReadableStream({
518
+ start(c) {
519
+ c.enqueue(
520
+ encoder.encode(`ssh: Could not resolve hostname ${secretHost.toLowerCase()}: Name or service not known`),
521
+ )
522
+ c.close()
523
+ },
524
+ }),
525
+ exited: Promise.resolve(255),
526
+ }
527
+ }
528
+
529
+ const result = await getDashboardComposeStatus(secretHost, mockSpawn)
530
+
531
+ expect(result.ok).toBe(false)
532
+ expect(result.error).not.toContain(secretHost)
533
+ expect(result.error).not.toContain(secretHost.toLowerCase())
534
+ })
535
+ })
536
+
537
+ // ─── SSH identity injection (DASHBOARD_SSH_KEY) ───────────────────────────────
538
+
539
+ describe('getDashboardComposeStatus — SSH identity injection', () => {
540
+ let originalEnv: Record<string, string | undefined>
541
+
542
+ beforeEach(() => {
543
+ originalEnv = {DASHBOARD_SSH_KEY: process.env.DASHBOARD_SSH_KEY}
544
+ })
545
+
546
+ afterEach(() => {
547
+ if (originalEnv.DASHBOARD_SSH_KEY === undefined) {
548
+ delete process.env.DASHBOARD_SSH_KEY
549
+ } else {
550
+ process.env.DASHBOARD_SSH_KEY = originalEnv.DASHBOARD_SSH_KEY
551
+ }
552
+ })
553
+
554
+ it('includes -i <path> and IdentitiesOnly=yes in ssh argv when DASHBOARD_SSH_KEY is set', async () => {
555
+ process.env.DASHBOARD_SSH_KEY = '-----BEGIN OPENSSH PRIVATE KEY-----\nfakekey\n-----END OPENSSH PRIVATE KEY-----\n'
556
+
557
+ let capturedCmd: string[] = []
558
+ const capturingSpawn: SpawnFn = (cmd, _opts) => {
559
+ capturedCmd = cmd
560
+ const enc = new TextEncoder()
561
+ return {
562
+ stdout: new ReadableStream({
563
+ start(c) {
564
+ c.enqueue(enc.encode(JSON.stringify([{Name: 'dashboard', State: 'running', Health: 'healthy'}])))
565
+ c.close()
566
+ },
567
+ }),
568
+ stderr: new ReadableStream({
569
+ start(c) {
570
+ c.close()
571
+ },
572
+ }),
573
+ exited: Promise.resolve(0),
574
+ }
575
+ }
576
+
577
+ await getDashboardComposeStatus('dashboard.fro.bot', capturingSpawn)
578
+
579
+ const iIdx = capturedCmd.indexOf('-i')
580
+ expect(iIdx).toBeGreaterThan(-1)
581
+ expect(capturedCmd[iIdx + 1]).toBeTruthy()
582
+
583
+ const identitiesOnlyIdx = capturedCmd.indexOf('IdentitiesOnly=yes')
584
+ expect(identitiesOnlyIdx).toBeGreaterThan(-1)
585
+
586
+ const destination = capturedCmd.find(arg => arg.includes('@'))
587
+ expect(destination).toBe('root@dashboard.fro.bot')
588
+ })
589
+
590
+ it('does not include -i or IdentitiesOnly=yes when DASHBOARD_SSH_KEY is absent', async () => {
591
+ delete process.env.DASHBOARD_SSH_KEY
592
+
593
+ let capturedCmd: string[] = []
594
+ const capturingSpawn: SpawnFn = (cmd, _opts) => {
595
+ capturedCmd = cmd
596
+ const enc = new TextEncoder()
597
+ return {
598
+ stdout: new ReadableStream({
599
+ start(c) {
600
+ c.enqueue(enc.encode(JSON.stringify([{Name: 'dashboard', State: 'running', Health: 'healthy'}])))
601
+ c.close()
602
+ },
603
+ }),
604
+ stderr: new ReadableStream({
605
+ start(c) {
606
+ c.close()
607
+ },
608
+ }),
609
+ exited: Promise.resolve(0),
610
+ }
611
+ }
612
+
613
+ await getDashboardComposeStatus('dashboard.fro.bot', capturingSpawn)
614
+
615
+ expect(capturedCmd.indexOf('-i')).toBe(-1)
616
+ expect(capturedCmd.indexOf('IdentitiesOnly=yes')).toBe(-1)
617
+ })
618
+
619
+ it('cleans up the temp key file after the SSH command completes', async () => {
620
+ const {statSync} = await import('node:fs')
621
+ process.env.DASHBOARD_SSH_KEY = '-----BEGIN OPENSSH PRIVATE KEY-----\nfakekey\n-----END OPENSSH PRIVATE KEY-----\n'
622
+
623
+ let capturedKeyPath: string | undefined
624
+ const capturingSpawn: SpawnFn = (cmd, _opts) => {
625
+ const iIdx = cmd.indexOf('-i')
626
+ if (iIdx !== -1) capturedKeyPath = cmd[iIdx + 1]
627
+ const enc = new TextEncoder()
628
+ return {
629
+ stdout: new ReadableStream({
630
+ start(c) {
631
+ c.enqueue(enc.encode(JSON.stringify([{Name: 'dashboard', State: 'running', Health: 'healthy'}])))
632
+ c.close()
633
+ },
634
+ }),
635
+ stderr: new ReadableStream({
636
+ start(c) {
637
+ c.close()
638
+ },
639
+ }),
640
+ exited: Promise.resolve(0),
641
+ }
642
+ }
643
+
644
+ await getDashboardComposeStatus('dashboard.fro.bot', capturingSpawn)
645
+
646
+ expect(capturedKeyPath).toBeTruthy()
647
+ const keyPath = capturedKeyPath
648
+ if (keyPath) {
649
+ expect(() => statSync(keyPath)).toThrow()
650
+ }
651
+ })
652
+ })