OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / org-event-log.test.js

180 lines · javascript ref:
// Unit tests for the OrgEventLog wire codec and the read/append
// surface. Mirror of repo-event-log's tests, scaled to the org schema
// (no `head` field).

import test from 'brittle'
import c from 'compact-encoding'
import { mkdtempSync } from '#node:fs'
import { join } from '#node:path'
import { tmpdir } from '#node:os'
import Corestore from 'corestore'

import { entry as entrySchema } from '../lib/services/org-event-log-messages.js'
import { OrgEventLog } from '../lib/services/org-event-log.js'

function tmpStorage () {
  return mkdtempSync(join(tmpdir(), 'pgv3-org-event-log-'))
}

test('codec round-trip with issueBootstrapKey present', (t) => {
  const key = Buffer.alloc(32, 0xab)
  const encoded = c.encode(entrySchema, {
    op: 0,
    name: 'hyper-fleeky-dev',
    ts: 1234567890,
    issueBootstrapKey: key
  })
  const decoded = c.decode(entrySchema, encoded)
  t.is(decoded.op, 0)
  t.is(decoded.name, 'hyper-fleeky-dev')
  t.is(decoded.ts, 1234567890)
  t.ok(decoded.issueBootstrapKey, 'issueBootstrapKey present')
  t.is(decoded.issueBootstrapKey.length, 32)
  t.ok(decoded.issueBootstrapKey.equals(key), 'key bytes match')
})

test('codec round-trip with issueBootstrapKey null', (t) => {
  const encoded = c.encode(entrySchema, {
    op: 1,
    name: 'updates-no-key',
    ts: 7,
    issueBootstrapKey: null
  })
  const decoded = c.decode(entrySchema, encoded)
  t.is(decoded.op, 1)
  t.is(decoded.name, 'updates-no-key')
  t.is(decoded.ts, 7)
  t.is(decoded.issueBootstrapKey, null, 'no key marker round-trips as null')
})

test('codec accepts hex-string issueBootstrapKey on encode', (t) => {
  const hex = 'cd'.repeat(32)
  const encoded = c.encode(entrySchema, {
    op: 0,
    name: 'foo',
    ts: 0,
    issueBootstrapKey: hex
  })
  const decoded = c.decode(entrySchema, encoded)
  t.ok(decoded.issueBootstrapKey.equals(Buffer.from(hex, 'hex')),
    'hex string accepted on encode, decoded as buffer')
})

test('OrgEventLog: writer appends, reader replays from seq 0', async (t) => {
  const store = new Corestore(tmpStorage())
  await store.ready()

  const writer = new OrgEventLog({ corestore: store })
  await writer.ready()
  t.ok(writer.writable, 'fresh log is writable')
  t.is(writer.length, 0, 'starts empty')

  const k1 = Buffer.alloc(32, 0x11)
  const k2 = Buffer.alloc(32, 0x22)
  await writer.append({ op: 0, name: 'org-a', issueBootstrapKey: k1 })
  await writer.append({ op: 0, name: 'org-b', issueBootstrapKey: k2 })
  await writer.append({ op: 0, name: 'org-c', issueBootstrapKey: null })
  t.is(writer.length, 3, 'three entries appended')

  const seen = []
  for await (const e of writer.entries({ live: false, start: 0 })) {
    seen.push(e)
  }
  t.is(seen.length, 3, 'replay yields three entries')
  t.is(seen[0].name, 'org-a')
  t.ok(seen[0].issueBootstrapKey.equals(k1))
  t.is(seen[1].name, 'org-b')
  t.ok(seen[1].issueBootstrapKey.equals(k2))
  t.is(seen[2].name, 'org-c')
  t.is(seen[2].issueBootstrapKey, null, 'null key round-trips')

  await writer.close()
  await store.close()
})

test('OrgPuller mounts the per-org RECORD autobase for each entry (#37/#42)', async (t) => {
  // Single-process integration: a writer log, a fake HyperOrgs, an OrgPuller.
  // Append entries and watch the puller mountOrg() each one. Same shape as
  // RepoPuller but without the git pull.
  const { OrgPuller } = await import('../lib/services/org-mirror.js')
  const store = new Corestore(tmpStorage())
  await store.ready()
  const writer = new OrgEventLog({ corestore: store })
  await writer.ready()

  // The contract this asserts changed with #37/#42 and the test was never
  // updated — it still expected bindOrgBootstrapKey + getForOrg, which
  // OrgPuller stopped calling. Orgs are now ONE autobase per org, and org
  // issues live INSIDE that record autobase (linked-core #42), so there is no
  // separate per-org issue tracker to bind: mounting the record autobase
  // brings the issues with it. The only thing the puller does per entry is
  // orgs.mountOrg(name, orgBootstrapKeyHex) — see lib/services/org-mirror.js.
  const mounted = new Map()
  const fakeOrgs = {
    async mountOrg (name, keyHex) { mounted.set(name, keyHex) }
  }
  // Still required by the constructor, but no longer called on this path.
  const fakeIssueRegistry = { bindOrgBootstrapKey () {}, async getForOrg () { return {} } }

  const puller = new OrgPuller({
    issueRegistry: fakeIssueRegistry,
    orgs: fakeOrgs,
    log: writer
  })
  await puller.start()

  const k1 = Buffer.alloc(32, 0xaa)
  const k2 = Buffer.alloc(32, 0xbb)
  await writer.append({ op: 0, name: 'org-one', orgBootstrapKey: k1 })
  await writer.append({ op: 0, name: 'org-two', orgBootstrapKey: k2 })

  // Wait for the puller's live read loop to drain. The loop is fire-
  // and-forget, so poll briefly for state to settle.
  for (let i = 0; i < 40; i++) {
    if (mounted.size === 2) break
    await new Promise((r) => setTimeout(r, 50))
  }

  t.is(mounted.get('org-one'), k1.toString('hex'),
    'org-one RECORD autobase mounted from the log entry')
  t.is(mounted.get('org-two'), k2.toString('hex'),
    'org-two RECORD autobase mounted from the log entry')
  t.alike([...mounted.keys()].sort(), ['org-one', 'org-two'],
    'one mount per entry')

  // Entries with no orgBootstrapKey are silently skipped — mounting needs a key.
  await writer.append({ op: 1, name: 'updated-no-key', orgBootstrapKey: null })
  await new Promise((r) => setTimeout(r, 200))
  t.absent(mounted.has('updated-no-key'),
    'no-key entries do not trigger a mount (defensive — real flow always carries a key on op=0)')

  await puller.close()
  await writer.close()
  await store.close()
})

test('OrgEventLog: read-only replica refuses append', async (t) => {
  const store = new Corestore(tmpStorage())
  await store.ready()

  const writer = new OrgEventLog({ corestore: store })
  await writer.ready()
  await writer.append({ op: 0, name: 'org-a', issueBootstrapKey: null })
  const writerKey = writer.keyHex

  const store2 = new Corestore(tmpStorage())
  await store2.ready()
  const replica = new OrgEventLog({ corestore: store2, key: writerKey })
  await replica.ready()
  t.absent(replica.writable, 'replica opened by key is not writable')

  await t.exception(async () => {
    await replica.append({ op: 0, name: 'should-fail', issueBootstrapKey: null })
  }, 'append on non-writable throws')

  await writer.close()
  await replica.close()
  await store.close()
  await store2.close()
})