Simplify telemetry to three game events

This commit is contained in:
2026-09-18 15:35:21 -04:00
parent 6d903adb31
commit 5bb49a776e
29 changed files with 658 additions and 1314 deletions
+61 -59
View File
@@ -1,100 +1,102 @@
import { describe, expect, it } from 'vitest';
import { defaultPlan, parseBetaPlan, requirement, type BetaPlan } from '../../src/lib/beta-plan.js';
import { applySample, applySignal, coveragePassed, emptyMetrics, evaluateRequirements, type TestSignal } from '../../src/lib/server/fleet/beta-evaluation.js';
import { applySample, applyGameEvent, coveragePassed, emptyMetrics, evaluateRequirements } from '../../src/lib/server/fleet/beta-evaluation.js';
import { gameEvent } from '../helpers/telemetry.js';
const signal = (event: TestSignal['event'], extra: Partial<TestSignal> = {}): TestSignal => ({ event, bootId: 'boot-1', sessionId: 'session-1', sequence: 0, round: 0, players: 4, name: '', ...extra });
const planWith = (...kinds: Parameters<typeof requirement>[0][]): BetaPlan => ({ ...defaultPlan(), requirements: kinds.map((k, i) => requirement(k, `r${i}`)) });
describe('beta requirement evaluation', () => {
it('copies a reactive preset without cloning proxies or mutating the saved requirements', () => {
it('copies reactive presets without mutating saved requirements', () => {
const source = defaultPlan();
const proxied = new Proxy(source, { get(target, key, receiver) {
return key === 'requirements' ? new Proxy(target.requirements, {}) : Reflect.get(target, key, receiver);
} });
const selected = parseBetaPlan(proxied);
const selected = parseBetaPlan(new Proxy(source, {}));
expect(selected).toEqual(source);
selected.requirements[0].target = 15;
selected.requirements.push(requirement('round', 'round'));
expect(source.requirements).toHaveLength(1);
expect(source.requirements[0].target).toBe(1);
});
it('validates plans without accepting unknown kinds, duplicate IDs, fractional counts or unsafe numbers', () => {
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('counts only qualifying rounds and completed matches from the same session', () => {
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();
expect(applySignal(plan, m, signal('round_change', { sequence: 1, round: 50 }), 0)).toBe(false);
applySignal(plan, m, signal('boot'), 0);
applySignal(plan, m, signal('session_start'), 0);
applySignal(plan, m, signal('round_change', { sequence: 1, round: 30, players: 1 }), 1000);
applySignal(plan, m, signal('match_end', { sequence: 2 }), 61_000);
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);
applySignal(plan, m, signal('session_start', { sessionId: 'session-2' }), 62_000);
applySignal(plan, m, signal('round_change', { sessionId: 'session-2', sequence: 1, round: 25 }), 63_000);
applySignal(plan, m, signal('match_end', { sessionId: 'session-2', sequence: 2 }), 123_000);
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 });
expect(applySignal(plan, m, signal('match_end', { sessionId: 'session-2', sequence: 3 }), 124_000)).toBe(false);
applyGameEvent(plan, m, gameEvent('game_end', 25), 124_000);
expect(m.values.r1).toBe(1);
});
it('rejects out-of-order events and retired boots, while counting actual boot changes once', () => {
const plan = planWith('feature', 'restart-budget');
plan.requirements[0].event = 'perk.purchased';
it('accepts ordinary rounds and game ends without boot events or a discovery handshake', () => {
const plan = planWith('round', 'matches');
const m = emptyMetrics();
applySignal(plan, m, signal('boot'), 0);
applySignal(plan, m, signal('session_start'), 0);
applySignal(plan, m, signal('feature', { sequence: 2, name: 'perk.purchased' }), 1);
expect(applySignal(plan, m, signal('feature', { sequence: 1, name: 'perk.purchased' }), 2)).toBe(false);
applySignal(plan, m, signal('boot', { bootId: 'boot-2' }), 3);
expect(applySignal(plan, m, signal('boot'), 4)).toBe(false);
expect(m.values.r0).toBe(1);
expect(evaluateRequirements(plan, m, 0)[1].failed).toBe(true);
expect(m.restarts).toBe(1);
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(2);
});
it('does not double count unqualified matches across log and telemetry sources', () => {
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();
applySignal(plan, m, signal('boot'), 0);
applySignal(plan, m, signal('session_start'), 0);
applySignal(plan, m, signal('match_end', { sequence: 1 }), 1000);
expect(m.values.r0).toBeUndefined(); // these requirements use incremental match logs
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('integrates populated time and player-hours conservatively and excludes monitoring gaps', () => {
const plan = planWith('populated-minutes', 'player-hours', 'healthy-minutes', 'population', 'sustained-population', 'failure-budget');
plan.requirements[4].minPlayers = 4;
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, 4);
applySample(plan, m, 30_000, true, 4);
applySample(plan, m, 60_000, true, 4);
expect(m.values).toMatchObject({ r0: 1, r1: 4 / 60, r2: 1, r3: 4, r4: 1 });
applySample(plan, m, 300_000, true, 4);
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('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);
expect(m.values.r2).toBe(0);
applySample(plan, m, 330_000, false, 0);
applySample(plan, m, 360_000, false, 0);
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)[5].failed).toBe(true);
});
it('requires readable logs, boot evidence and explicit manual sign-off', () => {
const plan = planWith('clean-logs', 'restart-budget', 'manual');
const m = emptyMetrics();
expect(evaluateRequirements(plan, m, 0).every(r => !r.passed)).toBe(true);
m.logAvailable = true; m.bootId = 'boot'; m.manual.r2 = 'Josh: HUD checked';
expect(evaluateRequirements(plan, m, 0).every(r => r.passed)).toBe(true);
m.logErrors = 1;
expect(evaluateRequirements(plan, m, 0)[0].failed).toBe(true);
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', () => {