// 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')
})