148 lines
6.4 KiB
TypeScript
148 lines
6.4 KiB
TypeScript
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
|
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
|
import { prepareNativeHost } from '../helpers/native-host.js';
|
|
import { zombieMaps } from '../../src/lib/map-aliases.js';
|
|
|
|
let fixture: Fixture;
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
fixture = await newFixture([]);
|
|
});
|
|
afterEach(async () => {
|
|
vi.useRealTimers();
|
|
await fixture.cleanup();
|
|
});
|
|
const server = 't6_zombie_town_1';
|
|
|
|
async function setup(mapSlug: string) {
|
|
const maps = zombieMaps('t6').filter((map) => mapSlug === '*' || map.slug === mapSlug);
|
|
const names = maps.map((map) => `t6_zombie_${map.slug}_1`);
|
|
const { host } = await prepareNativeHost(names);
|
|
const { db } = await import('../../src/lib/server/db/index.js');
|
|
const scaling = await import('../../src/lib/server/autoscaling/index.js');
|
|
const { autoscalingRemovalCapability } =
|
|
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([autoscalingRemovalCapability]), host.id);
|
|
const at = new Date().toISOString();
|
|
for (const map of maps) {
|
|
const name = `t6_zombie_${map.slug}_1`;
|
|
db()
|
|
.prepare(
|
|
`INSERT INTO servers(name,game,map,map_slug,container_state,container_checked_at,last_seen_at,last_checked_at,last_players)
|
|
VALUES(?,'t6',?,?,'running',?,?,?,0)`
|
|
)
|
|
.run(name, map.canonical, map.slug, at, at, at);
|
|
db()
|
|
.prepare("INSERT INTO server_keys(id,game,pluto_key) VALUES(?,'t6',?)")
|
|
.run(name, `key-${name}`);
|
|
db().prepare('INSERT INTO server_key_leases(server_id,key_id) VALUES(?,?)').run(name, name);
|
|
}
|
|
// A free key ensures a lack of capacity cannot hide unwanted scale-up.
|
|
db()
|
|
.prepare("INSERT INTO server_keys(id,game,pluto_key) VALUES('spare','t6','spare-key')")
|
|
.run();
|
|
const policy = scaling.saveScaling({ game: 't6', mapSlug, maximum: 2, mode: 'create-destroy' });
|
|
const { requestLifecycle } = await import('../../src/lib/server/provisioning/requests.js');
|
|
return { db, scaling, policy, requestLifecycle };
|
|
}
|
|
|
|
it.each(['town', '*'])('keeps lineup-changing actions blocked for %s ownership', async (scope) => {
|
|
const { db, requestLifecycle } = await setup(scope);
|
|
const { destroyServer } = await import('../../src/lib/server/provisioning/instances.js');
|
|
for (const action of ['start', 'stop'] as const) {
|
|
expect(requestLifecycle(server, action)).toMatchObject({
|
|
ok: false,
|
|
status: 409,
|
|
error: expect.stringContaining('Autoscaling')
|
|
});
|
|
}
|
|
expect(destroyServer(server)).toMatchObject({
|
|
ok: false,
|
|
message: expect.stringContaining('Autoscaling')
|
|
});
|
|
expect(db().prepare('SELECT id FROM server_deployments').all()).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
['town', 'restart'],
|
|
['town', 'rebuild'],
|
|
['*', 'restart'],
|
|
['*', 'rebuild']
|
|
] as const)(
|
|
'allows %s-owned %s without scaling up during work or recovery',
|
|
async (scope, action) => {
|
|
vi.useFakeTimers({ toFake: ['Date'] });
|
|
const { db, scaling, policy, requestLifecycle } = await setup(scope);
|
|
const { scalingSnapshot } = await import('../../src/lib/server/autoscaling/snapshot.js');
|
|
const result = requestLifecycle(server, action);
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) throw new Error(result.error);
|
|
expect(requestLifecycle(server, action)).toEqual(result);
|
|
|
|
for (const state of ['waiting', 'queued', 'running', 'awaiting-health', 'interrupted']) {
|
|
db()
|
|
.prepare('UPDATE server_deployments SET state=? WHERE id=?')
|
|
.run(state, result.deploymentId);
|
|
db()
|
|
.prepare(
|
|
"UPDATE servers SET container_state='stopped',last_players=NULL WHERE name=?"
|
|
)
|
|
.run(server);
|
|
const snapshot = scalingSnapshot(policy);
|
|
expect(snapshot.members.find((member) => member.name === server)).toMatchObject({
|
|
state: 'protected',
|
|
operationId: result.deploymentId
|
|
});
|
|
expect(snapshot.reservedNames).toContain(server);
|
|
expect(await scaling.runAutoscaling()).toBe(false);
|
|
}
|
|
|
|
// A terminal operation with unresolved recovery must still hold capacity.
|
|
db()
|
|
.prepare("UPDATE server_deployments SET state='failed' WHERE id=?")
|
|
.run(result.deploymentId);
|
|
const requestedAt = new Date().toISOString();
|
|
db()
|
|
.prepare('INSERT INTO restart_rollout(server,requested_at,operation_id) VALUES(?,?,?)')
|
|
.run(server, requestedAt, result.deploymentId);
|
|
expect(
|
|
scalingSnapshot(policy).members.find((member) => member.name === server)?.blocker
|
|
).toBe('Unresolved recovery');
|
|
expect(await scaling.runAutoscaling()).toBe(false);
|
|
expect(
|
|
db()
|
|
.prepare("SELECT id FROM server_deployments WHERE action IN ('create','destroy')")
|
|
.all()
|
|
).toEqual([]);
|
|
|
|
// Recovery status must be strictly newer than the restart request.
|
|
const at = new Date(Date.parse(requestedAt) + 1_000).toISOString();
|
|
vi.setSystemTime(new Date(at));
|
|
db()
|
|
.prepare("UPDATE server_deployments SET state='done',finished_at=? WHERE id=?")
|
|
.run(at, result.deploymentId);
|
|
db().prepare('UPDATE restart_rollout SET healthy_at=? WHERE server=?').run(at, server);
|
|
db()
|
|
.prepare(
|
|
"UPDATE servers SET container_state='running',last_players=0,container_checked_at=?,last_seen_at=?,last_checked_at=?"
|
|
)
|
|
.run(at, at, at);
|
|
expect(await scaling.runAutoscaling()).toBe(false);
|
|
expect(scaling.getScalingPolicy(policy.id)).toMatchObject({
|
|
mode: 'create-destroy',
|
|
pausedReason: null
|
|
});
|
|
|
|
// Once recovered, actual occupancy can request a new spare normally.
|
|
db().prepare('UPDATE servers SET last_players=1 WHERE name=?').run(server);
|
|
expect(await scaling.runAutoscaling()).toBe(true);
|
|
expect(
|
|
db().prepare("SELECT server FROM server_deployments WHERE action='create'").all()
|
|
).toEqual([{ server: 't6_zombie_town_2' }]);
|
|
}
|
|
);
|