OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / test / pointer-diag.test.mjs

245 lines · javascript ref:
// pointerDiag — does this node's LOCAL byte set match the CONVERGED pointer set?
//
// The pointer design splits a record in two: the autobase converges a { $oid, size } POINTER (so every
// co-owner agrees WHICH records exist), and the bytes live as content-addressed overlay files. The
// signed repo is built from the BYTES. Nothing ever checked that the two halves agree — and they can
// silently disagree, because ensureLocal returns a local file hit WITHOUT comparing it to the
// pointer's $oid:
//
//     async ensureLocal (collection, rkey, oid) {
//       const local = await this.get(collection, rkey)
//       if (local != null) return local          <-- returns superseded bytes; oid never consulted
//       ...fetch by oid...
//     }
//
// So a node holding an OLD copy of a record resolves it happily, never re-fetches, and signs it as
// truth — while its pointer set looks perfectly converged from every diagnostic surface.
//
// Measured on the live wj62 pair (2026-07-27): both co-owners held the SAME 139 converged pointers,
// both resolved all 139, and still produced DIFFERENT signed repos. They differed on exactly one
// record — app.bsky.actor.profile/self, two different avatar blob refs (6533 vs 5962 bytes) — so the
// signed content FLIPPED on every signer change, republishing the profile with a different avatar
// each time. Finding that required fetching two signed CARs and diffing them by hand, because no
// single-node call could answer "do my bytes match my pointers?". This is that call.
//
// Test 1 is the load-bearing one. It pins WHY this stayed invisible: the stale record still READS
// fine, so every read-path surface looks healthy. (reconcileBytes was equally blind — it skipped any
// record with a local file — but that is now fixed, so the repair is asserted in its own test below
// rather than as a control here.)
//
// Run: bare test/pointer-diag.test.mjs
import '../lib/bare-globals.js'
import test from 'brittle'
import fs from 'bare-fs'
import b4a from 'b4a'
import crypto from 'hypercore-crypto'
import { OverlayRepoStore } from 'atproto-store/lib/overlay-repo-store.js'
import { SwarmDevice } from '../lib/swarm-device.js'

