Add Prettier and ESLint tooling (Refs #77)

This commit is contained in:
2026-10-09 15:56:13 -04:00
parent 8ed059b8d3
commit 16469e2d43
711 changed files with 99737 additions and 28286 deletions
+116 -38
View File
@@ -1,88 +1,166 @@
import { describe, expect, it } from 'vitest';
import { rebalanceSettings, type RebalanceSettings } from '../../src/lib/rebalancing.js';
import { planRebalance, rebalanceMoveBlocker, type RebalancingHost, type RebalancingInstance, type RebalancingSnapshot } from '../../src/lib/server/rebalancing/planner.js';
import {
planRebalance,
rebalanceMoveBlocker,
type RebalancingHost,
type RebalancingInstance,
type RebalancingSnapshot
} from '../../src/lib/server/rebalancing/planner.js';
const gib = 1024 ** 3;
const settings: RebalanceSettings = { hostIds: ['a', 'b'], game: 'all', maxMoves: 20 };
const host = (id: string, cpu: number, count = 0): RebalancingHost => ({ id, name: id, cpuCount: 8, memoryTotal: 16 * gib,
used: { cpuCores: cpu, memoryBytes: gib }, running: { cpuCores: 0.5, memoryBytes: gib },
startup: { cpuCores: 1, memoryBytes: 2 * gib }, instances: count, ports: 20, reason: null });
const instance = (n: number, source = 'a', cpu = 1): RebalancingInstance => ({ server: `t6_zombie_town_${n}`, instanceId: `i${n}`, generation: 1,
sourceHostId: source, game: 't6', relief: { cpuCores: cpu, memoryBytes: gib }, demand: { cpuCores: cpu, memoryBytes: gib }, destinations: { a: null, b: null } });
const snapshot = (hosts = [host('a', 6, 6), host('b', 0)], instances = Array.from({ length: 6 }, (_, n) => instance(n))): RebalancingSnapshot => ({ hosts, instances, exclusions: [], warnings: [] });
const host = (id: string, cpu: number, count = 0): RebalancingHost => ({
id,
name: id,
cpuCount: 8,
memoryTotal: 16 * gib,
used: { cpuCores: cpu, memoryBytes: gib },
running: { cpuCores: 0.5, memoryBytes: gib },
startup: { cpuCores: 1, memoryBytes: 2 * gib },
instances: count,
ports: 20,
reason: null
});
const instance = (n: number, source = 'a', cpu = 1): RebalancingInstance => ({
server: `t6_zombie_town_${n}`,
instanceId: `i${n}`,
generation: 1,
sourceHostId: source,
game: 't6',
relief: { cpuCores: cpu, memoryBytes: gib },
demand: { cpuCores: cpu, memoryBytes: gib },
destinations: { a: null, b: null }
});
const snapshot = (
hosts = [host('a', 6, 6), host('b', 0)],
instances = Array.from({ length: 6 }, (_, n) => instance(n))
): RebalancingSnapshot => ({ hosts, instances, exclusions: [], warnings: [] });
describe('rebalance planning', () => {
it('validates bounded hosts and moves without silently choosing all hosts', () => {
expect(rebalanceSettings({ ...settings, hostIds: ['b', 'a'] }).hostIds).toEqual(['a', 'b']);
for (const input of [{}, { ...settings, hostIds: ['a'] }, { ...settings, hostIds: ['a', 'a'] }, { ...settings, game: 't4' },
{ ...settings, maxMoves: 0 }, { ...settings, maxMoves: 21 }, { ...settings, maxMoves: 2.5 }]) expect(() => rebalanceSettings(input)).toThrow();
for (const input of [
{},
{ ...settings, hostIds: ['a'] },
{ ...settings, hostIds: ['a', 'a'] },
{ ...settings, game: 't4' },
{ ...settings, maxMoves: 0 },
{ ...settings, maxMoves: 21 },
{ ...settings, maxMoves: 2.5 }
])
expect(() => rebalanceSettings(input)).toThrow();
});
it('spreads pressure with the fewest useful moves and never mutates live measurements', () => {
const input = snapshot(), saved = structuredClone(input);
const input = snapshot(),
saved = structuredClone(input);
const result = planRebalance(input, settings);
expect(result.moves.map(move => [move.server, move.sourceHostId, move.targetHostId])).toEqual([0, 1, 2].map(n => [`t6_zombie_town_${n}`, 'a', 'b']));
expect(result.hosts.map(item => item.after.instances)).toEqual([3, 3]);
expect(result.hosts.map(item => item.after.cpuPercent)).toEqual([37.5, 37.5]);
expect(new Set(result.moves.map(move => move.instanceId)).size).toBe(result.moves.length);
expect(
result.moves.map((move) => [move.server, move.sourceHostId, move.targetHostId])
).toEqual([0, 1, 2].map((n) => [`t6_zombie_town_${n}`, 'a', 'b']));
expect(result.hosts.map((item) => item.after.instances)).toEqual([3, 3]);
expect(result.hosts.map((item) => item.after.cpuPercent)).toEqual([37.5, 37.5]);
expect(new Set(result.moves.map((move) => move.instanceId)).size).toBe(result.moves.length);
expect(input).toEqual(saved);
});
it('uses host size and both resource dimensions rather than equal counts', () => {
const small = host('a', 3, 3); small.cpuCount = 4;
const large = host('b', 4, 4); large.cpuCount = 16;
expect(planRebalance(snapshot([small, large], [instance(1)]), settings).moves).toHaveLength(1);
small.used = { cpuCores: 1, memoryBytes: 12 * gib }; large.used = { cpuCores: 1, memoryBytes: 2 * gib };
const memory = instance(1); memory.relief.memoryBytes = 2 * gib; memory.demand.memoryBytes = 2 * gib;
expect(planRebalance(snapshot([small, large], [memory]), settings).moves[0].improvement).toBeCloseTo(12.5);
const small = host('a', 3, 3);
small.cpuCount = 4;
const large = host('b', 4, 4);
large.cpuCount = 16;
expect(planRebalance(snapshot([small, large], [instance(1)]), settings).moves).toHaveLength(
1
);
small.used = { cpuCores: 1, memoryBytes: 12 * gib };
large.used = { cpuCores: 1, memoryBytes: 2 * gib };
const memory = instance(1);
memory.relief.memoryBytes = 2 * gib;
memory.demand.memoryBytes = 2 * gib;
expect(
planRebalance(snapshot([small, large], [memory]), settings).moves[0].improvement
).toBeCloseTo(12.5);
});
it('respects startup fit, port capacity, compatibility, host eligibility, and reservations', () => {
for (const mutate of [
(target: RebalancingHost) => { target.ports = 0; },
(target: RebalancingHost) => { target.reason = 'Host disabled.'; },
(target: RebalancingHost) => { target.startup.cpuCores = 8; },
(target: RebalancingHost) => { target.startup.memoryBytes = 16 * gib; },
(target: RebalancingHost) => { target.used.cpuCores = 7; }
(target: RebalancingHost) => {
target.ports = 0;
},
(target: RebalancingHost) => {
target.reason = 'Host disabled.';
},
(target: RebalancingHost) => {
target.startup.cpuCores = 8;
},
(target: RebalancingHost) => {
target.startup.memoryBytes = 16 * gib;
},
(target: RebalancingHost) => {
target.used.cpuCores = 7;
}
]) {
const input = snapshot(); mutate(input.hosts[1]);
const input = snapshot();
mutate(input.hosts[1]);
expect(planRebalance(input, settings).moves).toEqual([]);
}
const input = snapshot(); input.instances.forEach(item => { item.destinations.b = 'Missing the deployed runtime.'; });
const input = snapshot();
input.instances.forEach((item) => {
item.destinations.b = 'Missing the deployed runtime.';
});
expect(planRebalance(input, settings).moves).toEqual([]);
expect(planRebalance(input, settings).exclusions[0].reason).toContain('Missing the deployed runtime');
input.instances.forEach(item => { delete item.destinations.b; });
expect(planRebalance(input, settings).exclusions[0].reason).toContain(
'Missing the deployed runtime'
);
input.instances.forEach((item) => {
delete item.destinations.b;
});
expect(planRebalance(input, settings).moves).toEqual([]);
});
it('requires five percentage points of improvement, preserves limits, and does not move twice', () => {
const input = snapshot([host('a', 4), host('b', 2)], [instance(1, 'a', 0.3)]);
expect(planRebalance(input, settings).moves).toEqual([]);
input.instances[0].relief.cpuCores = 0.4; input.instances[0].demand.cpuCores = 0.4;
input.instances[0].relief.cpuCores = 0.4;
input.instances[0].demand.cpuCores = 0.4;
expect(planRebalance(input, settings).moves[0].improvement).toBeCloseTo(5);
expect(planRebalance(snapshot(), { ...settings, maxMoves: 1 }).moves).toHaveLength(1);
expect(planRebalance(snapshot(), { ...settings, game: 't5' }).moves).toHaveLength(0);
const onePort = snapshot(); onePort.hosts[1].ports = 1;
const onePort = snapshot();
onePort.hosts[1].ports = 1;
expect(planRebalance(onePort, settings).moves).toHaveLength(1);
});
it('prefers the largest improvement deterministically and revalidates the approved pair', () => {
const input = snapshot([host('a', 6, 6), host('b', 0)], [instance(3), instance(2, 'a', 2), instance(1)]);
const input = snapshot(
[host('a', 6, 6), host('b', 0)],
[instance(3), instance(2, 'a', 2), instance(1)]
);
const move = planRebalance(input, { ...settings, maxMoves: 1 }).moves[0];
expect(move.server).toBe('t6_zombie_town_2');
expect(rebalanceMoveBlocker(input, move)).toBeNull();
expect(rebalanceMoveBlocker(input, { ...move, generation: 2 })).toContain('generation has changed');
expect(rebalanceMoveBlocker(input, { ...move, generation: 2 })).toContain(
'generation has changed'
);
input.hosts[1].used.cpuCores = 6;
expect(rebalanceMoveBlocker(input, move)).toBeTruthy();
input.instances = []; input.exclusions.push({ server: move.server, reason: 'Players are present.' });
input.instances = [];
input.exclusions.push({ server: move.server, reason: 'Players are present.' });
expect(rebalanceMoveBlocker(input, move)).toBe('Players are present.');
});
it('only credits source ports actually released inside the configured range and bind', () => {
const a = host('a', 6.8, 1), b = host('b', 5.2, 1), c = host('c', 0);
a.ports = 0; b.ports = 0; c.ports = 1;
const first = instance(1, 'a', 4), second = instance(2, 'b', 1);
first.destinations.c = null; second.destinations.c = null;
const a = host('a', 6.8, 1),
b = host('b', 5.2, 1),
c = host('c', 0);
a.ports = 0;
b.ports = 0;
c.ports = 1;
const first = instance(1, 'a', 4),
second = instance(2, 'b', 1);
first.destinations.c = null;
second.destinations.c = null;
const input = snapshot([a, b, c], [first, second]);
const selection = { ...settings, hostIds: ['a', 'b', 'c'] };
expect(planRebalance(input, selection).moves).toHaveLength(1);