OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-bridge / pds / test / control-evict.test.js

81 lines · javascript ref:
// 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`)
  }
})