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
+37
View File
@@ -68,6 +68,43 @@ async function tick(seconds = 15, players = 4, healthy = true) {
const scriptPath = (name: string) => join(fixture.t5, name, 'runtime/plutonium/storage/t5/raw/scripts/sp/test.gsc');
describe('durable beta test plans', () => {
it.each([
{ scope: 'quorum', fileScope: 'game', otherServer: 't6_zombie_town_1', otherMap: 'zm_transit' },
{ scope: 'game', fileScope: 'map', otherServer: 't5_zombie_cosmodrome_1', otherMap: 'zombie_cosmodrome' }
] as const)('requires a passing participant for every file with $scope coverage', async ({ scope, fileScope, otherServer, otherMap }) => {
const otherGame = otherServer.slice(0, 2);
await mkdir(otherGame === 't6' ? join(fixture.t6, otherServer, 'runtime/plutonium/storage/t6/scripts/zm')
: join(fixture.t5, otherServer, 'runtime/plutonium/storage/t5/raw/scripts/sp'), { recursive: true });
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
await seedServerRegistry();
const { createScript, getVersion } = await import('../../src/lib/server/fleet/scripts.js');
const { openBeta, getBeta, listBetaServers, promoteBeta } = await import('../../src/lib/server/fleet/beta.js');
const { testView } = await import('../../src/lib/server/fleet/beta-tests.js');
const files = [fileScope === 'game' ? 't5' : 'zombie_theater', fileScope === 'game' ? otherGame : otherMap]
.map(scopeValue => createScript({ name: `Test ${scopeValue}`, targetRelPath: `${scopeValue}.gsc`,
scopeType: fileScope, scopeValue, content: Buffer.from(`${scopeValue} beta`) }));
const { betaId } = await openBeta({
scripts: files.map(file => ({ scriptId: file.script.id, scriptVersionId: file.version.id })),
servers: [...names, otherServer], waitForHealth: true, waitForMatch: false,
testPlan: { ...defaultPlan(), scope, quorum: 1, requirements: [requirement('population', 'players')] }
});
await tick(); await tick();
await postTelemetry(gameEvent('game_start')); await tick();
expect(listBetaServers(betaId).find(server => server.server === names[0])?.stage).toBe('ready');
expect(getBeta(betaId)?.state).toBe('running');
expect(testView(betaId)?.outcome).toBe('active');
expect(files.map(file => getVersion(file.version.id)?.state)).toEqual(['untested', 'untested']);
await expect(promoteBeta(betaId)).rejects.toThrow('Test plan has not passed');
await postTelemetry(gameEvent('game_start', 25, 4, otherServer, otherMap)); await tick();
expect(getBeta(betaId)?.state).toBe('ready-promote');
expect(testView(betaId)?.outcome).toBe('passed');
expect(files.map(file => getVersion(file.version.id)?.state)).toEqual(['tested', 'tested']);
// Coverage still permits replicas to remain unproven once each file
// has a passing participant; it does not silently become an all gate.
expect(listBetaServers(betaId).find(server => server.server === names[1])?.stage).toBe('observing');
});
it('requires fresh gameplay after a revision and promotes exactly the replacement version', async () => {
const id = await openPlan({ ...defaultPlan(), requirements: [requirement('matches', 'matches')] });
await tick(); await tick();