Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -13,15 +13,26 @@ beforeEach(async () => {
|
||||
await seedServerRegistry();
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const now = new Date().toISOString();
|
||||
db().prepare(`UPDATE servers SET container_state='running', container_checked_at=?,
|
||||
last_checked_at=?, last_seen_at=?, last_players=0`).run(now, now, now);
|
||||
db()
|
||||
.prepare(
|
||||
`UPDATE servers SET container_state='running', container_checked_at=?,
|
||||
last_checked_at=?, last_seen_at=?, last_players=0`
|
||||
)
|
||||
.run(now, now, now);
|
||||
});
|
||||
afterEach(async () => {
|
||||
await fixture.cleanup();
|
||||
});
|
||||
afterEach(async () => { await fixture.cleanup(); });
|
||||
|
||||
async function file(scopeType: 'game' | 'map' = 'game', scopeValue = 't5') {
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
return createScript({ name: `Test ${scopeValue}`, targetRelPath: `${scopeValue}.gsc`,
|
||||
scopeType, scopeValue, content: Buffer.from('beta bytes') });
|
||||
return createScript({
|
||||
name: `Test ${scopeValue}`,
|
||||
targetRelPath: `${scopeValue}.gsc`,
|
||||
scopeType,
|
||||
scopeValue,
|
||||
content: Buffer.from('beta bytes')
|
||||
});
|
||||
}
|
||||
|
||||
describe('automatic beta server selection', () => {
|
||||
@@ -31,61 +42,123 @@ describe('automatic beta server selection', () => {
|
||||
db().prepare('UPDATE servers SET last_players=4 WHERE name=?').run(t5[0]);
|
||||
db().prepare('UPDATE servers SET last_players=2 WHERE name=?').run(t6[0]);
|
||||
const { selectBetaServers } = await import('../../src/lib/server/fleet/beta-selection.js');
|
||||
expect(selectBetaServers(files.map(f => f.script), { t5: 2, t6: 1 })).toEqual([t5[2], t5[1], t6[1]]);
|
||||
expect(
|
||||
selectBetaServers(
|
||||
files.map((f) => f.script),
|
||||
{ t5: 2, t6: 1 }
|
||||
)
|
||||
).toEqual([t5[2], t5[1], t6[1]]);
|
||||
});
|
||||
|
||||
it('accounts for occupied telemetry and fresh positive RCON evidence', async () => {
|
||||
const { script } = await file();
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { selectBetaServers } = await import('../../src/lib/server/fleet/beta-selection.js');
|
||||
const put = db().prepare('INSERT INTO server_telemetry(server,round,players,received_at,payload) VALUES(?,0,?,?,?)');
|
||||
const report = (server: string, players: number) => ({ event: 'game_start', server, map: 'zombie_theater',
|
||||
receivedAt: new Date().toISOString(), players: Array.from({ length: players }, (_, i) => ({ name: `P${i}`, guid: String(i) })) });
|
||||
const put = db().prepare(
|
||||
'INSERT INTO server_telemetry(server,round,players,received_at,payload) VALUES(?,0,?,?,?)'
|
||||
);
|
||||
const report = (server: string, players: number) => ({
|
||||
event: 'game_start',
|
||||
server,
|
||||
map: 'zombie_theater',
|
||||
receivedAt: new Date().toISOString(),
|
||||
players: Array.from({ length: players }, (_, i) => ({ name: `P${i}`, guid: String(i) }))
|
||||
});
|
||||
put.run(t5[2], 3, new Date().toISOString(), JSON.stringify(report(t5[2], 3)));
|
||||
put.run(t5[0], 0, new Date().toISOString(), JSON.stringify(report(t5[0], 0)));
|
||||
db().prepare('UPDATE servers SET last_players=2 WHERE name=?').run(t5[0]);
|
||||
expect(selectBetaServers([script], { t5: 1 })).toEqual([t5[1]]);
|
||||
});
|
||||
|
||||
it.each(['stale', 'stopped', 'destroyed', 'provisioning', 'beta', 'campaign'])('excludes %s servers', async reason => {
|
||||
const { script, version } = await file();
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const handle = db(), server = t5[2];
|
||||
if (reason === 'stale') handle.prepare("UPDATE servers SET last_seen_at='2000-01-01T00:00:00Z' WHERE name=?").run(server);
|
||||
if (reason === 'stopped') handle.prepare("UPDATE servers SET desired_state='stopped' WHERE name=?").run(server);
|
||||
if (reason === 'destroyed') handle.prepare("UPDATE servers SET destroyed_at='2020-01-01T00:00:00Z' WHERE name=?").run(server);
|
||||
if (reason === 'provisioning') handle.prepare(`INSERT INTO server_deployments(id,server,action,state,stage,started_at,triggered_by)
|
||||
VALUES('busy',?,'destroy','queued','Queued',?,'test')`).run(server, new Date().toISOString());
|
||||
if (reason === 'beta') {
|
||||
const { openBeta } = await import('../../src/lib/server/fleet/beta.js');
|
||||
await openBeta({ scripts: [{ scriptId: script.id, scriptVersionId: version.id }], servers: [server], waitForHealth: false, waitForMatch: false });
|
||||
it.each(['stale', 'stopped', 'destroyed', 'provisioning', 'beta', 'campaign'])(
|
||||
'excludes %s servers',
|
||||
async (reason) => {
|
||||
const { script, version } = await file();
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const handle = db(),
|
||||
server = t5[2];
|
||||
if (reason === 'stale')
|
||||
handle
|
||||
.prepare("UPDATE servers SET last_seen_at='2000-01-01T00:00:00Z' WHERE name=?")
|
||||
.run(server);
|
||||
if (reason === 'stopped')
|
||||
handle
|
||||
.prepare("UPDATE servers SET desired_state='stopped' WHERE name=?")
|
||||
.run(server);
|
||||
if (reason === 'destroyed')
|
||||
handle
|
||||
.prepare("UPDATE servers SET destroyed_at='2020-01-01T00:00:00Z' WHERE name=?")
|
||||
.run(server);
|
||||
if (reason === 'provisioning')
|
||||
handle
|
||||
.prepare(
|
||||
`INSERT INTO server_deployments(id,server,action,state,stage,started_at,triggered_by)
|
||||
VALUES('busy',?,'destroy','queued','Queued',?,'test')`
|
||||
)
|
||||
.run(server, new Date().toISOString());
|
||||
if (reason === 'beta') {
|
||||
const { openBeta } = await import('../../src/lib/server/fleet/beta.js');
|
||||
await openBeta({
|
||||
scripts: [{ scriptId: script.id, scriptVersionId: version.id }],
|
||||
servers: [server],
|
||||
waitForHealth: false,
|
||||
waitForMatch: false
|
||||
});
|
||||
}
|
||||
if (reason === 'campaign') {
|
||||
handle
|
||||
.prepare(
|
||||
"INSERT INTO deployments(id,action,triggered_by,started_at) VALUES('campaign','promote','test',?)"
|
||||
)
|
||||
.run(new Date().toISOString());
|
||||
handle
|
||||
.prepare(
|
||||
"INSERT INTO deployment_servers(deployment_id,server,files_changed,restart_state) VALUES('campaign',?,1,'pending')"
|
||||
)
|
||||
.run(server);
|
||||
}
|
||||
const { selectBetaServers } =
|
||||
await import('../../src/lib/server/fleet/beta-selection.js');
|
||||
expect(selectBetaServers([script], { t5: 1 })).toEqual([t5[0]]);
|
||||
}
|
||||
if (reason === 'campaign') {
|
||||
handle.prepare("INSERT INTO deployments(id,action,triggered_by,started_at) VALUES('campaign','promote','test',?)").run(new Date().toISOString());
|
||||
handle.prepare("INSERT INTO deployment_servers(deployment_id,server,files_changed,restart_state) VALUES('campaign',?,1,'pending')").run(server);
|
||||
}
|
||||
const { selectBetaServers } = await import('../../src/lib/server/fleet/beta-selection.js');
|
||||
expect(selectBetaServers([script], { t5: 1 })).toEqual([t5[0]]);
|
||||
});
|
||||
);
|
||||
|
||||
it.each(['disabled', 'draining', 'disconnected', 'deleted'])('excludes instances on a %s host despite fresh game status', async state => {
|
||||
const { script } = await file();
|
||||
const { host } = await prepareNativeHost([t5[2]]);
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
if (state === 'disconnected') db().prepare('UPDATE ec_hosts SET last_heartbeat=NULL WHERE id=?').run(host.id);
|
||||
else if (state === 'deleted') db().prepare('UPDATE ec_hosts SET deleted_at=? WHERE id=?').run(new Date().toISOString(), host.id);
|
||||
else db().prepare('UPDATE ec_hosts SET mode=? WHERE id=?').run(state, host.id);
|
||||
const { selectBetaServers } = await import('../../src/lib/server/fleet/beta-selection.js');
|
||||
expect(selectBetaServers([script], { t5: 1 })).toEqual([t5[0]]);
|
||||
});
|
||||
it.each(['disabled', 'draining', 'disconnected', 'deleted'])(
|
||||
'excludes instances on a %s host despite fresh game status',
|
||||
async (state) => {
|
||||
const { script } = await file();
|
||||
const { host } = await prepareNativeHost([t5[2]]);
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
if (state === 'disconnected')
|
||||
db().prepare('UPDATE ec_hosts SET last_heartbeat=NULL WHERE id=?').run(host.id);
|
||||
else if (state === 'deleted')
|
||||
db()
|
||||
.prepare('UPDATE ec_hosts SET deleted_at=? WHERE id=?')
|
||||
.run(new Date().toISOString(), host.id);
|
||||
else db().prepare('UPDATE ec_hosts SET mode=? WHERE id=?').run(state, host.id);
|
||||
const { selectBetaServers } =
|
||||
await import('../../src/lib/server/fleet/beta-selection.js');
|
||||
expect(selectBetaServers([script], { t5: 1 })).toEqual([t5[0]]);
|
||||
}
|
||||
);
|
||||
|
||||
it('covers every file scope instead of filling all slots from the emptiest map', async () => {
|
||||
const files = [await file('map', 'zombie_theater'), await file('map', 'zombie_cosmodrome')];
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
db().prepare('UPDATE servers SET last_players=3 WHERE name=?').run(t5[2]);
|
||||
const { selectBetaServers } = await import('../../src/lib/server/fleet/beta-selection.js');
|
||||
expect(selectBetaServers(files.map(f => f.script), { t5: 2 })).toEqual([t5[0], t5[2]]);
|
||||
expect(() => selectBetaServers(files.map(f => f.script), { t5: 1 })).toThrow('every selected file');
|
||||
expect(
|
||||
selectBetaServers(
|
||||
files.map((f) => f.script),
|
||||
{ t5: 2 }
|
||||
)
|
||||
).toEqual([t5[0], t5[2]]);
|
||||
expect(() =>
|
||||
selectBetaServers(
|
||||
files.map((f) => f.script),
|
||||
{ t5: 1 }
|
||||
)
|
||||
).toThrow('every selected file');
|
||||
});
|
||||
|
||||
it('reserves game slots when a map can be tested in either game', async () => {
|
||||
@@ -94,17 +167,28 @@ describe('automatic beta server selection', () => {
|
||||
db().prepare("UPDATE servers SET map='shared'").run();
|
||||
db().prepare("UPDATE servers SET map='only_t5', last_players=4 WHERE name=?").run(t5[2]);
|
||||
const { selectBetaServers } = await import('../../src/lib/server/fleet/beta-selection.js');
|
||||
expect(selectBetaServers(files.map(f => f.script), { t5: 1, t6: 1 })).toEqual([t6[0], t5[2]]);
|
||||
expect(
|
||||
selectBetaServers(
|
||||
files.map((f) => f.script),
|
||||
{ t5: 1, t6: 1 }
|
||||
)
|
||||
).toEqual([t6[0], t5[2]]);
|
||||
});
|
||||
|
||||
it('fails insufficient capacity or untested files before creating a beta', async () => {
|
||||
const files = [await file(), await file('game', 't6')];
|
||||
const { POST } = await import('../../src/routes/api/betas/+server.js');
|
||||
const scripts = files.map(f => ({ scriptId: f.script.id, scriptVersionId: f.version.id }));
|
||||
const scripts = files.map((f) => ({
|
||||
scriptId: f.script.id,
|
||||
scriptVersionId: f.version.id
|
||||
}));
|
||||
for (const instancesPerGame of [{ t5: 4, t6: 1 }, { t5: 1 }]) {
|
||||
const response = await POST({ request: new Request('http://test/api/betas', {
|
||||
method: 'POST', body: JSON.stringify({ scripts, instancesPerGame })
|
||||
}) });
|
||||
const response = await POST({
|
||||
request: new Request('http://test/api/betas', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ scripts, instancesPerGame })
|
||||
})
|
||||
});
|
||||
expect(response.status).toBe(409);
|
||||
}
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
@@ -115,10 +199,19 @@ describe('automatic beta server selection', () => {
|
||||
const files = [await file(), await file('game', 't6')];
|
||||
const { POST } = await import('../../src/routes/api/betas/+server.js');
|
||||
const { defaultPlan } = await import('../../src/lib/beta-plan.js');
|
||||
const response = await POST({ request: new Request('http://test/api/betas', { method: 'POST', body: JSON.stringify({
|
||||
scripts: files.map(f => ({ scriptId: f.script.id, scriptVersionId: f.version.id })), instancesPerGame: { t5: 2, t6: 1 },
|
||||
testPlan: { ...defaultPlan(), scope: 'quorum', quorum: 1 }
|
||||
}) }) });
|
||||
const response = await POST({
|
||||
request: new Request('http://test/api/betas', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
scripts: files.map((f) => ({
|
||||
scriptId: f.script.id,
|
||||
scriptVersionId: f.version.id
|
||||
})),
|
||||
instancesPerGame: { t5: 2, t6: 1 },
|
||||
testPlan: { ...defaultPlan(), scope: 'quorum', quorum: 1 }
|
||||
})
|
||||
})
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
const body = await response.json();
|
||||
expect(body.result.servers).toEqual([t5[2], t5[0], t6[0]]);
|
||||
@@ -128,21 +221,44 @@ describe('automatic beta server selection', () => {
|
||||
expect(db().prepare('SELECT * FROM beta_server_scripts').all()).toHaveLength(3);
|
||||
});
|
||||
|
||||
it.each([{}, { t5: 0 }, { t5: -1 }, { t5: 1.5 }, { t5: '2' }, { t5: 1001 }, { t7: 1 }, [], null])('rejects malformed counts %j', async instancesPerGame => {
|
||||
it.each([
|
||||
{},
|
||||
{ t5: 0 },
|
||||
{ t5: -1 },
|
||||
{ t5: 1.5 },
|
||||
{ t5: '2' },
|
||||
{ t5: 1001 },
|
||||
{ t7: 1 },
|
||||
[],
|
||||
null
|
||||
])('rejects malformed counts %j', async (instancesPerGame) => {
|
||||
const { script, version } = await file();
|
||||
const { POST } = await import('../../src/routes/api/betas/+server.js');
|
||||
const response = await POST({ request: new Request('http://test/api/betas', { method: 'POST', body: JSON.stringify({
|
||||
scripts: [{ scriptId: script.id, scriptVersionId: version.id }], instancesPerGame
|
||||
}) }) });
|
||||
const response = await POST({
|
||||
request: new Request('http://test/api/betas', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
scripts: [{ scriptId: script.id, scriptVersionId: version.id }],
|
||||
instancesPerGame
|
||||
})
|
||||
})
|
||||
});
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('rejects ambiguous simultaneous manual and automatic targeting', async () => {
|
||||
const { script, version } = await file();
|
||||
const { POST } = await import('../../src/routes/api/betas/+server.js');
|
||||
const response = await POST({ request: new Request('http://test/api/betas', { method: 'POST', body: JSON.stringify({
|
||||
scripts: [{ scriptId: script.id, scriptVersionId: version.id }], instancesPerGame: { t5: 1 }, servers: [t5[0]]
|
||||
}) }) });
|
||||
const response = await POST({
|
||||
request: new Request('http://test/api/betas', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
scripts: [{ scriptId: script.id, scriptVersionId: version.id }],
|
||||
instancesPerGame: { t5: 1 },
|
||||
servers: [t5[0]]
|
||||
})
|
||||
})
|
||||
});
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user