const DID = 'did:plc:pointerdiag0000000000000'
const POST = 'app.bsky.feed.post'
const PROFILE = 'app.bsky.actor.profile'
const tmp = () => { const d = '/tmp/hpp-pdiag-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }

function memKV () {
  const m = new Map()
  return {
    _m: m,
    async set (k, v) { m.set(k, v) },
    async del (k) { m.delete(k) },
    async get (k) { return m.has(k) ? m.get(k) : null },
    async list (prefix = '') { const o = {}; for (const [k, v] of m) if (k.startsWith(prefix)) o[k] = v; return o },
    async listNow (prefix = '') { return this.list(prefix) },
    async getNow (k) { return this.get(k) },
    holdsSigner: () => false
  }
}

// Content-addressed transport. hashFile mirrors the real overlay's read-only hash — hyper-overlay's
// streaming hasher is documented as matching crypto.data(buffer), so this is the same oid registerFile
// would assign.
function mkOverlay (net) {
  return {
    async registerFile (overlayPath, diskPath) {
      const bytes = fs.readFileSync(diskPath)
      const oid = crypto.data(bytes).toString('hex')
      net.set(oid, bytes)
      return { contentHash: oid }
    },
    async fetchFile (oid, { destPath } = {}) {
      const bytes = net.get(oid)
      if (!bytes) return null
      fs.writeFileSync(destPath, bytes)
      return { path: destPath }
    },
    async hashFile (diskPath) {
      let bytes
      try { bytes = fs.readFileSync(diskPath) } catch { return null }
      return { contentHash: crypto.data(bytes).toString('hex'), size: bytes.length }
    },
    get peerCount () { return net.size ? 1 : 0 }
  }
}

// Author a record as the OTHER peer would and return the pointer that converges to us.
async function authorElsewhere (net, collection, rkey, record) {
  const store = await new OverlayRepoStore(tmp(), { overlay: mkOverlay(net) }).ready()
  const { oid, size } = await store.put(collection, rkey, record)
  return { $oid: oid, size }
}

const mkDevice = (sb, store) => new SwarmDevice(sb, { did: DID, privateKey: crypto.randomBytes(32), self: 'peerA', recordStore: store })

test('a STALE local file is reported — the wj62 profile/avatar divergence', async (t) => {
  const net = new Map()
  const store = await new OverlayRepoStore(tmp(), { overlay: mkOverlay(net), did: DID }).ready()
  const sb = memKV()
  const device = mkDevice(sb, store)

  // We authored the profile with avatar A and hold those bytes locally.
  const mine = { $type: PROFILE, displayName: 'wj62', avatar: { ref: { $link: 'bafkreiAAA' }, size: 6533 } }
  const { oid: localOid } = await store.put(PROFILE, 'self', mine)

  // The co-owner authored it with avatar B; THAT pointer is what converged into the shared autobase.
  const theirs = { $type: PROFILE, displayName: 'wj62', avatar: { ref: { $link: 'bafkreiBBB' }, size: 5962 } }
  const ptr = await authorElsewhere(net, PROFILE, 'self', theirs)
  await sb.set(PROFILE + '/self', ptr)
  t.not(ptr.$oid, localOid, 'precondition: the converged pointer names different bytes than we hold')

  // WHY IT WAS INVISIBLE (1): the record still reads, so every read-path surface looks healthy.
  t.alike(await store.get(PROFILE, 'self'), mine, 'CONTROL: the record resolves — to OUR superseded copy')

  const d = await device.pointerDiag()
  t.is(d.checked, 1, 'one record checked')
  t.is(d.stale, 1, 'the divergence IS reported')
  t.is(d.ok, 0, 'and it is not counted as healthy')
  t.is(d.entries.length, 1, 'the offending record is named')
  t.is(d.entries[0].key, PROFILE + '/self', 'names the exact record — the one wj62 differed on')
  t.is(d.entries[0].state, 'stale', 'classified as stale, not missing')
  t.is(d.entries[0].pointerOid, ptr.$oid, 'reports the pointer oid')
  t.is(d.entries[0].localOid, localOid, 'and the oid our bytes actually hash to')

  await store.close()
})

test('a matching local file is OK and is not listed', async (t) => {
  const net = new Map()
  const store = await new OverlayRepoStore(tmp(), { overlay: mkOverlay(net), did: DID }).ready()
  const sb = memKV()
  const device = mkDevice(sb, store)

  const rec = { $type: POST, text: 'converged', createdAt: '2026-07-27T00:00:00.000Z' }
  const { oid, size } = await store.put(POST, '3kaaa', rec)
  await sb.set(POST + '/3kaaa', { $oid: oid, size })

  const d = await device.pointerDiag()
  t.is(d.checked, 1, 'checked the record')
  t.is(d.ok, 1, 'local bytes match the pointer')
  t.is(d.stale, 0, 'nothing stale')
  t.is(d.entries.length, 0, 'a healthy store lists nothing — quiet in steady state')

  await store.close()
})

test('a converged pointer with no local bytes is MISSING, not stale (a lagging peer is not corrupt)', async (t) => {
  const net = new Map()
  const store = await new OverlayRepoStore(tmp(), { overlay: mkOverlay(net), did: DID }).ready()
  const sb = memKV()
  const device = mkDevice(sb, store)

  const ptr = await authorElsewhere(net, POST, '3kbbb', { $type: POST, text: 'not here yet', createdAt: '2026-07-27T00:00:00.000Z' })
  await sb.set(POST + '/3kbbb', ptr)

  const d = await device.pointerDiag()
  t.is(d.missing, 1, 'reported as missing')
  t.is(d.stale, 0, 'NOT conflated with a stale file — different fault, different fix')
  t.is(d.entries[0].state, 'missing', 'and classified as such')
  t.is(d.entries[0].localOid, null, 'with no local oid to report')

  // reconcileBytes is the right remedy for THIS one, and it clears it.
  await device.reconcileBytes()
  const after = await device.pointerDiag()
  t.is(after.ok, 1, 'once the bytes are pulled the record is healthy')
  t.is(after.missing, 0, 'and nothing is missing')

  await store.close()
})

test('reserved keys and tombstones are never treated as records', async (t) => {
  const net = new Map()
  const store = await new OverlayRepoStore(tmp(), { overlay: mkOverlay(net), did: DID }).ready()
  const sb = memKV()
  const device = mkDevice(sb, store)

  await sb.set('__commit__/3kzzz', { rev: '3kzzz', ops: [] })
  await sb.set('__members__', { a: 1 })
  await sb.set('__blob__/bafkreiXYZ', { $oid: 'deadbeef', size: 1 })
  await sb.set(POST + '/3kdel', { $deleted: true })

  const d = await device.pointerDiag()
  t.is(d.checked, 0, 'no reserved key was checked as a record')
  t.is(d.tombstones, 1, 'the tombstone was counted, not chased')
  t.is(d.stale, 0, 'and nothing was misreported as divergent')

  await store.close()
})

test('it is a PURE READ — it never writes, fetches, or repairs', async (t) => {
  const net = new Map()
  const store = await new OverlayRepoStore(tmp(), { overlay: mkOverlay(net), did: DID }).ready()
  const sb = memKV()
  const device = mkDevice(sb, store)

  const mine = { $type: POST, text: 'ours', createdAt: '2026-07-27T00:00:00.000Z' }
  await store.put(POST, '3kccc', mine)
  const ptr = await authorElsewhere(net, POST, '3kccc', { $type: POST, text: 'theirs', createdAt: '2026-07-27T00:00:01.000Z' })
  await sb.set(POST + '/3kccc', ptr)

  const before = JSON.stringify([...sb._m.entries()])
  await device.pointerDiag()
  await device.pointerDiag()

  t.is(JSON.stringify([...sb._m.entries()]), before, 'the pointer set is untouched — nothing appended to the autobase')
  t.alike(await store.get(POST, '3kccc'), mine, 'the local bytes are untouched — it diagnoses, it does not repair')
  t.is((await device.pointerDiag()).stale, 1, 'and it keeps reporting the divergence rather than hiding it')

  await store.close()
})

test('no record store (legacy backend) skips cleanly rather than throwing', async (t) => {
  const device = new SwarmDevice(memKV(), { did: DID, privateKey: crypto.randomBytes(32), self: 'peerA' })
  const d = await device.pointerDiag()
  t.is(d.skipped, 'no-record-store', 'a legacy node reports why it cannot answer')
  t.absent(d.stale, 'and asserts nothing about staleness')
})

test('reconcileBytes now REPAIRS a stale file (presence is not agreement)', async (t) => {
  const net = new Map()
  const store = await new OverlayRepoStore(tmp(), { overlay: mkOverlay(net), did: DID }).ready()
  const sb = memKV()
  const device = mkDevice(sb, store)

  const mine = { $type: PROFILE, displayName: 'wj62', avatar: { ref: { $link: 'bafkreiAAA' }, size: 6533 } }
  const theirs = { $type: PROFILE, displayName: 'wj62', avatar: { ref: { $link: 'bafkreiBBB' }, size: 5962 } }
  await store.put(PROFILE, 'self', mine)
  const ptr = await authorElsewhere(net, PROFILE, 'self', theirs)
  await sb.set(PROFILE + '/self', ptr)

  t.is((await device.pointerDiag()).stale, 1, 'precondition: the local copy is superseded')

  const r = await device.reconcileBytes()
  t.is(r.stale, 1, 'the stale record is SEEN — it used to be skipped because a file was present')
  t.is(r.fetched, 1, 'and repaired')
  t.alike(await store.get(PROFILE, 'self'), theirs, 'the authoritative record is now local')

  const after = await device.pointerDiag()
  t.is(after.stale, 0, 'nothing stale remains')
  t.is(after.ok, 1, 'the record now matches the converged pointer')

  const second = await device.reconcileBytes()
  t.is(second.stale, 0, 'a converged pass reports no staleness')
  t.is(second.fetched, 0, 'and fetches nothing — no churn once verified')

  await store.close()
})