project-bifrost-platform/tests/member-number.test.ts
Jonathan Hvid 368ce3ac8c feat(db): dispatches + member-number allocation + focus-tags parser
db.ts:
  - User type gains pull_quote, member_number, focus_tags; all SELECT lists
    updated. getAllCabMembers (member_number asc) and countCabMembers (used
    by /pulse denominator) added.
  - createUser allocates a member_number in-transaction when role=cab.
  - updateUserRole returns { allocated: number | null } so admin can surface
    the assignment; allocation is one-way: pilot→cab→pilot→cab keeps the
    original number.
  - allocateMemberNumber: MAX(member_number)+1, idempotent, never reuses.
  - updateUserAdminFields: title / pull_quote / focus_tags (parsed array).
  - createEvent / updateEvent extended for audience, duration_label,
    action_label, notes_url.
  - Dispatch CRUD: create / update / publish (stamps published_at) /
    archive / delete. getDispatchById, getLatestPublishedDispatches,
    getAllDispatchesForAdmin, getAdjacentDispatches (prev/next in published
    order).
  - getEventAttendees(slug, status) backs the upcoming-event avatar pile.

format.ts:
  - AVATAR_PIGMENTS (terracotta/copper/walnut/indigo/heather) + pigmentForId
    (id % palette, deterministic).
  - parseFocusTags: trim, strip ASCII control chars (\x00-\x1F\x7F),
    collapse internal whitespace, dedupe, cap 3 × 24.
  - readFocusTags (safe JSON.parse for display).
  - dispatchSlug / parseDispatchSlug: {id}-{kebab(title)}; renames don't
    break links because the id leads.
  - dispatchKindLabel, stripMarkdownLight, dispatchExcerptParas (two-paragraph
    excerpt with sentence-boundary cut).

Tests: member-number allocation (idempotent, never reuses, allocates on
role transition) and focus_tags parser (control chars, whitespace collapse,
dedupe, cap). 24/24 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:55:35 +02:00

61 lines
2.6 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import type Database from 'better-sqlite3';
import { allocateMemberNumber, createUser, updateUserRole, deactivateUser } from '../src/lib/db.js';
const db = (globalThis as typeof globalThis & { __bifrost_db?: Database.Database }).__bifrost_db!;
function makeUser(name: string, role: 'pilot' | 'cab' | 'fenja' = 'pilot'): number {
return createUser({ email: `${name}@x.test`, password_hash: 'x', name, organisation: 'TestOrg', role });
}
beforeEach(() => {
db.exec(`
DELETE FROM votes;
DELETE FROM pulses;
DELETE FROM activity;
DELETE FROM dispatches;
DELETE FROM users;
`);
});
describe('allocateMemberNumber', () => {
it('allocates 1 for the first cab user and sequential numbers thereafter', () => {
const a = makeUser('alice', 'cab');
const b = makeUser('bob', 'cab');
const c = makeUser('cara', 'cab');
expect(db.prepare('SELECT member_number FROM users WHERE id=?').get(a)).toEqual({ member_number: 1 });
expect(db.prepare('SELECT member_number FROM users WHERE id=?').get(b)).toEqual({ member_number: 2 });
expect(db.prepare('SELECT member_number FROM users WHERE id=?').get(c)).toEqual({ member_number: 3 });
});
it('is idempotent — calling allocate on a user who already has a number returns the same one', () => {
const a = makeUser('alice', 'cab');
const first = (db.prepare('SELECT member_number AS n FROM users WHERE id=?').get(a) as { n: number }).n;
const reAllocated = allocateMemberNumber(a);
expect(reAllocated).toBe(first);
});
it('never reuses: deactivating a holder does not free their number', () => {
const a = makeUser('alice', 'cab'); // #1
const b = makeUser('bob', 'cab'); // #2
deactivateUser(a);
const c = makeUser('cara', 'cab'); // expects #3, not #1
expect((db.prepare('SELECT member_number AS n FROM users WHERE id=?').get(c) as { n: number }).n).toBe(3);
});
it('does not allocate when a non-cab user is created', () => {
const a = makeUser('alice', 'pilot');
expect(db.prepare('SELECT member_number FROM users WHERE id=?').get(a)).toEqual({ member_number: null });
});
it('allocates on role transition pilot → cab and only the first time', () => {
const a = makeUser('alice', 'pilot');
const r1 = updateUserRole(a, 'cab');
expect(r1.allocated).toBe(1);
// transition back and forth must not change the number
updateUserRole(a, 'pilot');
const r2 = updateUserRole(a, 'cab');
expect(r2.allocated).toBeNull();
expect((db.prepare('SELECT member_number AS n FROM users WHERE id=?').get(a) as { n: number }).n).toBe(1);
});
});