import { describe, it, expect, beforeEach } from 'vitest'; import type Database from 'better-sqlite3'; import { createDispatch, publishDispatch, archiveDispatch, getDispatchById, getLatestPublishedDispatches, getAdjacentDispatches, createUser, } from '../src/lib/db.js'; import { dispatchSlug, parseDispatchSlug } from '../src/lib/format.js'; const db = (globalThis as typeof globalThis & { __bifrost_db?: Database.Database }).__bifrost_db!; let admin: number; beforeEach(() => { db.exec(` DELETE FROM votes; DELETE FROM pulses; DELETE FROM activity; DELETE FROM dispatches; DELETE FROM users; `); admin = createUser({ email: 'a@x.test', password_hash: 'x', name: 'Admin', organisation: 'Fenja', role: 'fenja' }); }); describe('Dispatch status transitions', () => { it('draft create leaves published_at null', () => { const id = createDispatch({ title: 'T', body: 'B', excerpt: null, kind: 'note', author_id: admin, status: 'draft' }); const d = getDispatchById(id)!; expect(d.status).toBe('draft'); expect(d.published_at).toBeNull(); }); it('published create stamps published_at', () => { const id = createDispatch({ title: 'T', body: 'B', excerpt: null, kind: 'decision', author_id: admin, status: 'published' }); const d = getDispatchById(id)!; expect(d.status).toBe('published'); expect(d.published_at).not.toBeNull(); }); it('publishDispatch on a draft stamps published_at', () => { const id = createDispatch({ title: 'T', body: 'B', excerpt: null, kind: 'note', author_id: admin, status: 'draft' }); publishDispatch(id); const d = getDispatchById(id)!; expect(d.status).toBe('published'); expect(d.published_at).not.toBeNull(); }); it('publishDispatch is idempotent — re-publishing keeps the original published_at', () => { const id = createDispatch({ title: 'T', body: 'B', excerpt: null, kind: 'note', author_id: admin, status: 'draft' }); publishDispatch(id); const first = getDispatchById(id)!.published_at; // Force a tick so a re-stamp would visibly differ publishDispatch(id); const second = getDispatchById(id)!.published_at; expect(second).toBe(first); }); it('archiveDispatch preserves published_at and only flips status', () => { const id = createDispatch({ title: 'T', body: 'B', excerpt: null, kind: 'update', author_id: admin, status: 'published' }); const before = getDispatchById(id)!.published_at; archiveDispatch(id); const d = getDispatchById(id)!; expect(d.status).toBe('archived'); expect(d.published_at).toBe(before); }); it('getLatestPublishedDispatches excludes drafts and archived', () => { createDispatch({ title: 'A', body: 'B', excerpt: null, kind: 'note', author_id: admin, status: 'draft' }); const pubId = createDispatch({ title: 'B', body: 'B', excerpt: null, kind: 'note', author_id: admin, status: 'published' }); const archId = createDispatch({ title: 'C', body: 'B', excerpt: null, kind: 'note', author_id: admin, status: 'published' }); archiveDispatch(archId); const out = getLatestPublishedDispatches(10); expect(out.map(d => d.id)).toEqual([pubId]); }); it('getAdjacentDispatches returns prev/next in published order', () => { const ids: number[] = []; for (let i = 0; i < 3; i += 1) { const id = createDispatch({ title: `T${i}`, body: 'B', excerpt: null, kind: 'note', author_id: admin, status: 'published' }); // Backdate so ordering is deterministic db.prepare("UPDATE dispatches SET published_at = datetime('now', ?) WHERE id = ?").run(`-${(3 - i)} days`, id); ids.push(id); } const { prev, next } = getAdjacentDispatches(ids[1]); expect(prev?.id).toBe(ids[0]); expect(next?.id).toBe(ids[2]); }); }); describe('dispatch slug', () => { it('produces {id}-kebab(title)', () => { expect(dispatchSlug({ id: 7, title: 'A Decision About Telemetry' })).toBe('7-a-decision-about-telemetry'); expect(dispatchSlug({ id: 12, title: 'Behind the Scenes: April' })).toBe('12-behind-the-scenes-april'); }); it('parseDispatchSlug pulls the leading id', () => { expect(parseDispatchSlug('7-a-decision-about-telemetry')).toBe(7); expect(parseDispatchSlug('99')).toBe(99); expect(parseDispatchSlug('not-a-slug')).toBeNull(); expect(parseDispatchSlug('')).toBeNull(); }); });