import { describe, expect, it } from 'vitest'; import { defaultPlan, parseBetaPlan, requirement, type BetaPlan } from '../../src/lib/beta-plan.js'; import { applySample, applyGameEvent, coveragePassed, emptyMetrics, evaluateRequirements } from '../../src/lib/server/fleet/beta-evaluation.js'; import { gameEvent } from '../helpers/telemetry.js'; const planWith = (...kinds: Parameters[0][]): BetaPlan => ({ ...defaultPlan(), requirements: kinds.map((k, i) => requirement(k, `r${i}`)) }); describe('beta requirement evaluation', () => { it('credits heartbeat participation during a long round without completing a match', () => { const plan = planWith('populated-minutes', 'round', 'matches'); const m = emptyMetrics(); applyGameEvent(plan, m, gameEvent('game_start'), 0); applyGameEvent(plan, m, gameEvent('round_change', 30), 60_000); applyGameEvent(plan, m, gameEvent('heartbeat', 30), 120_000); applyGameEvent(plan, m, gameEvent('heartbeat', 30), 180_000); expect(m.values).toMatchObject({ r0: 3, r1: 30 }); expect(m.values.r2 ?? 0).toBe(0); expect(m.match).toMatchObject({ started: 0, lastEvent: 180_000, ended: false }); }); it('copies reactive presets without mutating saved requirements', () => { const source = defaultPlan(); const selected = parseBetaPlan(new Proxy(source, {})); expect(selected).toEqual(source); selected.requirements[0].target = 15; expect(source.requirements[0].target).toBe(1); }); it('rejects removed checks, duplicate IDs, fractional counts and unsafe numbers', () => { expect(parseBetaPlan(defaultPlan())).toEqual(defaultPlan()); expect(() => parseBetaPlan({ ...defaultPlan(), requirements: [] })).toThrow(); expect(() => parseBetaPlan({ ...defaultPlan(), timeoutHours: NaN })).toThrow(); expect(() => parseBetaPlan({ ...defaultPlan(), requirements: [requirement('round', 'same'), requirement('round', 'same')] }) ).toThrow(); for (const kind of ['feature', 'restart-budget', 'clean-logs']) { expect(() => parseBetaPlan({ ...defaultPlan(), requirements: [{ ...requirement('round', 'r'), kind }] }) ).toThrow(); } expect(() => parseBetaPlan({ ...defaultPlan(), requirements: [{ ...requirement('round', 'r'), target: 1.5 }] }) ).toThrow(); expect(() => parseBetaPlan({ ...defaultPlan(), requirements: [requirement('manual', 'constructor')] }) ).toThrow(); }); it('uses qualifying round and population together within the same game, including the final report', () => { const plan = planWith('round', 'matches'); plan.requirements[0].minPlayers = 4; plan.requirements[1] = { ...plan.requirements[1], target: 2, minRound: 25, minPlayers: 4, minMinutes: 1 }; const m = emptyMetrics(); applyGameEvent(plan, m, gameEvent('game_start'), 0); applyGameEvent(plan, m, gameEvent('round_change', 30, 1), 1000); applyGameEvent(plan, m, gameEvent('game_end', 30, 1), 61_000); expect(m.values.r0 ?? 0).toBe(0); expect(m.values.r1 ?? 0).toBe(0); applyGameEvent(plan, m, gameEvent('game_start'), 62_000); applyGameEvent(plan, m, gameEvent('game_end', 25), 123_000); expect(m.values).toMatchObject({ r0: 25, r1: 1 }); applyGameEvent(plan, m, gameEvent('game_end', 25), 124_000); expect(m.values.r1).toBe(1); }); it('accepts ordinary rounds and game ends without boot events or a discovery handshake', () => { const plan = planWith('round', 'matches'); const m = emptyMetrics(); applyGameEvent(plan, m, gameEvent('round_change'), 0); applyGameEvent(plan, m, gameEvent('game_end'), 1000); expect(m.values).toMatchObject({ r0: 25, r1: 1 }); applyGameEvent(plan, m, gameEvent('game_start'), 2000); applyGameEvent(plan, m, gameEvent('game_end', 1), 3000); expect(m.values.r1).toBe(1); applyGameEvent(plan, m, gameEvent('game_start'), 4000); applyGameEvent(plan, m, gameEvent('game_end', 2), 5000); expect(m.values.r1).toBe(2); }); it('does not combine games or maps to satisfy duration and round qualifiers', () => { const plan = planWith('matches'); plan.requirements[0] = { ...plan.requirements[0], minRound: 25, minMinutes: 1 }; const m = emptyMetrics(); applyGameEvent(plan, m, gameEvent('game_start'), 0); applyGameEvent(plan, m, gameEvent('round_change'), 1000); applyGameEvent(plan, m, gameEvent('game_start'), 60_000); applyGameEvent(plan, m, gameEvent('game_end', 1), 120_000); expect(m.values.r0 ?? 0).toBe(0); applyGameEvent( plan, m, gameEvent('game_end', 30, 4, 't5_zombie_theater_1', 'zombie_moon'), 180_000 ); expect(m.values.r0 ?? 0).toBe(0); }); it('derives participation only from game reports, even when rounds are many minutes apart', () => { const plan = planWith( 'populated-minutes', 'player-hours', 'population', 'sustained-population' ); plan.requirements[3].minPlayers = 4; const m = emptyMetrics(); applySample(plan, m, 0, true); applySample(plan, m, 30_000, true); expect(m.values).toEqual({}); applyGameEvent(plan, m, gameEvent('game_start'), 0); applyGameEvent(plan, m, gameEvent('round_change'), 600_000); expect(m.values).toMatchObject({ r0: 10, r1: 40 / 60, r2: 4, r3: 10 }); applyGameEvent(plan, m, gameEvent('game_end', 26, 2), 1_200_000); expect(m.values).toMatchObject({ r0: 20, r1: 1, r3: 10 }); applyGameEvent(plan, m, gameEvent('game_start'), 3_000_000); expect(m.values.r0).toBe(20); }); it('keeps health continuous through the freshness boundary but resets beyond it', () => { const plan = planWith('healthy-minutes'); const m = emptyMetrics(); applySample(plan, m, 0, true); applySample(plan, m, 90_000, true); expect(m.values.r0).toBe(1.5); applySample(plan, m, 180_001, true); expect(m.values.r0).toBe(0); }); it.each([ { minutes: 2, gap: 60_005 }, { minutes: 2.5, gap: 60_048 }, { minutes: 2.5, gap: 90_000 } ])( 'preserves a satisfied healthy-minute requirement after a $gap ms gap', ({ minutes, gap }) => { const plan = planWith('healthy-minutes', 'failure-budget'); const m = emptyMetrics(); const previous = minutes * 60_000; for (let at = 0; at <= previous; at += 30_000) applySample(plan, m, at, true); expect(m.values.r0).toBe(minutes); expect(evaluateRequirements(plan, m, 0).every((r) => r.passed)).toBe(true); applySample(plan, m, previous + gap, true); expect(m.values.r0).toBeCloseTo(minutes + gap / 60_000); expect(m.wasHealthy).toBe(true); expect(m.outages).toBe(0); expect(evaluateRequirements(plan, m, 0).every((r) => r.passed)).toBe(true); } ); it('resets a satisfied healthy-minute requirement on an unhealthy sample within the freshness window', () => { const plan = planWith('healthy-minutes', 'failure-budget'); const m = emptyMetrics(); applySample(plan, m, 0, true); applySample(plan, m, 60_000, true); expect(evaluateRequirements(plan, m, 0)[0].passed).toBe(true); applySample(plan, m, 90_000, false); expect(m.values.r0).toBe(0); expect(m.wasHealthy).toBe(false); expect(m.outages).toBe(1); expect(evaluateRequirements(plan, m, 0)[0].passed).toBe(false); }); it('excludes monitoring gaps from healthy time and requires explicit manual sign-off', () => { const plan = planWith('healthy-minutes', 'failure-budget', 'manual'); const m = emptyMetrics(); applySample(plan, m, 0, true); applySample(plan, m, 60_000, true); expect(m.values.r0).toBe(1); applySample(plan, m, 300_000, true); expect(m.values.r0).toBe(0); applySample(plan, m, 330_000, false); applySample(plan, m, 360_000, false); expect(m.outages).toBe(1); expect(evaluateRequirements(plan, m, 0)[1].failed).toBe(true); expect(evaluateRequirements(plan, m, 0)[2].passed).toBe(false); m.manual.r2 = 'Josh: HUD checked'; expect(evaluateRequirements(plan, m, 0)[2].passed).toBe(true); }); it('requires each selected coverage group even when many servers on one map pass', () => { const servers = [ { game: 't5', map: 'a', passed: true }, { game: 't5', map: 'a', passed: true }, { game: 't6', map: 'b', passed: false } ]; expect(coveragePassed({ ...defaultPlan(), scope: 'quorum', quorum: 2 }, servers)).toBe( true ); for (const scope of ['all', 'map', 'game', 'map-game'] as const) expect(coveragePassed({ ...defaultPlan(), scope }, servers)).toBe(false); servers[2].passed = true; expect(coveragePassed({ ...defaultPlan(), scope: 'map-game' }, servers)).toBe(true); }); });