OverviewFilesCommitsIssuesDocsReleasesPeers

favela-gateway / test / watch.test.js

62 lines · javascript ref:
// The watcher: checks on its own (agreed 2026-10-13 — a manual fix nobody hears about is the outage), keeps
// alert state on disk so "open since" survives a restart, and remembers what resolved.
import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import { createWatcher } from '../lib/watch.js'

const tmp = () => fs.mkdtempSync(path.join('/tmp', 'gw-watch-'))
const offline = () => ({ roles: {}, process: { rss: 1 }, bsky: { hostname: 'h', relay: { status: 'offline', seq: 1 }, dids: [] } })
const healthy = () => ({ roles: {}, process: { rss: 1 }, bsky: { hostname: 'h', relay: { status: 'active', seq: 1 }, dids: [] } })

test('★ a check opens alerts, persists them, and a new watcher (a restart) keeps "since"', async (t) => {
  const dir = tmp()
  let now = 1000
  const calls = []
  const w1 = createWatcher({ metrics: async (p) => { calls.push(p); return offline() }, stateFile: path.join(dir, 'alerts.json'), now: () => now, log: () => {} })
  await w1.checkNow()
  t.alike(calls, [{ refresh: true }], 'checks with FRESH probes, not the cache')
  t.alike(w1.current().open.map((a) => a.id), ['relay-offline'])
  w1.stop()
  now = 900000
  const w2 = createWatcher({ metrics: async () => offline(), stateFile: path.join(dir, 'alerts.json'), now: () => now, log: () => {} })
  t.is(w2.current().open[0].since, 1000, 'read back from disk before any new check')
  await w2.checkNow()
  t.is(w2.current().open[0].since, 1000, 'and kept by the next check')
  t.is(w2.current().lastCheckAt, 900000)
  w2.stop()
})

test('resolved alerts are remembered (newest first, bounded) for the panel', async (t) => {
  const dir = tmp()
  let m = offline()
  const w = createWatcher({ metrics: async () => m, stateFile: path.join(dir, 'a.json'), log: () => {} })
  t.teardown(() => w.stop())
  await w.checkNow()
  m = healthy()
  await w.checkNow()
  t.alike(w.current().open, [])
  t.alike(w.current().recentlyResolved.map((a) => a.id), ['relay-offline'])
})

test('a failing metrics call is logged and leaves the state untouched', async (t) => {
  const lines = []
  const w = createWatcher({ metrics: async () => { throw new Error('boom') }, stateFile: null, log: (o) => lines.push(o) })
  t.teardown(() => w.stop())
  await w.checkNow()
  t.ok(lines.some((l) => /boom/.test(l.error || '')))
  t.alike(w.current().open, [])
})

test('start() schedules the first check and the interval; stop() clears both', async (t) => {
  let n = 0
  const w = createWatcher({ metrics: async () => { n++; return healthy() }, stateFile: null, firstMs: 20, intervalMs: 30, log: () => {} })
  w.start()
  await new Promise((resolve) => setTimeout(resolve, 120))
  w.stop()
  const after = n
  t.ok(after >= 2, 'first check plus at least one interval check (' + after + ')')
  await new Promise((resolve) => setTimeout(resolve, 80))
  t.is(n, after, 'nothing runs after stop')
})