// migrateOrgs (#51): orgs v1 single-autobase → v2 per-org + resourceKeys.
// Pure transformation, deps mocked — no live corestore.
import test from 'brittle'
import { migrateOrgs } from '../lib/services/migrate/migrate-orgs.js'
function recorder () {
const calls = { created: [], members: [], resources: [] }
return {
calls,
create: async (o) => calls.created.push(o),
addMember: async (name, m) => calls.members.push([name, m]),
addResource: async (name, r) => calls.resources.push([name, r])
}
}
const OLD = [
{
name: 'alpha', type: 'group', displayName: 'Alpha', description: 'a', about: 'A', visibility: 'public', userId: 'owner-a',
members: [{ userId: 'owner-a', role: 'owner' }, { userId: 'bob', role: 'contributor' }],
resources: ['repo-1', 'repo-2']
},
{
name: 'beta', type: 'group', members: [{ userId: 'owner-b', role: 'owner' }], resources: []
}
]
test('migrates every org, replaying members + resources', async (t) => {
const r = recorder()
const summary = await migrateOrgs({ oldOrgs: OLD, ...r })
t.alike(summary, { orgs: 2, members: 3, resources: 2, skipped: 0, errors: 0 })
t.alike(r.calls.created.map(o => o.name).sort(), ['alpha', 'beta'])
t.alike(r.calls.created.find(o => o.name === 'alpha').displayName, 'Alpha')
t.is(r.calls.members.filter(([n]) => n === 'alpha').length, 2)
t.alike(r.calls.resources.map(([n, res]) => n + ':' + res.name).sort(), ['alpha:repo-1', 'alpha:repo-2'])
})
test('resolves resourceKeys via the injected resolver', async (t) => {
const r = recorder()
await migrateOrgs({
oldOrgs: [OLD[0]],
...r,
resolveResourceKey: async (repo) => repo === 'repo-1' ? 'k1'.padEnd(64, '1') : null
})
const r1 = r.calls.resources.find(([, res]) => res.name === 'repo-1')[1]
const r2 = r.calls.resources.find(([, res]) => res.name === 'repo-2')[1]
t.is(r1.bootstrapKey, 'k1'.padEnd(64, '1'), 'repo-1 key resolved')
t.is(r2.bootstrapKey, null, 'repo-2 had no key → null (name-only)')
})
test('idempotent: a populated `seen` set skips already-migrated orgs', async (t) => {
const r = recorder()
const seen = new Set(['alpha'])
const summary = await migrateOrgs({ oldOrgs: OLD, ...r, seen })
t.is(summary.skipped, 1, 'alpha skipped')
t.is(summary.orgs, 1, 'only beta migrated')
t.alike(r.calls.created.map(o => o.name), ['beta'])
t.ok(seen.has('beta'), 'beta recorded as migrated for resumability')
})
test('one org failing does not abort the rest', async (t) => {
const r = recorder()
const errors = []
const create = async (o) => { if (o.name === 'alpha') throw new Error('boom'); r.calls.created.push(o) }
const summary = await migrateOrgs({ oldOrgs: OLD, create, addMember: r.addMember, addResource: r.addResource, onError: (n, e) => errors.push([n, e.message]) })
t.is(summary.errors, 1)
t.is(summary.orgs, 1, 'beta still migrated')
t.alike(errors, [['alpha', 'boom']])
t.alike(r.calls.created.map(o => o.name), ['beta'])
})
test('async-iterable source (a real autobase view stream)', async (t) => {
const r = recorder()
async function * gen () { for (const o of OLD) yield o }
const summary = await migrateOrgs({ oldOrgs: gen(), ...r })
t.is(summary.orgs, 2, 'consumes an async iterable, not just an array')
})