OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / test / diag.test.js

171 lines · javascript ref:
// diag — one read-only JSON blob per hosted DID, over each DID's owner-gated control socket.
//
// Inspecting a running daemon used to mean ssh + a heredoc + a throwaway .mjs with absolute-path imports
// into the installed tree; a whole working day went into that on 2026-07-27 (and one heredoc died because
// a paste indented the closing EOF). The daemon already answered get-state / members-diag / pointer-diag —
// nothing collected them. These tests pin the properties that make the collected blob trustworthy:
//
//   1. all three sections arrive for a hosted DID, and the summary rolls up the fields an operator scans
//      first (rev, writable, amCreator/forcedRole, stale/missing).
//   2. the per-DID socket path is resolved the way the daemon lays it out — primary DID on the FLAT
//      control.sock, every other DID under dids/<did16>/. Getting this wrong looks exactly like "the
//      daemon isn't running", which is the most expensive way to be wrong here.
//   3. ONE failure never eats the report: an unreachable DID, or a single op that throws, still leaves
//      every other DID and every other section reported — a node with a wedged pointer store must still
//      be able to tell you its state and roster, because that is usually what says WHY it is wedged.
//
// Run: bare test/diag.test.js
import '../lib/bare-globals.js'
import test from 'brittle'
import fs from 'bare-fs'
import { join } from 'bare-path'
import { serveDeviceUds } from '../lib/serve-device-uds.js'
import { collectLocalDiag, summarize } from '../lib/diag.js'
import { did16For } from '../lib/control-socket.js'

