OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / migrate-real-orgs.test.js

92 lines · javascript ref:
// END-TO-END migration verification against PROD'S REAL orgs data.
//
// Seeds an old-format (single-autobase) orgs view with the actual records
// pulled from the running prod daemon (/tmp/prod-orgs.json), runs the REAL
// migration (runner → orgsToPerOrg = readOldOrgs → migrateOrgs → per-org
// HyperOrgs), then opens the result in the experimental per-org HyperOrgs
// and asserts parity. No prod store touched, no device-file guard.
//
// If /tmp/prod-orgs.json is absent (fetch it with:
//   pgh --json --app pear-git-v3 org list > /tmp/prod-orgs.json)
// the test skips.

import test from 'brittle'
import Corestore from 'corestore'
import Autobase from 'autobase'
import Hyperbee from 'hyperbee'
import { HyperOrgs } from 'hyper-orgs'
import { mkdtempSync, readFileSync, existsSync } from '#node:fs'
import { join } from '#node:path'
import { tmpdir } from '#node:os'

import { runMigrations } from '../lib/services/migrate/runner.js'
import { orgsToPerOrg } from '../lib/services/migrate/migrations.js'

const FIXTURE = '/tmp/prod-orgs.json'

async function makeOldOrgsBase (t) {
  const store = new Corestore(mkdtempSync(join(tmpdir(), 'mro-old-')))
  const base = new Autobase(store, null, {
    valueEncoding: 'json',
    open (s) { return new Hyperbee(s.get('view', { valueEncoding: 'json' }), { keyEncoding: 'utf-8', valueEncoding: 'json' }) },
    async apply (nodes, view) {
      for (const node of nodes) {
        const op = node.value
        if (op && op.type === 'create-org') await view.put('org:' + op.org.name, op.org)
      }
    }
  })
  await base.ready()
  t.teardown(async () => { try { await base.close() } catch {}; try { await store.close() } catch {} })
  return base
}

test('upgrade PROD real orgs: v1 single-autobase → v2 per-org, parity verified', async (t) => {
  if (!existsSync(FIXTURE)) { t.pass('skipped — ' + FIXTURE + ' absent'); return }
  const prodOrgs = JSON.parse(readFileSync(FIXTURE, 'utf8'))
  t.ok(prodOrgs.length > 0, 'have prod orgs to migrate (' + prodOrgs.length + ')')

  // 1. seed an OLD single-orgs autobase with prod's real records
  const oldBase = await makeOldOrgsBase(t)
  for (const o of prodOrgs) await oldBase.append({ type: 'create-org', org: o })
  await oldBase.update()

  // 2. target: a fresh per-org HyperOrgs (the experimental v2 model)
  const newStore = new Corestore(mkdtempSync(join(tmpdir(), 'mro-new-')))
  const newOrgs = new HyperOrgs(newStore, { namespace: 'orgs' })
  await newOrgs.ready()
  t.teardown(async () => { try { await newOrgs.close() } catch {}; try { await newStore.close() } catch {} })

  // 3. run the REAL migration through the generic runner
  let version = 0
  const r = await runMigrations({
    getVersion: async () => version,
    setVersion: async (v) => { version = v },
    migrations: [orgsToPerOrg],
    ctx: { oldOrgsBase: oldBase, newOrgs }
  })
  t.is(r.version, 2, 'store reports schema v2 after migration')
  t.alike(r.ran, [2])

  // 4. PARITY: the experimental per-org store has every prod org
  const migrated = await newOrgs.list()
  t.is(migrated.length, prodOrgs.length, 'all ' + prodOrgs.length + ' orgs present in v2 model')
  t.alike(migrated.map(o => o.name).sort(), prodOrgs.map(o => o.name).sort(), 'org names match exactly')

  // 5. members preserved — check the org that has them (hyper-fleeky-dev: 4)
  const prodWithMembers = prodOrgs.filter(o => (o.members || []).length > 0)
  for (const po of prodWithMembers) {
    const mig = await newOrgs.get(po.name)
    const migIds = new Set((mig.members || []).map(m => m.userId))
    for (const pm of po.members) {
      t.ok(migIds.has(pm.userId), po.name + ' member ' + String(pm.userId).slice(0, 10) + ' present after migration')
    }
  }

  // 6. each org is its OWN autobase now (the #37 property) — listOrgKeys
  if (typeof newOrgs.listOrgKeys === 'function') {
    const keys = newOrgs.listOrgKeys()
    t.is(keys.length, prodOrgs.length, 'one per-org autobase per migrated org')
  }
})