import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import Corestore from 'corestore'
import { LinkedRepo } from '../../lib/repo-link/linked-repo.js'
import { encodeOp, decodeOp, validateOp, OP_ADD_WRITER, OP_UPDATE_REF, OP_CREATE_ISSUE } from '../../lib/repo-link/operations.js'
const tmpDir = path.join(os.tmpdir(), 'pear-git-test-linked-' + Date.now())
test('setup: create temp dir', async (t) => {
await fs.promises.mkdir(tmpDir, { recursive: true })
t.pass('temp dir created')
})
test('operations: encode and decode roundtrip', async (t) => {
const op = { type: 'update-ref', ref: 'refs/heads/main', hash: 'abc123', userId: 'user1' }
const encoded = encodeOp(op)
t.ok(Buffer.isBuffer(encoded), 'encode returns Buffer')
const decoded = decodeOp(encoded)
t.is(decoded.type, 'update-ref')
t.is(decoded.ref, 'refs/heads/main')
t.is(decoded.hash, 'abc123')
t.is(decoded.userId, 'user1')
})
test('operations: validate catches missing fields', async (t) => {
// Missing type
t.is(validateOp({}).valid, false)
t.is(validateOp({ type: 'unknown-type' }).valid, false)
// add-writer needs userId and serverKey and role
t.is(validateOp({ type: OP_ADD_WRITER }).valid, false)
t.is(validateOp({ type: OP_ADD_WRITER, userId: 'u1' }).valid, false)
t.is(validateOp({ type: OP_ADD_WRITER, userId: 'u1', serverKey: 'abc', role: 'owner' }).valid, true)
// update-ref needs ref, hash, userId
t.is(validateOp({ type: OP_UPDATE_REF, ref: 'r', hash: 'h' }).valid, false)
t.is(validateOp({ type: OP_UPDATE_REF, ref: 'r', hash: 'h', userId: 'u' }).valid, true)
// create-issue needs id, title, userId
t.is(validateOp({ type: OP_CREATE_ISSUE, id: 1, title: 'Bug' }).valid, false)
t.is(validateOp({ type: OP_CREATE_ISSUE, id: 1, title: 'Bug', userId: 'u1' }).valid, true)
})
test('linked-repo: create new autobase', async (t) => {
const store = new Corestore(path.join(tmpDir, 'store-create'))
const linked = new LinkedRepo({ corestore: store, bootstrapKey: null, repoName: 'test-repo' })
await linked.ready()
t.ok(linked.bootstrapKey, 'has bootstrap key')
t.ok(linked.bootstrapKeyHex, 'has bootstrap key hex')
t.is(linked.bootstrapKeyHex.length, 64, 'key is 32 bytes hex')
t.ok(linked.writable, 'is writable (creator)')
await linked.close()
await store.close()
})
test('linked-repo: append and read ref', async (t) => {
const store = new Corestore(path.join(tmpDir, 'store-ref'))
const linked = new LinkedRepo({ corestore: store, bootstrapKey: null, repoName: 'ref-repo' })
await linked.ready()
await linked.updateRef('refs/heads/main', 'aabbccdd', 'user1')
const ref = await linked.getRef('refs/heads/main')
t.ok(ref, 'ref exists')
t.is(ref.hash, 'aabbccdd')
t.is(ref.updatedBy, 'user1')
t.ok(ref.timestamp > 0, 'has timestamp')
await linked.close()
await store.close()
})
test('linked-repo: update and delete ref', async (t) => {
const store = new Corestore(path.join(tmpDir, 'store-ref-ud'))
const linked = new LinkedRepo({ corestore: store, bootstrapKey: null, repoName: 'ref-ud-repo' })
await linked.ready()
// Create ref
await linked.updateRef('refs/heads/feature', '111111', 'user1')
// Update ref
await linked.updateRef('refs/heads/feature', '222222', 'user2')
let ref = await linked.getRef('refs/heads/feature')
t.is(ref.hash, '222222', 'ref updated')
t.is(ref.updatedBy, 'user2', 'updatedBy changed')
// Delete ref
await linked.deleteRef('refs/heads/feature', 'user1')
ref = await linked.getRef('refs/heads/feature')
t.is(ref, null, 'ref deleted')
// List refs
await linked.updateRef('refs/heads/main', 'aaa', 'user1')
await linked.updateRef('refs/heads/dev', 'bbb', 'user1')
const refs = await linked.listRefs()
t.is(refs.length, 2, 'two refs listed')
await linked.close()
await store.close()
})
test('linked-repo: create and list issues', async (t) => {
const store = new Corestore(path.join(tmpDir, 'store-issues'))
const linked = new LinkedRepo({ corestore: store, bootstrapKey: null, repoName: 'issue-repo' })
await linked.ready()
const id1 = await linked.createIssue('Bug report', 'Something is broken', 'user1')
const id2 = await linked.createIssue('Feature request', 'Add dark mode', 'user2')
t.is(id1, 1, 'first issue id is 1')
t.is(id2, 2, 'second issue id is 2')
const issue1 = await linked.getIssue(1)
t.ok(issue1, 'issue 1 exists')
t.is(issue1.title, 'Bug report')
t.is(issue1.body, 'Something is broken')
t.is(issue1.author, 'user1')
t.is(issue1.status, 'open')
// Update issue
await linked.updateIssue(1, { status: 'closed' }, 'user1')
const updated = await linked.getIssue(1)
t.is(updated.status, 'closed', 'issue closed')
// List all issues
const issues = await linked.listIssues()
t.is(issues.length, 2, 'two issues listed')
await linked.close()
await store.close()
})
test('linked-repo: create and list comments', async (t) => {
const store = new Corestore(path.join(tmpDir, 'store-comments'))
const linked = new LinkedRepo({ corestore: store, bootstrapKey: null, repoName: 'comment-repo' })
await linked.ready()
await linked.createIssue('Bug', 'Details', 'user1')
const commentId = await linked.createComment(1, 'I can reproduce this', 'user2')
t.ok(commentId, 'comment id returned')
const comments = await linked.getComments(1)
t.is(comments.length, 1, 'one comment')
t.is(comments[0].body, 'I can reproduce this')
t.is(comments[0].author, 'user2')
// Issue updatedAt should be touched
const issue = await linked.getIssue(1)
t.ok(issue.updatedAt >= issue.createdAt, 'issue updatedAt updated')
await linked.close()
await store.close()
})
test('linked-repo: create and delete labels', async (t) => {
const store = new Corestore(path.join(tmpDir, 'store-labels'))
const linked = new LinkedRepo({ corestore: store, bootstrapKey: null, repoName: 'label-repo' })
await linked.ready()
await linked.createLabel('bug', '#d73a4a', 'Something is broken')
await linked.createLabel('enhancement', '#a2eeef', 'New feature')
let labels = await linked.listLabels()
t.is(labels.length, 2, 'two labels')
t.is(labels[0].name, 'bug')
t.is(labels[0].color, '#d73a4a')
await linked.deleteLabel('bug')
labels = await linked.listLabels()
t.is(labels.length, 1, 'one label after delete')
t.is(labels[0].name, 'enhancement')
await linked.close()
await store.close()
})
test('linked-repo: set and get meta', async (t) => {
const store = new Corestore(path.join(tmpDir, 'store-meta'))
const linked = new LinkedRepo({ corestore: store, bootstrapKey: null, repoName: 'meta-repo' })
await linked.ready()
await linked.setMeta('description', 'A test repository')
await linked.setMeta('defaultBranch', 'main')
const desc = await linked.getMeta('description')
t.is(desc, 'A test repository')
const branch = await linked.getMeta('defaultBranch')
t.is(branch, 'main')
// Overwrite
await linked.setMeta('description', 'Updated description')
const newDesc = await linked.getMeta('description')
t.is(newDesc, 'Updated description')
// Non-existent key
const missing = await linked.getMeta('nonexistent')
t.is(missing, null)
await linked.close()
await store.close()
})
test('linked-repo: list writers (owner is first)', async (t) => {
const store = new Corestore(path.join(tmpDir, 'store-writers'))
const linked = new LinkedRepo({ corestore: store, bootstrapKey: null, repoName: 'writer-repo' })
await linked.ready()
// Add owner
const ownerKey = 'a'.repeat(64)
await linked.addWriter('owner-user', ownerKey, null, null, 'owner')
const writers = await linked.listWriters()
t.is(writers.length, 1, 'one writer')
t.is(writers[0].userId, 'owner-user')
t.is(writers[0].role, 'owner')
t.is(writers[0].serverKey, ownerKey)
await linked.close()
await store.close()
})
test('linked-repo: reopen from bootstrap key', async (t) => {
const storePath = path.join(tmpDir, 'store-reopen')
const store1 = new Corestore(storePath)
const linked1 = new LinkedRepo({ corestore: store1, bootstrapKey: null, repoName: 'reopen-repo' })
await linked1.ready()
const key = linked1.bootstrapKeyHex
// Write some data
await linked1.updateRef('refs/heads/main', 'abc123', 'user1')
await linked1.setMeta('name', 'reopen-test')
await linked1.close()
await store1.close()
// Reopen with same store path and bootstrap key
const store2 = new Corestore(storePath)
const linked2 = new LinkedRepo({ corestore: store2, bootstrapKey: key, repoName: 'reopen-repo' })
await linked2.ready()
const ref = await linked2.getRef('refs/heads/main')
t.ok(ref, 'ref persisted')
t.is(ref.hash, 'abc123')
const meta = await linked2.getMeta('name')
t.is(meta, 'reopen-test', 'meta persisted')
await linked2.close()
await store2.close()
})
test('cleanup: remove temp dir', async (t) => {
await fs.promises.rm(tmpDir, { recursive: true, force: true })
t.pass('cleaned up')
})