// What the public Team pages say (docs/website/TEAMS.md §3.2, §3.3, §4.3). // // lib/teams.js is plain JS precisely so these can be asserted without a DOM. The // cases worth protecting are the ones where a wrong sentence is a false statement // about the game rather than a cosmetic slip — an empty roster reported as "no // members" when the module could not be asked being the clearest. import { test } from 'node:test' import assert from 'node:assert/strict' import { LINK_STATE, activityScopeNote, emptyRosterReason, filterTeams, freshnessNote, groupByDay, linkStateOf, relativeTime, rosterSummary, sortTeams, } from '../src/lib/teams.js' // ── The roster header ────────────────────────────────────────────────────── test('the header states what each number is, so the gap reads as information', () => { assert.equal(rosterSummary({ members: 37, linked: 21 }), '37 members · 21 linked') }) test('one member is not "1 members"', () => { assert.equal(rosterSummary({ members: 1, linked: 1 }), '1 member · 1 linked') }) test('forum guests appear only once there are some', () => { assert.equal(rosterSummary({ members: 37, linked: 21, guests: 0 }), '37 members · 21 linked') assert.equal(rosterSummary({ members: 37, linked: 21, guests: 4 }), '37 members · 21 linked · 4 forum guests') assert.equal(rosterSummary({ members: 2, linked: 1, guests: 1 }), '2 members · 1 linked · 1 forum guest') }) test('an absent roster still produces a sentence rather than NaN', () => { assert.equal(rosterSummary(), '0 members · 0 linked') }) test('link state is a value, not an absence', () => { assert.equal(linkStateOf({ linked: true }), LINK_STATE.linked) assert.equal(linkStateOf({ linked: false }), LINK_STATE.unlinked) assert.equal(linkStateOf(undefined), LINK_STATE.unlinked) }) // ── Freshness ────────────────────────────────────────────────────────────── const NOW = new Date('2026-08-17T12:00:00Z').getTime() const ago = (ms) => new Date(NOW - ms).toISOString() test('a deployment with no provider is not stale, it is uninvolved', () => { assert.equal(freshnessNote({ configured: false }, NOW), null) }) test('never synced is a warning, and never reads as a confirmed empty shard', () => { const note = freshnessNote({ configured: true, lastSyncAt: null }, NOW) assert.equal(note.tone, 'warn') assert.match(note.text, /Not yet confirmed/) }) test('a stale projection says how old it is and that the game may have moved on', () => { const note = freshnessNote({ configured: true, lastSyncAt: ago(14 * 60_000), stale: true }, NOW) assert.equal(note.tone, 'warn') assert.equal(note.text, 'Last confirmed 14 minutes ago — the game may have moved on.') }) test('a current projection is stated quietly', () => { const note = freshnessNote({ configured: true, lastSyncAt: ago(90_000), stale: false }, NOW) assert.equal(note.tone, 'idle') assert.equal(note.text, 'Last confirmed 1 minute ago.') }) test('relative time singularises and steps through the units', () => { assert.equal(relativeTime(ago(5_000), NOW), 'just now') assert.equal(relativeTime(ago(60_000), NOW), '1 minute ago') assert.equal(relativeTime(ago(3 * 3_600_000), NOW), '3 hours ago') assert.equal(relativeTime(ago(2 * 86_400_000), NOW), '2 days ago') assert.equal(relativeTime(null, NOW), null) assert.equal(relativeTime('not a date', NOW), null) }) // ── Why a roster is empty ────────────────────────────────────────────────── test('a populated roster has no explaining to do', () => { assert.equal(emptyRosterReason({ members: [{}] }), null) }) test('a module that could not be asked is never reported as an empty guild', () => { // The failure this function exists to prevent: saying something false about // the game because core could not reach the module. const reason = emptyRosterReason({ members: [], projectionUnavailable: true }) assert.match(reason, /could not be reached/) }) test('an unconfirmed projection says so rather than claiming the Team is empty', () => { const reason = emptyRosterReason({ members: [], configured: true, lastSyncAt: null }) assert.match(reason, /not been confirmed/) }) test('a genuinely empty, confirmed roster says the plain thing', () => { const reason = emptyRosterReason({ members: [], configured: true, lastSyncAt: ago(1000) }) assert.equal(reason, 'Nobody is in this Team.') }) // ── The activity feed ────────────────────────────────────────────────────── test('items group into days, newest day first, order kept within a day', () => { const days = groupByDay([ { id: 3, occurredAt: '2026-08-17T09:00:00' }, { id: 2, occurredAt: '2026-08-17T08:00:00' }, { id: 1, occurredAt: '2026-08-16T22:00:00' }, ], 'en-US') assert.equal(days.length, 2) assert.deepEqual(days[0].items.map((i) => i.id), [3, 2]) assert.deepEqual(days[1].items.map((i) => i.id), [1]) }) test('an unparseable timestamp is skipped rather than making a day called Invalid Date', () => { const days = groupByDay([{ id: 1, occurredAt: 'nonsense' }], 'en-US') assert.deepEqual(days, []) }) test('a caller who saw everything is told nothing', () => { assert.equal(activityScopeNote({ scope: 'members' }, true), null) }) test('a filtered feed says so, and invites an anonymous caller to sign in', () => { assert.match(activityScopeNote({ scope: 'public' }, false), /Sign in/) assert.match(activityScopeNote({ scope: 'public' }, true), /members of this Team only/) }) // ── The index ────────────────────────────────────────────────────────────── test('teams sort by size then by name', () => { const sorted = sortTeams([ { name: 'Zephyr', memberCount: 3 }, { name: 'Anvil', memberCount: 10 }, { name: 'Bell', memberCount: 3 }, ]) assert.deepEqual(sorted.map((t) => t.name), ['Anvil', 'Bell', 'Zephyr']) }) test('sorting does not mutate its input', () => { const input = [{ name: 'B', memberCount: 1 }, { name: 'A', memberCount: 9 }] sortTeams(input) assert.equal(input[0].name, 'B') }) test('search matches the two things a visitor knows a Team by', () => { const teams = [{ name: 'The Silver Hand', abbr: 'TSH' }, { name: 'Anvil', abbr: 'ANV' }] assert.deepEqual(filterTeams(teams, 'silver').map((t) => t.abbr), ['TSH']) assert.deepEqual(filterTeams(teams, 'anv').map((t) => t.abbr), ['ANV']) assert.equal(filterTeams(teams, ' ').length, 2) assert.equal(filterTeams(teams, 'nothing').length, 0) }) test('search survives a team with no abbreviation', () => { assert.doesNotThrow(() => filterTeams([{ name: 'Anvil', abbr: null }], 'a')) })