Support multi-game betas with automatic server selection

This commit is contained in:
2026-10-07 09:41:17 -04:00
parent a6dee8c5e2
commit c6352d86b4
17 changed files with 881 additions and 173 deletions
+41 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { defaultPlan, betaPlanIssues, requirement } from '../../src/lib/beta-plan.js';
import { readBetaDraft, compatibleBetaServers } from '../../src/lib/beta-draft.js';
import { readBetaDraft, compatibleBetaServers, betaCandidateServers, betaApplicableGames, betaInstanceCounts, betaScopeMatches } from '../../src/lib/beta-draft.js';
import { betaWaitingCondition, betaEvidenceSummary } from '../../src/lib/beta-evidence.js';
describe('beta draft and plan recovery', () => {
@@ -9,10 +9,24 @@ describe('beta draft and plan recovery', () => {
const raw = JSON.stringify({ version: 1, savedAt: new Date().toISOString(), step: 2, picks: [{ scriptId: 12, versionId: 45 }], servers: ['town'], testPlan: { ...plan, timeoutHours: null } });
const draft = readBetaDraft(raw)!;
expect(draft.picks).toEqual([{ scriptId: 12, versionId: 45 }]);
expect(draft.selectionMode).toBe('manual');
expect(draft.servers).toEqual(['town']);
expect(betaPlanIssues(draft.testPlan, 1)).toContainEqual(expect.objectContaining({ field: 'timeoutHours' }));
expect(readBetaDraft('{bad')).toBeNull();
expect(readBetaDraft(JSON.stringify({ version: 1, picks: [{ scriptId: '12', versionId: 45 }] }))).toBeNull();
});
it('restores automatic selection and incomplete counts without substituting manual selections', () => {
const raw = { version: 1, savedAt: '', step: 1, picks: [{ scriptId: 12, versionId: 45 }],
servers: ['previous-manual-choice'], selectionMode: 'automatic', instancesPerGame: { t5: 2, t6: null }, testPlan: defaultPlan() };
expect(readBetaDraft(JSON.stringify(raw))).toMatchObject({ selectionMode: 'automatic', instancesPerGame: { t5: 2, t6: null } });
expect(readBetaDraft(JSON.stringify({ ...raw, instancesPerGame: { t5: 1.5, t6: -1 } }))?.instancesPerGame).toEqual({ t5: 1.5, t6: -1 });
expect(readBetaDraft(JSON.stringify({ ...raw, instancesPerGame: { t5: '2' } }))).toBeNull();
expect(readBetaDraft(JSON.stringify({ ...raw, instancesPerGame: { unknown: 2 } }))).toBeNull();
expect(readBetaDraft(JSON.stringify({ ...raw, selectionMode: 'unknown' }))).toBeNull();
});
it('defaults a legacy draft with no server choices to automatic selection', () => {
expect(readBetaDraft(JSON.stringify({ version: 1, picks: [{ scriptId: 12, versionId: 45 }], servers: [], testPlan: defaultPlan() }))?.selectionMode).toBe('automatic');
});
it('keeps compatible explicit selections without selecting newly eligible servers', () => {
const result = compatibleBetaServers(['town1', 'town2'], ['town2', 'town3']);
expect([...result.kept]).toEqual(['town2']);
@@ -24,6 +38,32 @@ describe('beta draft and plan recovery', () => {
expect(betaPlanIssues(plan, 2).find(issue => issue.field === 'matches-target')?.requirementId).toBe('matches');
});
});
describe('multi-game beta setup', () => {
const servers = [
{ name: 'kino', game: 't5', map: 'zombie_theater' },
{ name: 'town', game: 't6', map: 'zm_transit' },
{ name: 'origins', game: 't6', map: 'zm_tomb' }
];
it('offers the union of different file scopes while keeping file-to-server matches separate', () => {
const files = [{ scope_type: 'game' as const, scope_value: 't5' }, { scope_type: 'map' as const, scope_value: 'zm_transit' }];
expect(betaCandidateServers(files, servers).map(server => server.name)).toEqual(['kino', 'town']);
expect(betaApplicableGames(files, servers)).toEqual(['t5', 't6']);
expect(betaScopeMatches(files[0], servers[1])).toBe(false);
expect(betaScopeMatches(files[1], servers[0])).toBe(false);
});
it('includes every server for an unscoped file and requires a selected file first', () => {
expect(betaCandidateServers([{ scope_type: null, scope_value: null }], servers)).toEqual(servers);
expect(betaCandidateServers([], servers)).toEqual([]);
expect(betaApplicableGames([], servers)).toEqual([]);
});
it('keeps explicitly scoped games visible without fleet matches and preserves zero or incomplete counts', () => {
const files = [{ scope_type: 'game' as const, scope_value: 't5' }, { scope_type: 'game' as const, scope_value: 't6' }];
expect(betaApplicableGames(files, [])).toEqual(['t5', 't6']);
expect(betaInstanceCounts(['t5', 't6'], {})).toEqual({ t5: 1, t6: 1 });
expect(betaInstanceCounts(['t5', 't6'], { t5: 0, t6: null })).toEqual({ t5: 0, t6: null });
expect(betaInstanceCounts(['t6'], { t5: 3, t6: 2 })).toEqual({ t6: 2 });
});
});
describe('beta evidence explanations', () => {
const observation = { lastGameplayAt: new Date().toISOString(), lastEvent: 'round_change', players: 2, round: 12, matchStarted: true, gameEnded: false, healthy: true, healthCheckedAt: new Date().toISOString() };
it('distinguishes missing reports, insufficient population, and report-driven time', () => {