@marcusrbrown/infra 0.4.10 → 0.4.11
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {afterEach, beforeEach, describe, expect, it} from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import {parseComposePs, type ComposePsEntry, type ServiceRow} from './status'
|
|
3
|
+
import {parseComposePs, parseComposePsOutput, type ComposePsEntry, type ServiceRow} from './status'
|
|
4
4
|
|
|
5
5
|
// ─── parseComposePs ──────────────────────────────────────────────────────────
|
|
6
6
|
|
|
@@ -257,3 +257,98 @@ describe('getGatewayComposeStatus', () => {
|
|
|
257
257
|
expect(result.error).toContain('SSH')
|
|
258
258
|
})
|
|
259
259
|
})
|
|
260
|
+
|
|
261
|
+
// ─── parseComposePsOutput ─────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
// Realistic fixture shape matching live droplet output
|
|
264
|
+
const ndjsonFixture = [
|
|
265
|
+
String.raw`{"Command":"\"docker-entrypoint.sh\"","CreatedAt":"2026-05-20 08:20:18 +0000 UTC","ExitCode":0,"Health":"healthy","ID":"01e1a16f5752","Image":"fro-bot-gateway","Labels":"","LocalVolumes":"0","Mounts":"","Name":"fro-bot-gateway-1","Names":"fro-bot-gateway-1","Networks":"gateway_default","Ports":"","Project":"gateway","Publishers":null,"RunningFor":"2 hours ago","Service":"gateway","Size":"0B","State":"running","Status":"Up 2 hours (healthy)"}`,
|
|
266
|
+
String.raw`{"Command":"\"mitmproxy\"","CreatedAt":"2026-05-20 08:20:18 +0000 UTC","ExitCode":0,"Health":"healthy","ID":"02b2c27f6863","Image":"fro-bot-mitmproxy","Labels":"","LocalVolumes":"0","Mounts":"","Name":"fro-bot-mitmproxy-1","Names":"fro-bot-mitmproxy-1","Networks":"gateway_default","Ports":"","Project":"gateway","Publishers":null,"RunningFor":"2 hours ago","Service":"mitmproxy","Size":"0B","State":"running","Status":"Up 2 hours (healthy)"}`,
|
|
267
|
+
String.raw`{"Command":"\"sleep infinity\"","CreatedAt":"2026-05-20 08:20:18 +0000 UTC","ExitCode":0,"Health":"","ID":"03c3d38g7974","Image":"fro-bot-workspace","Labels":"","LocalVolumes":"0","Mounts":"","Name":"fro-bot-workspace-1","Names":"fro-bot-workspace-1","Networks":"gateway_default","Ports":"","Project":"gateway","Publishers":null,"RunningFor":"2 hours ago","Service":"workspace","Size":"0B","State":"running","Status":"Up 2 hours"}`,
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
describe('parseComposePsOutput', () => {
|
|
271
|
+
it('parses NDJSON with 3 lines into 3 entries with correct Name/State/Health', () => {
|
|
272
|
+
const raw = ndjsonFixture.join('\n')
|
|
273
|
+
const entries = parseComposePsOutput(raw)
|
|
274
|
+
|
|
275
|
+
expect(entries).toHaveLength(3)
|
|
276
|
+
expect(entries[0]?.Name).toBe('fro-bot-gateway-1')
|
|
277
|
+
expect(entries[0]?.State).toBe('running')
|
|
278
|
+
expect(entries[0]?.Health).toBe('healthy')
|
|
279
|
+
expect(entries[1]?.Name).toBe('fro-bot-mitmproxy-1')
|
|
280
|
+
expect(entries[1]?.State).toBe('running')
|
|
281
|
+
expect(entries[1]?.Health).toBe('healthy')
|
|
282
|
+
expect(entries[2]?.Name).toBe('fro-bot-workspace-1')
|
|
283
|
+
expect(entries[2]?.State).toBe('running')
|
|
284
|
+
expect(entries[2]?.Health).toBe('')
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('parses legacy single JSON array format', () => {
|
|
288
|
+
const raw = JSON.stringify([
|
|
289
|
+
{Name: 'fro-bot-gateway-1', State: 'running', Health: 'healthy'},
|
|
290
|
+
{Name: 'fro-bot-mitmproxy-1', State: 'running', Health: 'healthy'},
|
|
291
|
+
])
|
|
292
|
+
const entries = parseComposePsOutput(raw)
|
|
293
|
+
|
|
294
|
+
expect(entries).toHaveLength(2)
|
|
295
|
+
expect(entries[0]?.Name).toBe('fro-bot-gateway-1')
|
|
296
|
+
expect(entries[1]?.Name).toBe('fro-bot-mitmproxy-1')
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('returns empty array for empty string', () => {
|
|
300
|
+
expect(parseComposePsOutput('')).toEqual([])
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('returns empty array for whitespace-only input', () => {
|
|
304
|
+
expect(parseComposePsOutput(' \n \n ')).toEqual([])
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('parses a single NDJSON line', () => {
|
|
308
|
+
const raw = ndjsonFixture.at(0) ?? ''
|
|
309
|
+
const entries = parseComposePsOutput(raw)
|
|
310
|
+
|
|
311
|
+
expect(entries).toHaveLength(1)
|
|
312
|
+
expect(entries[0]?.Name).toBe('fro-bot-gateway-1')
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('handles trailing newline correctly', () => {
|
|
316
|
+
const raw = `${ndjsonFixture.join('\n')}\n`
|
|
317
|
+
const entries = parseComposePsOutput(raw)
|
|
318
|
+
|
|
319
|
+
expect(entries).toHaveLength(3)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('throws on a malformed NDJSON line', () => {
|
|
323
|
+
const raw = `${ndjsonFixture[0]}\nnot-valid-json\n${ndjsonFixture[2]}`
|
|
324
|
+
|
|
325
|
+
expect(() => parseComposePsOutput(raw)).toThrow()
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
// ─── getGatewayComposeStatus — NDJSON integration ────────────────────────────
|
|
330
|
+
|
|
331
|
+
describe('getGatewayComposeStatus — NDJSON stdout', () => {
|
|
332
|
+
it('parses NDJSON docker compose ps output and returns correct services', async () => {
|
|
333
|
+
const {getGatewayComposeStatus} = await import('./status')
|
|
334
|
+
|
|
335
|
+
const ndjsonOutput = ndjsonFixture.join('\n')
|
|
336
|
+
const result = await getGatewayComposeStatus('gateway.example.com', makeSpawnOk(ndjsonOutput))
|
|
337
|
+
|
|
338
|
+
expect(result.ok).toBe(true)
|
|
339
|
+
expect(result.services).toHaveLength(3)
|
|
340
|
+
expect(result.services[0]).toEqual({service: 'fro-bot-gateway-1', state: 'running', health: 'healthy'})
|
|
341
|
+
expect(result.services[1]).toEqual({service: 'fro-bot-mitmproxy-1', state: 'running', health: 'healthy'})
|
|
342
|
+
expect(result.services[2]).toEqual({service: 'fro-bot-workspace-1', state: 'running', health: 'n-a'})
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('returns ok=false with error message when NDJSON contains a malformed line', async () => {
|
|
346
|
+
const {getGatewayComposeStatus} = await import('./status')
|
|
347
|
+
|
|
348
|
+
const badOutput = `${ndjsonFixture[0]}\nnot-valid-json`
|
|
349
|
+
const result = await getGatewayComposeStatus('gateway.example.com', makeSpawnOk(badOutput))
|
|
350
|
+
|
|
351
|
+
expect(result.ok).toBe(false)
|
|
352
|
+
expect(result.error).toContain('Failed to parse docker compose ps output')
|
|
353
|
+
})
|
|
354
|
+
})
|
|
@@ -45,6 +45,24 @@ export type SpawnFn = (
|
|
|
45
45
|
|
|
46
46
|
// ─── Pure helpers ─────────────────────────────────────────────────────────────
|
|
47
47
|
|
|
48
|
+
export function parseComposePsOutput(stdoutText: string): ComposePsEntry[] {
|
|
49
|
+
const trimmed = stdoutText.trim()
|
|
50
|
+
if (trimmed.length === 0) return []
|
|
51
|
+
|
|
52
|
+
// Legacy compose may emit a single JSON array; current versions emit NDJSON.
|
|
53
|
+
if (trimmed.startsWith('[')) {
|
|
54
|
+
const parsed: unknown = JSON.parse(trimmed)
|
|
55
|
+
return Array.isArray(parsed) ? (parsed as ComposePsEntry[]) : []
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// NDJSON: one JSON object per non-empty line.
|
|
59
|
+
return trimmed
|
|
60
|
+
.split('\n')
|
|
61
|
+
.map(line => line.trim())
|
|
62
|
+
.filter(line => line.length > 0)
|
|
63
|
+
.map(line => JSON.parse(line) as ComposePsEntry)
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
function normalizeHealth(raw: string): HealthStatus {
|
|
49
67
|
if (raw === 'healthy' || raw === 'unhealthy' || raw === 'starting') {
|
|
50
68
|
return raw
|
|
@@ -117,8 +135,7 @@ export async function getGatewayComposeStatus(
|
|
|
117
135
|
let entries: ComposePsEntry[]
|
|
118
136
|
|
|
119
137
|
try {
|
|
120
|
-
|
|
121
|
-
entries = Array.isArray(parsed) ? (parsed as ComposePsEntry[]) : []
|
|
138
|
+
entries = parseComposePsOutput(stdoutText)
|
|
122
139
|
} catch {
|
|
123
140
|
return {ok: false, services: [], error: `Failed to parse docker compose ps output: ${stdoutText.slice(0, 200)}`}
|
|
124
141
|
}
|