OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / schema-guard.test.js

113 lines · javascript ref:
// In-band schema-version guard (#52 Phase 4.4 hardening). Proves the origin
// stamps the schema version into the replicated OrgEventLog, a node reads it
// back, and an under-versioned node detects it's BEHIND (Keet's interrupt) —
// while the marker stays wire-safe (op=3 with no orgBootstrapKey, so the
// existing OrgPuller consumer ignores it).

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

import { OrgEventLog } from '../lib/services/org-event-log.js'
import { OrgPuller } from '../lib/services/org-mirror.js'
import {
  announceSchemaVersion, readDeclaredSchemaVersion, checkSchemaGuard,
  isSchemaVersionEntry, SCHEMA_VERSION_OP
} from '../lib/services/schema-guard.js'

async function makeLog (t) {
  const store = new Corestore(mkdtempSync(join(tmpdir(), 'schema-guard-')))
  const log = new OrgEventLog({ corestore: store })
  await log.ready()
  t.teardown(async () => { try { await log.close() } catch {}; try { await store.close() } catch {} })
  return log
}

test('announce stamps the version; read returns it; idempotent', async (t) => {
  const log = await makeLog(t)
  t.is(await readDeclaredSchemaVersion(log), 0, 'no marker yet → v0')

  t.is(await announceSchemaVersion(log, 2), true, 'announced v2')
  t.is(await readDeclaredSchemaVersion(log), 2, 'log declares v2')

  t.is(await announceSchemaVersion(log, 2), false, 'idempotent — no second marker for the same version')
  t.is(log.length, 1, 'exactly one marker entry')

  t.is(await announceSchemaVersion(log, 3), true, 'a higher version IS announced')
  t.is(await readDeclaredSchemaVersion(log), 3, 'log now declares v3')
})

test('checkSchemaGuard: a node behind the declared schema is flagged', async (t) => {
  const log = await makeLog(t)
  await announceSchemaVersion(log, 2)

  const ok = await checkSchemaGuard(log, 2)
  t.is(ok.behind, false, 'v2 code on a v2 log is NOT behind')
  t.is(ok.declared, 2)

  const behind = await checkSchemaGuard(log, 1)
  t.is(behind.behind, true, 'v1 code on a v2 log IS behind (must refuse to serve stale)')
  t.is(behind.declared, 2)
  t.is(behind.supported, 1)

  const ahead = await checkSchemaGuard(log, 3)
  t.is(ahead.behind, false, 'v3 code on a v2 log is ahead → fine (it will announce v3)')
})

test('marker is wire-safe: OrgPuller ignores it (no org mounted)', async (t) => {
  const log = await makeLog(t)
  await announceSchemaVersion(log, 2)
  // also a real org entry, to prove the puller still mounts orgs around markers
  await log.append({ op: 0, name: 'realorg', orgBootstrapKey: Buffer.alloc(32, 7) })

  const mounted = []
  const orgs = { mountOrg: async (name) => { mounted.push(name) } }
  const puller = new OrgPuller({ issueRegistry: {}, orgs, log })
  const seen = []
  puller._onProgress = (p) => seen.push(p.name)
  await puller.start()
  // drain a couple of entries (live loop) then stop
  await new Promise(r => setTimeout(r, 150))
  await puller.close()

  t.absent(mounted.includes('@schema-version:2'), 'schema marker was NOT treated as an org')
  t.ok(mounted.includes('realorg'), 'real org around the marker still mounted')
})

test('OrgPuller live interrupt: a stale node stops binding after a newer-schema marker', async (t) => {
  const log = await makeLog(t)
  // origin declares v2, THEN announces a real org (order matters: the stale
  // mirror must stop binding orgs that arrive after it learns it's behind).
  await announceSchemaVersion(log, 2)
  await log.append({ op: 0, name: 'after-marker', orgBootstrapKey: Buffer.alloc(32, 9) })

  const mounted = []
  const orgs = { mountOrg: async (name) => { mounted.push(name) } }
  let behindInfo = null
  const puller = new OrgPuller({
    issueRegistry: {},
    orgs,
    log,
    supportedVersion: 1, // this node only supports v1 — it's STALE vs the v2 log
    onBehind: (info) => { behindInfo = info }
  })
  await puller.start()
  await new Promise(r => setTimeout(r, 200))
  await puller.close()

  t.ok(behindInfo, 'onBehind fired')
  t.is(behindInfo.declared, 2, 'declared v2')
  t.is(behindInfo.supported, 1, 'supported v1')
  t.absent(mounted.includes('after-marker'),
    'stale node refused to bind the org that arrived after the newer-schema marker (interrupt)')
})

test('SCHEMA_VERSION_OP and detection helpers', async (t) => {
  t.is(SCHEMA_VERSION_OP, 3, 'distinct from 0(created)/2(deleted)')
  t.ok(isSchemaVersionEntry({ op: 3, name: '@schema-version:2' }), 'detects a marker')
  t.absent(isSchemaVersionEntry({ op: 0, name: 'realorg' }), 'a real org is not a marker')
  t.absent(isSchemaVersionEntry({ op: 3, name: 'not-a-version' }), 'op 3 without prefix is not a marker')
})