const PRIMARY = 'did:plc:diagprimary00000000000000'
const SECOND = 'did:plc:diagsecond000000000000000'
const tmp = () => { const d = '/tmp/hpp-diag-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }

// The three reads a real SwarmDevice answers. These shapes mirror lib/swarm-device.js state() and
// membersDiag() FIELD FOR FIELD — an invented stub shape is not coverage: the first version of this stub
// returned `{rev, records}`, the summary was written against it, and running the real thing printed
// `rev: null` with `records` as the key LIST. The stub is only worth something if it lies the same way
// the daemon does.
function stubDevice (did, { stale = 0, amCreator = true, forcedRole = null, throwOn = null } = {}) {
  return {
    did,
    async state () {
      if (throwOn === 'get-state') throw new Error('state exploded')
      return {
        did,
        self: 'peerA',
        holdsLease: false, holdsSigner: true,
        leaseHolder: 'peerA',
        leaseEpoch: 3,
        indexers: 1,
        writable: true,
        head: { rev: '3ke6kgap4u222', commitCid: 'bafyreiabc' },
        recordCount: 7,
        records: [{ collection: 'app.bsky.feed.post', rkey: 'a' }]
      }
    },
    async membersDiag () {
      if (throwOn === 'members-diag') throw new Error('members exploded')
      return {
        did,
        writable: true,
        amCreator,
        forcedRole,
        baseLength: 122287,
        indexedLength: 122287,
        // Per-writer detail: the aggregate above cannot say WHO accounts for a gap. Two writers, one of
        // them carrying blocks the view has not consumed.
        writers: [
          { key: 'aaaaaaaaaaaa', authored: 100, seen: 100, pending: 0, indexer: true, removed: false, local: true },
          { key: 'bbbbbbbbbbbb', authored: 146, seen: 100, pending: 46, indexer: false, removed: false, local: false }
        ],
        membersProjected: true,
        rosterLen: 2,
        roster: [{ key: 'f4a7f860', role: 'owner' }, { key: 'b825e3d2', role: 'writer' }],
        membersInView: [{ key: 'f4a7f860' }],
        devicesInView: null
      }
    },
    async pointerDiag ({ limit = 0 } = {}) {
      if (throwOn === 'pointer-diag') throw new Error('pointer store wedged')
      return { did, checked: 7, ok: 7 - stale, stale, missing: 0, unresolvable: 0, tombstones: 0, entries: [], limit }
    }
  }
}

async function serveAt (t, device, uds) {
  fs.mkdirSync(uds.slice(0, uds.lastIndexOf('/')), { recursive: true })
  const srv = await serveDeviceUds(device, { uds })
  t.teardown(async () => { try { await srv.close() } catch {} })
  return srv
}

test('diag collects state + members + pointers for a hosted DID, and summarizes them', async (t) => {
  const storage = tmp()
  const cfg = { dids: [{ did: PRIMARY, primary: true }] }
  await serveAt(t, stubDevice(PRIMARY, { stale: 2, forcedRole: 'forced-non-creator', amCreator: false }), join(storage, 'control.sock'))

  const rows = await collectLocalDiag({ cfg, storage, dids: [PRIMARY] })
  t.is(rows.length, 1, 'one row per hosted DID')
  const r = rows[0]
  t.absent(r.unreachable, 'the DID was reachable over its control socket')
  t.ok(r.state && r.state.head && r.state.head.rev === '3ke6kgap4u222', 'get-state is in the blob')
  t.ok(r.members && Array.isArray(r.members.roster), 'members-diag is in the blob')
  t.ok(r.pointers && r.pointers.checked === 7, 'pointer-diag is in the blob')
  t.absent(r.errors, 'no per-op errors when every op answers')

  // The summary is what an operator reads first — it must carry the role AND the byte health, because
  // those are the two things that were unknowable at a glance during the loser-club/wj62 incidents.
  const s = summarize(PRIMARY, r)
  t.is(s.rev, '3ke6kgap4u222', 'summary reads rev off state.head.rev — the field the daemon ACTUALLY returns')
  t.is(s.records, 7, 'summary reads the record COUNT (recordCount), not the key list')
  t.is(s.indexed, '122287/122287', 'summary carries the indexed/base pair (stuck vs slow, at a glance)')
  t.is(s.writers, 2, 'summary carries the writer COUNT')
  t.is(s.writersPending, 46, 'and the total blocks authored-but-not-in-the-view — what attributes a gap')
  t.alike(r.members.writers.find((w) => w.pending > 0).key, 'bbbbbbbbbbbb', 'the full section names WHICH writer is behind')
  t.is(s.writable, true, 'summary carries writable')
  t.is(s.roster, 2, 'summary carries the roster size')
  t.is(s.membersInView, true, 'summary says whether __members__ is in the linearized view')
  t.is(s.devicesInView, false, 'summary says whether __devices__ is in the view (empty on wj62)')
  t.is(s.amCreator, false, 'summary carries the HONEST creator role (_isCreator, not the prototype method)')
  t.is(s.forcedRole, 'forced-non-creator', 'summary carries an explicit role override')
  t.is(s.stale, 2, 'summary carries the stale-byte count (the wj62 signature)')
  t.is(s.reachable, true, 'summary marks the DID reachable')
})

test('the per-DID control socket is resolved as the daemon lays it out', async (t) => {
  const storage = tmp()
  const cfg = { dids: [{ did: PRIMARY, primary: true }, { did: SECOND }] }
  await serveAt(t, stubDevice(PRIMARY), join(storage, 'control.sock'))
  await serveAt(t, stubDevice(SECOND), join(storage, 'dids', did16For(SECOND), 'control.sock'))

  const rows = await collectLocalDiag({ cfg, storage, dids: [PRIMARY, SECOND] })
  t.is(rows.length, 2, 'both hosted DIDs reported')
  t.ok(rows[0].socket.endsWith('/control.sock') && !rows[0].socket.includes('/dids/'), 'the PRIMARY DID keeps the flat control.sock')
  t.ok(rows[1].socket.includes('/dids/' + did16For(SECOND) + '/'), 'a non-primary DID is read under dids/<did16>/')
  t.absent(rows[0].unreachable, 'primary reachable')
  t.absent(rows[1].unreachable, 'non-primary reachable')
  t.is(rows[1].state.did, SECOND, 'the non-primary row carries ITS own state, not the primary\'s')
})

test('one failure never eats the report', async (t) => {
  const storage = tmp()
  const missing = 'did:plc:diagmissing00000000000000'
  const cfg = { dids: [{ did: PRIMARY, primary: true }, { did: missing }] }
  // PRIMARY is up but its pointer store is wedged; `missing` has no socket at all.
  await serveAt(t, stubDevice(PRIMARY, { throwOn: 'pointer-diag' }), join(storage, 'control.sock'))

  const rows = await collectLocalDiag({ cfg, storage, dids: [PRIMARY, missing] })
  const up = rows.find((r) => r.did === PRIMARY)
  const down = rows.find((r) => r.did === missing)

  t.ok(up.state && up.members, 'a wedged pointer store still reports state AND roster — usually what says why')
  t.absent(up.pointers, 'the failing section is absent rather than faking a value')
  t.ok(up.errors && /wedged/.test(up.errors['pointer-diag']), 'the failing op NAMES its error')

  t.ok(down.unreachable && /control socket/.test(down.unreachable), 'an unreachable DID says so, with the path it looked at')
  t.absent(down.state, 'and reports no state for it')
  t.is(rows.length, 2, 'the reachable DID is still reported alongside the unreachable one')
})

// A signing node reports holdsLease FALSE — the __lease__ heartbeat was replaced by the presence
// election and the field is vestigial. Surfacing it in the summary beside `rev` cost a real session
// (2026-07-29) chasing a signing wedge that did not exist: both co-owners read holdsLease:false while
// federating to bsky perfectly. The summary must report `signs` (the gate sign() consults) and must NOT
// report holdsLease at all.
{
  const row = summarize('did:plc:x', { state: { head: { rev: 'r1' }, holdsLease: false, holdsSigner: true, recordCount: 2, writable: true } })
  if (row.signs !== true) throw new Error('FAIL: summary must report `signs` from holdsSigner')
  if ('holdsLease' in row) throw new Error('FAIL: summary must NOT surface the legacy holdsLease field')
  console.log('  ok - summary reports `signs`, never the legacy holdsLease')
  const unknown = summarize('did:plc:y', { state: { head: null, recordCount: 0 } })
  if (unknown.signs !== null) throw new Error('FAIL: absent holdsSigner must be null, not false')
  console.log('  ok - an older daemon that does not report holdsSigner reads null, not a confident false')
}