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>
61 lines
2.6 KiB
TypeScript
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);
|
|
});
|
|
});
|