// readOldIssues / readOldLabels (#42 migration source): read issue, comment,
// and label records from a legacy org issue tracker. The fixture builds a
// REAL HyperIssues (the v1 separate `issues:org:<name>` shape) on a fresh tmp
// store — no prod data, no copy, so no hypercore-storage device-file guard.
import test from 'brittle'
import Corestore from 'corestore'
import { HyperIssues } from 'hyper-issues'
import { mkdtempSync } from '#node:fs'
import { join } from '#node:path'
import { tmpdir } from '#node:os'
import { readOldIssues, readOldLabels } from '../lib/services/migrate/read-old-issues.js'
// A v1 org issue tracker is just a standalone HyperIssues — same view shape
// (issue:/comment:/label:) the old separate `issues:org:<name>` autobase had.
async function makeOldTracker (t) {
const store = new Corestore(mkdtempSync(join(tmpdir(), 'old-issues-')))
const tracker = new HyperIssues(store, { namespace: 'issues:org:fixture' })
await tracker.ready()
t.teardown(async () => {
try { await tracker.close() } catch {}
try { await store.close() } catch {}
})
return tracker
}
async function collect (gen) {
const out = []
for await (const x of gen) out.push(x)
return out
}
test('readOldIssues streams every issue with its comments attached', async (t) => {
const tracker = await makeOldTracker(t)
const id1 = await tracker.create({ title: 'First bug', body: 'broken', author: 'alice', labels: ['bug'] })
const id2 = await tracker.create({ title: 'Second', body: 'feature', author: 'bob' })
await tracker.comment(id1, { body: 'reproduced', author: 'bob' })
await tracker.comment(id1, { body: 'fixed?', author: 'alice' })
await tracker.comment(id2, { body: 'a comment', author: 'carol' })
const issues = await collect(readOldIssues(tracker))
t.is(issues.length, 2, 'both issues read back')
t.alike(issues.map(i => i.id).sort((a, b) => a - b), [id1, id2], 'ids preserved')
const first = issues.find(i => i.id === id1)
t.is(first.title, 'First bug', 'title preserved')
t.is(first.author, 'alice', 'author preserved')
t.alike(first.labels, ['bug'], 'labels preserved')
t.is(first.comments.length, 2, 'issue 1 has both comments')
t.ok(first.comments.every(c => c.commentId && c.body && c.author), 'comments carry commentId/body/author')
const second = issues.find(i => i.id === id2)
t.is(second.comments.length, 1, 'issue 2 has its one comment')
})
test('comment ranges do not bleed across issue ids (1 vs 11)', async (t) => {
const tracker = await makeOldTracker (t)
// Force ids 1..11 so we have a `1` and an `11`.
let lastId = 0
for (let n = 0; n < 11; n++) lastId = await tracker.create({ title: 'i' + n, author: 'a' })
t.is(lastId, 11, 'eleven issues created, last id 11')
await tracker.comment(1, { body: 'on issue one', author: 'a' })
await tracker.comment(11, { body: 'on issue eleven', author: 'a' })
const issues = await collect(readOldIssues(tracker))
const one = issues.find(i => i.id === 1)
const eleven = issues.find(i => i.id === 11)
t.is(one.comments.length, 1, 'issue 1 has exactly its own comment')
t.is(one.comments[0].body, 'on issue one')
t.is(eleven.comments.length, 1, 'issue 11 has exactly its own comment')
t.is(eleven.comments[0].body, 'on issue eleven')
})
test('readOldLabels streams label records', async (t) => {
const tracker = await makeOldTracker(t)
await tracker.createLabel('bug', '#d73a4a', 'Something broken', 'Bug')
await tracker.createLabel('enhancement', '#a2eeef', 'New feature', 'Enhancement')
const labels = await collect(readOldLabels(tracker))
t.is(labels.length, 2, 'both labels read back')
t.alike(labels.map(l => l.name).sort(), ['bug', 'enhancement'])
const bug = labels.find(l => l.name === 'bug')
t.is(bug.color, '#d73a4a', 'color preserved')
t.is(bug.displayName, 'Bug', 'displayName preserved')
})
test('readOldIssues on an empty tracker yields nothing', async (t) => {
const tracker = await makeOldTracker(t)
const issues = await collect(readOldIssues(tracker))
t.alike(issues, [], 'no issues → empty')
const labels = await collect(readOldLabels(tracker))
t.alike(labels, [], 'no labels → empty')
})
test('readOldIssues rejects a base without a Hyperbee view', async (t) => {
await t.exception(() => collect(readOldIssues({})), /base.view must be a Hyperbee/)
await t.exception(() => collect(readOldIssues(null)), /requires an opened/)
})