195 lines
8.2 KiB
TypeScript
195 lines
8.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { defaultPlan, betaPlanIssues, requirement } from '../../src/lib/beta-plan.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', () => {
|
|
it('retains exact version IDs and incomplete plan fields in a recoverable draft', () => {
|
|
const plan = defaultPlan();
|
|
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 the shared count and incomplete input without substituting manual selections', () => {
|
|
const raw = {
|
|
version: 1,
|
|
savedAt: '',
|
|
step: 1,
|
|
picks: [{ scriptId: 12, versionId: 45 }],
|
|
servers: ['previous-manual-choice'],
|
|
selectionMode: 'automatic',
|
|
instanceCount: 2,
|
|
testPlan: defaultPlan()
|
|
};
|
|
expect(readBetaDraft(JSON.stringify(raw))).toMatchObject({
|
|
selectionMode: 'automatic',
|
|
instanceCount: 2
|
|
});
|
|
for (const instanceCount of [null, 1.5, -1]) {
|
|
expect(readBetaDraft(JSON.stringify({ ...raw, instanceCount }))?.instanceCount).toBe(
|
|
instanceCount
|
|
);
|
|
}
|
|
expect(readBetaDraft(JSON.stringify({ ...raw, instanceCount: '2' }))).toBeNull();
|
|
expect(readBetaDraft(JSON.stringify({ ...raw, selectionMode: 'unknown' }))).toBeNull();
|
|
});
|
|
it('migrates per-game drafts without reducing coverage and revisits unequal counts', () => {
|
|
const raw = {
|
|
version: 1,
|
|
savedAt: '',
|
|
step: 3,
|
|
picks: [{ scriptId: 12, versionId: 45 }],
|
|
servers: [],
|
|
selectionMode: 'automatic',
|
|
instancesPerGame: { t5: 2, t6: 3 },
|
|
testPlan: defaultPlan()
|
|
};
|
|
expect(readBetaDraft(JSON.stringify(raw))).toMatchObject({ instanceCount: 3, step: 1 });
|
|
expect(
|
|
readBetaDraft(JSON.stringify({ ...raw, instancesPerGame: { t5: 2, t6: 2 } }))
|
|
).toMatchObject({ instanceCount: 2, step: 3 });
|
|
expect(
|
|
readBetaDraft(JSON.stringify({ ...raw, instancesPerGame: { t5: 2, t6: null } }))
|
|
?.instanceCount
|
|
).toBeNull();
|
|
expect(readBetaDraft(JSON.stringify({ ...raw, instancesPerGame: {} }))?.instanceCount).toBe(
|
|
1
|
|
);
|
|
expect(readBetaDraft(JSON.stringify({ ...raw, selectionMode: 'manual' }))?.step).toBe(3);
|
|
expect(readBetaDraft(JSON.stringify({ ...raw, instancesPerGame: { t5: '2' } }))).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']);
|
|
expect(result.removed).toEqual(['town1']);
|
|
});
|
|
it('identifies the precise requirement and rejects an impossible quorum before review', () => {
|
|
const plan = {
|
|
...defaultPlan(),
|
|
scope: 'quorum' as const,
|
|
quorum: 3,
|
|
requirements: [{ ...requirement('matches', 'matches'), target: 1.5, minPlayers: 0 }]
|
|
};
|
|
expect(betaPlanIssues(plan, 2).map((issue) => issue.field)).toEqual([
|
|
'quorum',
|
|
'matches-target',
|
|
'matches-minPlayers'
|
|
]);
|
|
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('uses the shared count for every applicable game, including games without fleet matches', () => {
|
|
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(betaApplicableGames(files, []), 2)).toEqual({ t5: 2, t6: 2 });
|
|
expect(betaInstanceCounts(betaApplicableGames([files[1]], servers), 3)).toEqual({ t6: 3 });
|
|
for (const count of [null, 0, -1, 1.5, 1001])
|
|
expect(betaInstanceCounts(['t5', 't6'], count)).toEqual({});
|
|
expect(betaInstanceCounts([], 2)).toEqual({});
|
|
});
|
|
});
|
|
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', () => {
|
|
const r = { ...requirement('populated-minutes', 'play'), minPlayers: 3 };
|
|
expect(betaWaitingCondition(r)).toContain('No gameplay report');
|
|
expect(betaWaitingCondition(r, observation)).toContain('latest report has 2 players');
|
|
expect(betaWaitingCondition(r, { ...observation, players: 3 })).toContain(
|
|
'next report arrives'
|
|
);
|
|
});
|
|
it('summarizes telemetry while keeping arbitrary or legacy payloads readable', () => {
|
|
const event = {
|
|
id: 1,
|
|
server: 'town',
|
|
server_deleted_at: null,
|
|
received_at: '',
|
|
kind: 'game_end',
|
|
payload: JSON.stringify({ round: 25, players: [{ name: 'A' }], map: 'zm_transit' })
|
|
};
|
|
expect(betaEvidenceSummary(event)).toBe(
|
|
'Game ended at round 25 with 1 players on zm_transit.'
|
|
);
|
|
expect(betaEvidenceSummary({ ...event, kind: 'old-event', payload: 'invalid' })).toBe(
|
|
'old event'
|
|
);
|
|
});
|
|
});
|