import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { newFixture, type Fixture } from '../helpers/fixture.js'; import { parseMetricsQuery } from '../../src/lib/metrics.js'; let fixture: Fixture; beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); }); afterEach(async () => { vi.restoreAllMocks(); await fixture.cleanup(); }); it('runs reports off the request thread and invalidates cached history after writes', async () => { const { db } = await import('../../src/lib/server/db/index.js'); const { recordMetrics, readMetrics } = await import('../../src/lib/server/fleet/metrics.js'); const { metricsReport } = await import('../../src/lib/server/reports/index.js'); const now = Date.UTC(2026, 9, 4, 12); vi.spyOn(Date, 'now').mockReturnValue(now); const sample = { server: 'one', game: 't5', map: 'Moon', players: 1, capacity: 4 }; recordMetrics([sample], now - 30_000); const query = parseMetricsQuery(new URLSearchParams('days=1')); const first = metricsReport(query); const winner = await Promise.race([ first.then(() => 'report'), new Promise((resolve) => setTimeout(() => resolve('timer'), 0)) ]); expect(winner).toBe('timer'); expect(await first).toEqual(readMetrics(query, now)); expect(await metricsReport(query)).toEqual(await first); db().prepare('UPDATE metric_samples SET players=3').run(); const changed = await metricsReport(query); expect(changed).toEqual(readMetrics(query, now)); expect(changed.summary.playerHours).toBeCloseTo((await first).summary.playerHours * 3); }); it('refreshes policy signals after policy edits and global pauses without new samples', async () => { const { db } = await import('../../src/lib/server/db/index.js'); const { recordMetrics } = await import('../../src/lib/server/fleet/metrics.js'); const { metricsReport } = await import('../../src/lib/server/reports/index.js'); const now = Date.UTC(2026, 9, 4, 12); vi.spyOn(Date, 'now').mockReturnValue(now); recordMetrics( [{ server: 'one', game: 't5', map: 'Moon', players: 1, capacity: null }], now - 60_000 ); const query = parseMetricsQuery(new URLSearchParams('days=1')); expect((await metricsReport(query)).maps[0].policy).toBeNull(); const settings = { game: 't5', mapSlug: '*', mode: 'create-destroy', minimum: 1, maximum: 1, spares: 1, emptyMinutes: 15 }; db() .prepare( `INSERT INTO autoscaling_policies (id,game,map_slug,settings,mode,owns_map,updated_at) VALUES(?,?,?,?,?,1,?)` ) .run( 'moon-policy', 't5', '*', JSON.stringify(settings), settings.mode, new Date(now).toISOString() ); const atMaximum = (await metricsReport(query)).maps[0]; expect(atMaximum.policy?.maximum).toBe(1); expect(atMaximum.noEmptyAtMaximum).toBeCloseTo(1 / 60); db() .prepare('UPDATE autoscaling_policies SET settings=? WHERE id=?') .run(JSON.stringify({ ...settings, maximum: 3 }), 'moon-policy'); const belowMaximum = (await metricsReport(query)).maps[0]; expect(belowMaximum.policy?.maximum).toBe(3); expect(belowMaximum.noEmptyAtMaximum).toBe(0); expect(belowMaximum.noEmptyBelowMaximum).toBeCloseTo(1 / 60); db() .prepare( "INSERT INTO settings(key,value) VALUES('autoscaling_paused','1') ON CONFLICT(key) DO UPDATE SET value='1'" ) .run(); const globallyPaused = await metricsReport(query); expect(globallyPaused.autoscalingPaused).toBe(true); expect(globallyPaused.maps[0]).toEqual(belowMaximum); db().prepare("UPDATE settings SET value='0' WHERE key='autoscaling_paused'").run(); const resumed = await metricsReport(query); expect(resumed.autoscalingPaused).toBe(false); expect(resumed.maps).toEqual(globallyPaused.maps); db().prepare("DELETE FROM autoscaling_policies WHERE id='moon-policy'").run(); expect((await metricsReport(query)).maps[0].policy).toBeNull(); });