Support game-wide autoscaling rules with per-map targets

This commit is contained in:
2026-10-06 23:34:16 -04:00
parent a61eccaed6
commit bc744e2840
11 changed files with 152 additions and 32 deletions
+19 -5
View File
@@ -1,5 +1,6 @@
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { newFixture, type Fixture } from '../helpers/fixture.js';
import { zombieMaps } from '../../src/lib/map-aliases.js';
import { prepareNativeHost } from '../helpers/native-host.js';
vi.mock('../../src/lib/server/integrations/rcon.js', () => ({ rconGetPlayerCount: vi.fn(async () => 0) }));
@@ -9,24 +10,30 @@ afterEach(async () => { vi.useRealTimers(); await fixture.cleanup(); vi.restoreA
const names = ['t6_zombie_town_1', 't6_zombie_town_2'];
const settings = { game: 't6', mapSlug: 'town', maximum: 2, mode: 'create-destroy', emptyMinutes: 1, cooldownMinutes: 1 };
async function setup() {
const host = await prepareNativeHost(names);
async function setup(gameWide = false) {
const servers = gameWide ? [...names, ...zombieMaps('t6').filter(m => m.slug !== 'town').map(m => `t6_zombie_${m.slug}_1`)] : names;
const policySettings = { ...settings, mapSlug: gameWide ? '*' : settings.mapSlug };
const host = await prepareNativeHost(servers);
const { db } = await import('../../src/lib/server/db/index.js');
const scaling = await import('../../src/lib/server/autoscaling/index.js');
const authorization = await import('../../src/lib/server/autoscaling/authorization.js');
db().prepare("UPDATE ec_hosts SET inventory=json_set(inventory,'$.capabilities',json(?)) WHERE id=?").run(JSON.stringify([authorization.autoscalingRemovalCapability]), host.host.id);
const base = Date.now(); vi.useFakeTimers({ toFake: ['Date'] }); vi.setSystemTime(base);
for (const name of names) {
db().prepare("INSERT INTO servers(name,game,map,map_slug,container_state,last_players) VALUES(?,'t6','zm_transit','town','running',0)").run(name);
for (const name of servers) {
const map = zombieMaps('t6').find(m => name === `t6_zombie_${m.slug}_1` || name === `t6_zombie_${m.slug}_2`)!;
db().prepare("INSERT INTO servers(name,game,map,map_slug,container_state,last_players) VALUES(?,'t6',?,?,'running',0)").run(name, map.canonical, map.slug);
db().prepare('INSERT INTO server_keys(server_id,pluto_key) VALUES(?,?)').run(name, `key-${name}`);
}
if (gameWide) for (const map of zombieMaps('t6').filter(m => m.slug !== 'town')) {
db().prepare('INSERT INTO server_keys(server_id,pluto_key) VALUES(?,?)').run(`t6_zombie_${map.slug}_2`, `key-${map.slug}-2`);
}
const fresh = (offset: number) => {
vi.setSystemTime(base + offset); const at = new Date().toISOString();
db().prepare('UPDATE servers SET container_checked_at=?,last_seen_at=?,last_checked_at=?').run(at, at, at);
db().prepare('UPDATE ec_hosts SET last_heartbeat=?').run(at);
};
fresh(0);
const saved = scaling.saveScaling(settings, undefined, undefined, scaling.previewScaling(settings).token);
const saved = scaling.saveScaling(policySettings, undefined, undefined, scaling.previewScaling(policySettings).token);
for (const offset of [0, 30_000, 60_000]) { fresh(offset); await scaling.runAutoscaling(); }
const intent = db().prepare('SELECT operation_id FROM autoscaling_decisions WHERE resolved=0').get() as { operation_id: string };
expect(intent?.operation_id).toBeTruthy();
@@ -103,3 +110,10 @@ it('rejects expired, changed revision, and newly protected removal authorization
s.fresh(130_000);
expect(s.authorization.claimRemovalAuthorization(s.hostId, command.id, command.payload).authorized).toBe(false);
});
it('authorizes a game-wide removal without trimming any map below its targets', async () => {
const s = await setup(true);
expect(s.db().prepare("SELECT server FROM server_deployments WHERE id=?").get(s.operationId)).toEqual({ server: names[0] });
const command = await s.dispatch();
expect(s.authorization.claimRemovalAuthorization(s.hostId, command.id, command.payload).authorized).toBe(true);
});