OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer / test / control.test.js

65 lines · javascript ref:
// favela-peer answers the sidecar's OTA push.
//
// ⛔ WHY (2026-10-12). favela-peer had no control surface, so the sidecar never knew where to send
// `release-ready`: an update installed (staged, since the daemon was running) and the daemon kept
// running the old code until someone restarted it by hand. tabertron and stortron were both still on
// 0.0.57 after 0.0.61 was published. INSTALLED != RUNNING, with nothing to close the gap.
//
// The contract (pgh-sidecar lib/index.js `_notifyAppsOfNewReleases`): the sidecar installs, then
// pushes `release-ready` ONLY to an app that registered a udsPath with `app-register`. The app
// answers, shuts down, and exits 75; the supervisor applies the staged tree and respawns.

import test from 'brittle'
import fs from '#node:fs'
import os from '#node:os'
import process from '#node:process'
import { join } from '#node:path'
import { Service, Client } from 'hyper-svc'
import { startControl } from '../lib/control.js'

const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-ctl-'))

// A stand-in for pgh-sidecar: records every app-register it receives.
async function fakeSidecar () {
  const dir = tmp()
  const sock = join(dir, 's.sock')
  const registered = []
  const svc = new Service({ name: 'pgh-sidecar', uds: sock })
  svc.handle('app-register', async (ctx, params) => { registered.push(params); return { ok: true } })
  await svc.listen()
  return { sock, registered, close: () => svc.close() }
}

async function until (fn, ms = 10000) {
  const end = Date.now() + ms
  while (Date.now() < end) { const v = await fn(); if (v) return v; await new Promise((r) => setTimeout(r, 25)) }
  return null
}

test('★ registers its control socket with the sidecar, and answers release-ready', async (t) => {
  const sidecar = await fakeSidecar()
  // Bare's process.env cannot delete a key, so the previous value is put back instead.
  const prev = process.env.PGH_SIDECAR_SOCK || join(os.homedir(), '.pgh', 'svc.sock')
  process.env.PGH_SIDECAR_SOCK = sidecar.sock
  const udsPath = join(tmp(), 'svc.sock')
  let released = null
  const control = await startControl({ udsPath, name: 'favela-peer', version: '0.0.0-test', onRelease: (v) => { released = v }, log: () => {} })
  t.teardown(async () => { await control.close(); await sidecar.close(); process.env.PGH_SIDECAR_SOCK = prev })

  t.ok(control, 'the control surface came up')
  const reg = await until(() => sidecar.registered.find((r) => r.udsPath === udsPath))
  t.ok(reg, 'the sidecar was told where to push release-ready — without this it never calls us')
  t.is(reg && reg.name, 'favela-peer')

  const c = await Client.connectUds(udsPath)
  const reply = await c.call('release-ready', { name: 'favela-peer', version: '9.9.9' })
  await c.close()
  t.alike(reply, { ok: true, willRespawn: true }, 'answered before exiting, so the sidecar sees ok, not a dropped connection')
  t.is(await until(() => released), '9.9.9', 'and the daemon was asked to shut down for the respawn')
})

test('fails soft: no control socket is not a reason to stop serving the feed', async (t) => {
  const control = await startControl({ udsPath: join(tmp(), 'no-such-dir', 'svc.sock'), name: 'favela-peer', onRelease: () => {}, log: () => {} })
  t.is(control, null, 'returns null instead of throwing')
})