mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-10-22 05:54:10 -04:00
69 lines
2.6 KiB
TypeScript
69 lines
2.6 KiB
TypeScript
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> = {}): StatsSettings {
|
|
return { ...SETTINGS_DEFAULTS, ...overrides };
|
|
}
|
|
|
|
function player(id: string, overrides: Partial<PlayerState>): 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
|
|
});
|
|
});
|