import { describe, test, expect } from 'bun:test'; import { deriveSegments, countSegments } from '$lib/server/stats/segments'; import { SETTINGS_DEFAULTS, type PlayerState, type StatsSettings } from '$lib/types/stats'; import { addDays } from '$lib/server/stats/dates'; const AS_OF = '2026-09-15'; function settings(overrides: Partial = {}): StatsSettings { return { ...SETTINGS_DEFAULTS, ...overrides }; } function player(id: string, overrides: Partial): PlayerState { return { id, firstDate: addDays(AS_OF, -100), lastDate: AS_OF, totalPlays: 10, plays7: 2, plays30: 10, streak: 2, ...overrides }; } const players: PlayerState[] = [ player('new', { firstDate: addDays(AS_OF, -1), lastDate: AS_OF, plays7: 1 }), player('core', { lastDate: AS_OF, plays7: 5 }), player('resurrected', { lastDate: AS_OF, plays7: 2, plays30: 2 }), player('returning', { lastDate: AS_OF, plays7: 2, plays30: 10 }), player('atRisk', { lastDate: addDays(AS_OF, -10) }), player('churned', { lastDate: addDays(AS_OF, -40) }) ]; describe('deriveSegments', () => { test('classifies each lifecycle segment', () => { const sets = deriveSegments(players, AS_OF, settings()); expect(sets.new).toEqual(['new']); expect(sets.core).toEqual(['core']); expect(sets.resurrected).toEqual(['resurrected']); expect(sets.returning).toEqual(['returning']); expect(sets.atRisk).toEqual(['atRisk']); expect(sets.churned).toEqual(['churned']); expect(sets.active).toEqual(['new', 'core', 'resurrected', 'returning']); }); test('changing the core threshold re-segments without new data', () => { const strict = deriveSegments(players, AS_OF, settings({ core_plays_per_week: 6 })); expect(strict.core).toEqual([]); expect(strict.returning).toContain('core'); const lenient = deriveSegments(players, AS_OF, settings({ core_plays_per_week: 2 })); expect(lenient.core).toContain('core'); expect(lenient.core).toContain('returning'); }); test('counts include every player exactly once across mutually exclusive sets', () => { const counts = countSegments(deriveSegments(players, AS_OF, settings())); const activeTotal = counts.new + counts.core + counts.returning + counts.resurrected; expect(activeTotal).toBe(counts.active); expect(counts.total).toBe(players.length); }); test('at-risk and churned boundaries follow settings', () => { const sets = deriveSegments(players, AS_OF, settings({ at_risk_days: 5, churn_days: 20 })); expect(sets.atRisk).toContain('atRisk'); // inactive 10 >= 5, < 20 expect(sets.churned).toContain('churned'); // inactive 40 >= 20 }); });