import test from 'brittle'
import { Board } from '../lib/board.js'
import { tmpdir } from 'os'
import { join } from 'path'
import { randomBytes } from 'crypto'
function tmpStorage () {
return join(tmpdir(), 'hyper-board-test-' + randomBytes(8).toString('hex'))
}
test('Board - ready creates default lanes', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
const lanes = await board.listLanes()
t.is(lanes.length, 4)
t.is(lanes[0].name, 'backlog')
t.is(lanes[1].name, 'todo')
t.is(lanes[2].name, 'in-progress')
t.is(lanes[3].name, 'done')
await board.close()
})
test('Board - create and get card', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
const card = await board.createCard({
title: 'Fix bug',
description: 'Critical production bug',
lane: 'todo',
priority: 'high',
tags: ['bug', 'production']
})
t.ok(card.id)
t.is(card.title, 'Fix bug')
t.is(card.lane, 'todo')
t.is(card.priority, 'high')
t.alike(card.tags, ['bug', 'production'])
t.ok(card.created > 0)
const fetched = await board.getCard(card.id)
t.is(fetched.title, 'Fix bug')
await board.close()
})
test('Board - update card', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
const card = await board.createCard({ title: 'Task A' })
const updated = await board.updateCard(card.id, {
title: 'Task A (updated)',
description: 'Now with details',
assignee: 'fleeky'
})
t.is(updated.title, 'Task A (updated)')
t.is(updated.assignee, 'fleeky')
t.is(updated.id, card.id)
t.is(updated.created, card.created)
t.ok(updated.updated >= card.updated)
await board.close()
})
test('Board - delete card', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
const card = await board.createCard({ title: 'Delete me' })
t.ok(await board.getCard(card.id))
const ok = await board.deleteCard(card.id)
t.ok(ok)
t.is(await board.getCard(card.id), null)
// Deleting non-existent returns false
const nope = await board.deleteCard('nonexistent')
t.is(nope, false)
await board.close()
})
test('Board - move card between lanes', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
const card = await board.createCard({ title: 'Move me', lane: 'backlog' })
t.is(card.lane, 'backlog')
const moved = await board.moveCard(card.id, 'in-progress')
t.is(moved.lane, 'in-progress')
const fetched = await board.getCard(card.id)
t.is(fetched.lane, 'in-progress')
await board.close()
})
test('Board - list cards with lane filter', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
await board.createCard({ title: 'A', lane: 'todo' })
await board.createCard({ title: 'B', lane: 'todo' })
await board.createCard({ title: 'C', lane: 'done' })
const todo = await board.listCards({ lane: 'todo' })
t.is(todo.length, 2)
const done = await board.listCards({ lane: 'done' })
t.is(done.length, 1)
const all = await board.listCards()
t.is(all.length, 3)
await board.close()
})
test('Board - list cards with assignee filter', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
await board.createCard({ title: 'A', assignee: 'alice' })
await board.createCard({ title: 'B', assignee: 'bob' })
await board.createCard({ title: 'C', assignee: 'alice' })
const alice = await board.listCards({ assignee: 'alice' })
t.is(alice.length, 2)
await board.close()
})
test('Board - list cards with tag filter', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
await board.createCard({ title: 'A', tags: ['bug'] })
await board.createCard({ title: 'B', tags: ['feature'] })
await board.createCard({ title: 'C', tags: ['bug', 'urgent'] })
const bugs = await board.listCards({ tag: 'bug' })
t.is(bugs.length, 2)
await board.close()
})
test('Board - priority sorting', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
await board.createCard({ title: 'Normal', priority: 'normal' })
await board.createCard({ title: 'Urgent', priority: 'urgent' })
await board.createCard({ title: 'Low', priority: 'low' })
await board.createCard({ title: 'High', priority: 'high' })
const cards = await board.listCards()
t.is(cards[0].title, 'Urgent')
t.is(cards[1].title, 'High')
t.is(cards[2].title, 'Normal')
t.is(cards[3].title, 'Low')
await board.close()
})
test('Board - add and remove lane', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
await board.addLane('review', 2)
const lanes = await board.listLanes()
t.ok(lanes.find(l => l.name === 'review'))
await board.removeLane('review')
const lanes2 = await board.listLanes()
t.absent(lanes2.find(l => l.name === 'review'))
await board.close()
})
test('Board - cannot remove lane with cards', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
await board.createCard({ title: 'Blocker', lane: 'todo' })
try {
await board.removeLane('todo')
t.fail('Should have thrown')
} catch (err) {
t.ok(err.message.includes('Cannot remove'))
t.ok(err.message.includes('1 cards'))
}
await board.close()
})
test('Board - duplicate lane name rejected', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
try {
await board.addLane('todo')
t.fail('Should have thrown')
} catch (err) {
t.ok(err.message.includes('already exists'))
}
await board.close()
})
test('Board - search by title', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
await board.createCard({ title: 'Fix authentication bug' })
await board.createCard({ title: 'Add new feature' })
await board.createCard({ title: 'Bug in login page' })
const results = await board.search('bug')
t.is(results.length, 2)
await board.close()
})
test('Board - search by description', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
await board.createCard({ title: 'Task', description: 'needs performance optimization' })
await board.createCard({ title: 'Other' })
const results = await board.search('performance')
t.is(results.length, 1)
t.is(results[0].title, 'Task')
await board.close()
})
test('Board - search by tag', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
await board.createCard({ title: 'A', tags: ['frontend'] })
await board.createCard({ title: 'B', tags: ['backend'] })
const results = await board.search('frontend')
t.is(results.length, 1)
await board.close()
})
test('Board - stats', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
await board.createCard({ title: 'A', lane: 'todo' })
await board.createCard({ title: 'B', lane: 'todo' })
await board.createCard({ title: 'C', lane: 'done' })
const stats = await board.stats()
t.is(stats.totalCards, 3)
t.is(stats.totalLanes, 4)
t.is(stats.byLane.todo, 2)
t.is(stats.byLane.done, 1)
t.is(stats.byLane.backlog, 0)
await board.close()
})
test('Board - view returns lanes with cards', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
await board.createCard({ title: 'A', lane: 'todo' })
await board.createCard({ title: 'B', lane: 'done' })
const view = await board.view()
t.is(view.length, 4) // all 4 default lanes
const todoView = view.find(l => l.name === 'todo')
t.is(todoView.cards.length, 1)
t.is(todoView.cards[0].title, 'A')
const doneView = view.find(l => l.name === 'done')
t.is(doneView.cards.length, 1)
const backlogView = view.find(l => l.name === 'backlog')
t.is(backlogView.cards.length, 0)
await board.close()
})
test('Board - persistence', async (t) => {
const storage = tmpStorage()
// Create and populate
const b1 = new Board(storage)
await b1.ready()
await b1.createCard({ title: 'Persistent', lane: 'todo' })
await b1.close()
// Reopen
const b2 = new Board(storage)
await b2.ready()
const cards = await b2.listCards()
t.is(cards.length, 1)
t.is(cards[0].title, 'Persistent')
await b2.close()
})
test('Board - update nonexistent card throws', async (t) => {
const board = new Board(tmpStorage())
await board.ready()
try {
await board.updateCard('nonexistent', { title: 'X' })
t.fail('Should have thrown')
} catch (err) {
t.ok(err.message.includes('not found'))
}
await board.close()
})