// The gateway's own CLI: `favela-gateway <verb> [key=value …]` talks to the RUNNING gateway's control
// socket — no pgh in between (operator request, 2026-10-13).
//
// ⛔ A CLIENT NEVER BOOTS. An earlier audit agent ran `favela-peer --help` and booted a second daemon on a
// live store. Here any argument means "client"; only the bare command (what pgh's supervisor runs) is
// the daemon — and that refuses when a gateway already answers (bin/favela-gateway.js).
import test from 'brittle'
import { runCli, parseParams, renderMetrics } from '../lib/cli.js'
function capture () {
const lines = []
return { lines, write: (s) => lines.push(String(s)), text: () => lines.join('\n') }
}
// A fake connection answering verbs from a table, recording what was asked.
function fakeConnect (answers, calls = []) {
return async () => ({
call: async (verb, params) => {
calls.push({ verb, params })
if (!(verb in answers)) throw new Error('unknown method: ' + verb)
const a = answers[verb]
return typeof a === 'function' ? a(params) : a
},
close: async () => {}
})
}
test('parseParams: key=value pairs, with numbers, booleans and JSON values typed', (t) => {
t.alike(parseParams(['did=did:plc:x', 'limit=5', 'force=true', 'raw={"a":1}']), { did: 'did:plc:x', limit: 5, force: true, raw: { a: 1 } })
t.alike(parseParams([]), {})
t.exception(() => parseParams(['nonsense']), /key=value/, 'a bare word is refused, not guessed at')
})
test('★ a verb is called on the socket and its reply printed', async (t) => {
const calls = []
const out = capture()
const code = await runCli(['probe-front', 'did=did:plc:x'], { sock: '/x', out: out.write, err: out.write, connect: fakeConnect({ 'probe-front': { ok: true, did: 'did:plc:x' } }, calls) })
t.is(code, 0)
t.alike(calls, [{ verb: 'probe-front', params: { did: 'did:plc:x' } }])
t.ok(out.text().includes('"did": "did:plc:x"'), 'the reply, as readable JSON')
})
test('help lists the verbs the RUNNING gateway serves', async (t) => {
const out = capture()
await runCli(['help'], { sock: '/x', out: out.write, err: out.write, connect: fakeConnect({ verbs: { verbs: ['status', 'metrics', 'invite'] } }) })
t.ok(out.text().includes('metrics') && out.text().includes('invite'))
})
test('⛔ a gateway that is not running says so, and how to start it — exit 1, nothing booted', async (t) => {
const out = capture()
const code = await runCli(['status'], { sock: '/x', out: out.write, err: out.write, connect: async () => { const e = new Error('connect ENOENT /x'); e.code = 'ENOENT'; throw e } })
t.is(code, 1)
t.ok(/not running/.test(out.text()) && /pgh app start favela-gateway/.test(out.text()))
})
test('a verb the gateway does not serve fails with the gateway\'s own error', async (t) => {
const out = capture()
const code = await runCli(['nope'], { sock: '/x', out: out.write, err: out.write, connect: fakeConnect({}) })
t.is(code, 1)
t.ok(/unknown method: nope/.test(out.text()))
})
const sampleMetrics = {
version: '0.2.0',
process: { uptimeS: 3725, rss: 196e6, heapUsed: 60e6, heapTotal: 80e6 },
roles: { membrane: { up: true }, fediverse: { up: false, error: 'fediverse config: AP_HOST is required' } },
membrane: {
dids: [{ did: 'did:plc:wj62ebbusln6gxo5azo6ogzo', handle: 'p2p.7rnx.net', rev: '3mrhxnkbhv622', fronts: 2, live: 1, lastAppendAt: 1e12, forkRejected: 0, denied: 0 }],
relay: { emitted: 40267, seqBase: 35200 },
guard: { configured: true, lastAt: 1e12, last: { relayStatus: 'active' }, heals: 1, lastHealAt: 1e12 }
},
fediverse: null,
bsky: { at: 1e12, hostname: 'pds.7rnx.net', relay: { status: 'active', seq: 40267 }, dids: [{ did: 'did:plc:wj62ebbusln6gxo5azo6ogzo', relayRev: '3mrhxnkbhv622', inSync: true, plcPds: 'https://pds.7rnx.net', handle: 'p2p.7rnx.net', appview: { resolves: true, postsCount: 201, newestIndexedAt: '2026-09-15T14:05:39Z' } }], problems: ['did:plc:x: the relay is behind'] }
}
test('★ metrics renders as a readable summary: roles, bsky, peers, problems', (t) => {
const s = renderMetrics(sampleMetrics, { now: 1e12 + 120e3 })
t.ok(/fediverse +DOWN.*AP_HOST is required/.test(s), 'a down role, with why')
t.ok(/relay +active/.test(s) && s.includes('40267'), 'bsky relay status and seq')
t.ok(s.includes('p2p.7rnx.net') && s.includes('in sync'), 'per DID: handle and relay sync')
t.ok(/1\/2 live/.test(s), 'peers: live of cached fronts')
t.ok(/2m ago/.test(s), 'last append as an age, not a timestamp')
t.ok(/problems/i.test(s) && s.includes('the relay is behind'), 'the named problems, last')
})
test('metrics --json prints the raw answer', async (t) => {
const out = capture()
await runCli(['metrics', '--json'], { sock: '/x', out: out.write, err: out.write, connect: fakeConnect({ metrics: sampleMetrics }) })
t.is(JSON.parse(out.text()).version, '0.2.0')
})
test('alerts render first, each with its fix', async (t) => {
const { renderAlerts } = await import('../lib/cli.js')
const s = renderAlerts({ lastCheckAt: 1e12, open: [{ id: 'relay-newer:x', severity: 'critical', title: 'relay newer', detail: 'relay z, ours a', fix: 'favela-gateway emit-sync did=x', since: 1e12 - 7200e3 }], recentlyResolved: [] }, { now: 1e12 })
t.ok(/CRITICAL relay newer/.test(s))
t.ok(s.includes('fix: favela-gateway emit-sync did=x'))
t.ok(/open 2h/.test(s), 'how long it has been open')
})