// 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()
})