import type { Pool } from 'pg'; import { afterAll, beforeEach, describe, expect, it } from 'vitest'; import '../src/pipeline/projections/index.ts'; import { processEvents } from '../src/pipeline/process.ts'; import { applyRetention, takeSnapshots } from '../src/stats/snapshots.ts'; import { closeTestPool, getTestPool, resetDb } from './helpers/db.ts'; import { ALICE, ev, T0 } from './helpers/events.ts'; let pool: Pool; beforeEach(async () => { pool = await getTestPool(); await resetDb(pool); await processEvents( pool, [ev('stats_snapshot', { ...ALICE, full: false, ts: T0, stats: { 'minecraft:custom': { 'minecraft:play_time': 72000 } } })], 'push', 'atm10', ); }); afterAll(closeTestPool); const DAY = 86_400_000; const count = async (granularity: string) => (await pool.query(`SELECT count(*)::int AS n FROM stat_snapshots WHERE granularity = $1`, [granularity])).rows[0].n; describe('snapshots', () => { it('takeSnapshots idempotent', async () => { await takeSnapshots(pool, 'hour', new Date(T0 + 1000)); const first = await count('hour'); await takeSnapshots(pool, 'hour', new Date(T0 + 2000)); expect(await count('hour')).toBe(first); const { rows } = await pool.query(`SELECT value FROM stat_snapshots WHERE metric = 'playtime'`); expect(rows[0].value).toBe(72000); }); it('retention deletes hourly older than 30 days but keeps daily', async () => { await takeSnapshots(pool, 'hour', new Date(T0)); await takeSnapshots(pool, 'day', new Date(T0)); await applyRetention(pool, new Date(T0 + 31 * DAY)); expect(await count('hour')).toBe(0); expect(await count('day')).toBeGreaterThan(0); }); it('retention folds health minutes older than 14 days into hourly aggregates', async () => { for (const m of [0, 1, 2]) { await pool.query( `INSERT INTO server_health VALUES (to_timestamp($1 / 1000.0), $2, 50, 100, 20, 2, 500, 1000, 4000)`, [T0 + m * 60_000, 10 * (m + 1)], ); } await applyRetention(pool, new Date(T0 + 15 * DAY)); expect((await pool.query('SELECT count(*)::int AS n FROM server_health')).rows[0].n).toBe(0); const { rows } = await pool.query('SELECT mspt_avg, samples FROM server_health_hourly'); expect(rows).toEqual([{ mspt_avg: 20, samples: 3 }]); }); }); describe('events retention', () => { it('drops bulky projected events after 30 days but keeps player events', async () => { const old = T0 - 40 * DAY; await processEvents( pool, [ ev('health', { msptAvg: 1, msptP95: 1, msptMax: 1, tps: 20, players: 0, chunks: 0, heapUsedMb: 1, heapMaxMb: 1, windowStart: old, windowEnd: old, ts: old }), ev('player_join', { ...ALICE, ts: old }), ], 'push', 'atm10', ); await pool.query(`UPDATE events SET received_at = to_timestamp($1 / 1000.0)`, [old]); await applyRetention(pool, new Date(T0)); const { rows } = await pool.query(`SELECT type FROM events ORDER BY type`); expect(rows.map((r) => r.type)).toEqual(['player_join']); }); });