// The operator MUST be able to evict a front over the UDS control plane — without restarting the process.
//
// Twice on 2026-07-27 the only way to stop the membrane serving a wrong head (a peer announcing a FORKED
// line whose rev sorts higher, so freshest-by-rev kept picking it) was to bounce atproto-bridge. That is
// not acceptable for a production read surface: a restart drops every OTHER fronted DID's warm replica too.
// See hyper-social#16 P0. The router-level behaviour (fallback reads, denial across re-announce, age-out)
// is proven over a real testnet in atproto-did-peer/test/evict.test.mjs; this asserts the CONTROL SURFACE
// is wired to it, since a capability with no operator surface is one nobody can use at 3am.
import test from 'brittle'
import { mkdtempSync, rmSync } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'
import { Client } from 'hyper-svc'
import { startControl } from '../lib/control.js'
import { mkDidRouter } from './mk-did-router.js'
const DID = 'did:plc:evictme000000000000000'
test('control plane: drop-front / allow-front / forget-did evict a cached front in place', async (t) => {
const fakeSwarm = { on () {}, off () {}, join () { return { flushed: async () => {} } }, flush: async () => {}, connections: new Set() }
const router = mkDidRouter(fakeSwarm, {}, { fronted: [DID], refreshMs: 0 })
// Two co-owners of one DID, as the bridge caches them: 'good' + 'forked' (the higher-rev fork).
const mkFront = (rev) => ({ handle: null, _repoKey: null, head: { rev }, seq: { core: { length: 3 } }, rpc: null, close: async () => {} })
router._fronts.set(DID, new Map([['good', mkFront('3aaa')], ['forked', mkFront('3zzz')]]))
router._peers.set(DID, router._fronts.get(DID).get('forked'))
router._byDid.set(DID, new Map([['good', {}], ['forked', {}]]))
const udsDir = mkdtempSync(join(tmpdir(), 'evict-uds-'))
const uds = join(udsDir, 'svc.sock')
const control = await startControl({ uds, name: 'atproto-bridge-evict-test', peers: router })
const cli = await Client.connectUds(uds)
t.teardown(async () => {
await cli.close(); await control.close()
rmSync(udsDir, { recursive: true, force: true })
})
t.is(router._freshestFront(DID).head.rev, '3zzz', 'the forked (higher-rev) front is what reads resolve to')
// --- drop-front with forget: evict the fork and keep it out ---
const dropped = await cli.call('drop-front', { did: DID, firehoseKey: 'forked', forget: true })
t.ok(dropped.ok, 'drop-front returns ok')
t.alike(dropped.dropped, ['forked'], 'drop-front reports the evicted firehose key')
t.is(router._freshestFront(DID).head.rev, '3aaa', 'reads now resolve to the good co-owner — no restart')
t.absent(router._fronts.get(DID).has('forked'), 'the evicted front is out of the co-owner set')
t.ok(router._isDenied(DID, 'forked'), 'the evicted firehose key is DENIED (a re-announce cannot resurrect it)')
// The primary must have been re-pointed off the closed front, not left dangling.
t.is(router._peers.get(DID), router._fronts.get(DID).get('good'), 'primary re-pointed at a surviving front')
const diag = await cli.call('diagnose')
t.alike(diag.denied[DID], ['forked'], 'diagnose SHOWS the denial (an operator can see what is evicted)')
// --- allow-front: the undo ---
const allowed = await cli.call('allow-front', { did: DID, firehoseKey: 'forked' })
t.alike(allowed.allowed, ['forked'], 'allow-front clears the denial')
t.absent(router._isDenied(DID, 'forked'), 'no longer denied — the next announce may re-front it')
// --- forget-did: drop everything cached for the DID, deny nothing ---
const forgot = await cli.call('forget-did', { did: DID })
t.ok(forgot.ok, 'forget-did returns ok')
t.alike(forgot.dropped, ['good'], 'forget-did drops the remaining front')
t.absent(router._fronts.has(DID), 'no cached fronts remain for the DID')
t.absent(router._byDid.has(DID), 'identified connections cleared — the next request re-fronts from scratch')
t.ok(router.isFronted(DID), 'the DID is STILL fronted (forget-did must not make it unservable)')
t.absent(router._denied.has(DID), 'forget-did denies nothing')
})
test('control plane: eviction handlers fail closed without a did / router', async (t) => {
const udsDir = mkdtempSync(join(tmpdir(), 'evict-uds2-'))
const uds = join(udsDir, 'svc.sock')
const control = await startControl({ uds, name: 'atproto-bridge-evict-test2', peers: null })
const cli = await Client.connectUds(uds)
t.teardown(async () => { await cli.close(); await control.close(); rmSync(udsDir, { recursive: true, force: true }) })
for (const m of ['drop-front', 'allow-front', 'forget-did']) {
const r = await cli.call(m, {})
t.absent(r.ok, `${m} with no peers router reports not-ok instead of throwing`)
}
})