Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -2,25 +2,57 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||
|
||||
let fixture: Fixture;
|
||||
import { defaultFleetSettings as defaults, additionalConcurrencyKeys, fleetSettingLabels, settingDisplayValue, settingHelp,
|
||||
settingMinimum, settingsGroups, settingUnit, settingZeroMode, validateFleetSettings, type FleetSettings } from '../../src/lib/fleet-settings.js';
|
||||
import {
|
||||
defaultFleetSettings as defaults,
|
||||
additionalConcurrencyKeys,
|
||||
fleetSettingLabels,
|
||||
settingDisplayValue,
|
||||
settingHelp,
|
||||
settingMinimum,
|
||||
settingsGroups,
|
||||
settingUnit,
|
||||
settingZeroMode,
|
||||
validateFleetSettings,
|
||||
type FleetSettings
|
||||
} from '../../src/lib/fleet-settings.js';
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
fixture = await newFixture([]);
|
||||
for (const key of ['DEPLOY_KEEP_DAYS', 'DEPLOYMENT_RESTART_CONCURRENCY', 'DEPLOYMENT_CREATE_CONCURRENCY']) vi.stubEnv(key, undefined);
|
||||
for (const key of [
|
||||
'DEPLOY_KEEP_DAYS',
|
||||
'DEPLOYMENT_RESTART_CONCURRENCY',
|
||||
'DEPLOYMENT_CREATE_CONCURRENCY'
|
||||
])
|
||||
vi.stubEnv(key, undefined);
|
||||
});
|
||||
afterEach(async () => {
|
||||
await fixture.cleanup();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
afterEach(async () => { await fixture.cleanup(); vi.unstubAllEnvs(); });
|
||||
|
||||
describe('fleet settings API and persistence', () => {
|
||||
it('returns defaults and saves all settings across a module reload and database reopen', async () => {
|
||||
const { GET, PUT } = await import('../../src/routes/api/settings/+server.js');
|
||||
expect(await GET().json()).toEqual(defaults);
|
||||
const saved = { ...defaults, deployKeepDays: 30, observationKeepDays: 5, restartConcurrency: 3, createConcurrency: 2, destroyConcurrency: 4 };
|
||||
const response = await PUT({ request: new Request('http://localhost/api/settings', { method: 'PUT', body: JSON.stringify(saved) }) });
|
||||
const saved = {
|
||||
...defaults,
|
||||
deployKeepDays: 30,
|
||||
observationKeepDays: 5,
|
||||
restartConcurrency: 3,
|
||||
createConcurrency: 2,
|
||||
destroyConcurrency: 4
|
||||
};
|
||||
const response = await PUT({
|
||||
request: new Request('http://localhost/api/settings', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(saved)
|
||||
})
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toEqual(saved);
|
||||
const { closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
closeDb(); vi.resetModules();
|
||||
closeDb();
|
||||
vi.resetModules();
|
||||
// Once saved, even invalid legacy environment values are irrelevant.
|
||||
vi.stubEnv('DEPLOY_KEEP_DAYS', '-1');
|
||||
vi.stubEnv('DEPLOYMENT_RESTART_CONCURRENCY', 'bad');
|
||||
@@ -38,68 +70,140 @@ describe('fleet settings API and persistence', () => {
|
||||
vi.stubEnv('DEPLOYMENT_RESTART_CONCURRENCY', '4');
|
||||
vi.stubEnv('DEPLOYMENT_CREATE_CONCURRENCY', '2');
|
||||
const { fleetSettings } = await import('../../src/lib/server/settings.js');
|
||||
expect(fleetSettings()).toEqual({ ...defaults, deployKeepDays: 14, observationKeepDays: 1, restartConcurrency: 4, createConcurrency: 2, destroyConcurrency: 1 });
|
||||
expect(fleetSettings()).toEqual({
|
||||
...defaults,
|
||||
deployKeepDays: 14,
|
||||
observationKeepDays: 1,
|
||||
restartConcurrency: 4,
|
||||
createConcurrency: 2,
|
||||
destroyConcurrency: 1
|
||||
});
|
||||
closeDb();
|
||||
vi.stubEnv('DEPLOY_KEEP_DAYS', '7');
|
||||
expect(fleetSettings().deployKeepDays).toBe(14);
|
||||
expect(db().prepare("SELECT value FROM settings WHERE key = 'unrelated'").get()).toEqual({ value: 'true' });
|
||||
expect(db().prepare("SELECT value FROM settings WHERE key = 'unrelated'").get()).toEqual({
|
||||
value: 'true'
|
||||
});
|
||||
});
|
||||
|
||||
it('upgrades saved settings with a default destruction limit while retaining existing values', async () => {
|
||||
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
const previous = { deployKeepDays: 21, restartConcurrency: 3, createConcurrency: 2 };
|
||||
db().prepare("UPDATE settings SET value = ? WHERE key = 'fleet_settings'").run(JSON.stringify(previous));
|
||||
db()
|
||||
.prepare("UPDATE settings SET value = ? WHERE key = 'fleet_settings'")
|
||||
.run(JSON.stringify(previous));
|
||||
closeDb();
|
||||
vi.stubEnv('DEPLOY_KEEP_DAYS', 'bad');
|
||||
vi.stubEnv('DEPLOYMENT_RESTART_CONCURRENCY', '9');
|
||||
vi.stubEnv('DEPLOYMENT_CREATE_CONCURRENCY', '8');
|
||||
const { GET } = await import('../../src/routes/api/settings/+server.js');
|
||||
const upgraded = { ...defaults, ...previous, destroyConcurrency: 1, observationKeepDays: 1 };
|
||||
const upgraded = {
|
||||
...defaults,
|
||||
...previous,
|
||||
destroyConcurrency: 1,
|
||||
observationKeepDays: 1
|
||||
};
|
||||
expect(await GET().json()).toEqual(upgraded);
|
||||
expect(JSON.parse((db().prepare("SELECT value FROM settings WHERE key = 'fleet_settings'").get() as { value: string }).value)).toEqual(upgraded);
|
||||
expect(
|
||||
JSON.parse(
|
||||
(
|
||||
db()
|
||||
.prepare("SELECT value FROM settings WHERE key = 'fleet_settings'")
|
||||
.get() as { value: string }
|
||||
).value
|
||||
)
|
||||
).toEqual(upgraded);
|
||||
closeDb();
|
||||
expect(await GET().json()).toEqual(upgraded);
|
||||
});
|
||||
|
||||
it('adds routine observation retention to modern saved settings without changing any existing values', async () => {
|
||||
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
const previous = { deployKeepDays: 45, restartConcurrency: 3, createConcurrency: 2, destroyConcurrency: 4 };
|
||||
db().prepare("UPDATE settings SET value = ? WHERE key = 'fleet_settings'").run(JSON.stringify(previous));
|
||||
const previous = {
|
||||
deployKeepDays: 45,
|
||||
restartConcurrency: 3,
|
||||
createConcurrency: 2,
|
||||
destroyConcurrency: 4
|
||||
};
|
||||
db()
|
||||
.prepare("UPDATE settings SET value = ? WHERE key = 'fleet_settings'")
|
||||
.run(JSON.stringify(previous));
|
||||
db().prepare("INSERT INTO settings(key,value) VALUES('unrelated','keep')").run();
|
||||
closeDb();
|
||||
vi.stubEnv('DEPLOY_KEEP_DAYS', 'bad');
|
||||
const { GET } = await import('../../src/routes/api/settings/+server.js');
|
||||
const upgraded = { ...defaults, ...previous, observationKeepDays: 1 };
|
||||
expect(await GET().json()).toEqual(upgraded);
|
||||
expect(JSON.parse((db().prepare("SELECT value FROM settings WHERE key = 'fleet_settings'").get() as { value: string }).value)).toEqual(upgraded);
|
||||
expect(db().prepare("SELECT value FROM settings WHERE key='unrelated'").get()).toEqual({ value: 'keep' });
|
||||
expect(
|
||||
JSON.parse(
|
||||
(
|
||||
db()
|
||||
.prepare("SELECT value FROM settings WHERE key = 'fleet_settings'")
|
||||
.get() as { value: string }
|
||||
).value
|
||||
)
|
||||
).toEqual(upgraded);
|
||||
expect(db().prepare("SELECT value FROM settings WHERE key='unrelated'").get()).toEqual({
|
||||
value: 'keep'
|
||||
});
|
||||
closeDb();
|
||||
expect(await GET().json()).toEqual(upgraded);
|
||||
});
|
||||
|
||||
it.each([0, 36500])('persists routine observation retention of %s days across database reopen', async observationKeepDays => {
|
||||
const { GET, PUT } = await import('../../src/routes/api/settings/+server.js');
|
||||
const saved = { ...defaults, observationKeepDays };
|
||||
const response = await PUT({ request: new Request('http://localhost/api/settings', { method: 'PUT', body: JSON.stringify(saved) }) });
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toEqual(saved);
|
||||
const { closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
closeDb();
|
||||
expect(await GET().json()).toEqual(saved);
|
||||
});
|
||||
it.each([0, 36500])(
|
||||
'persists routine observation retention of %s days across database reopen',
|
||||
async (observationKeepDays) => {
|
||||
const { GET, PUT } = await import('../../src/routes/api/settings/+server.js');
|
||||
const saved = { ...defaults, observationKeepDays };
|
||||
const response = await PUT({
|
||||
request: new Request('http://localhost/api/settings', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(saved)
|
||||
})
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toEqual(saved);
|
||||
const { closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
closeDb();
|
||||
expect(await GET().json()).toEqual(saved);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
'null', '[]', '{}', '{',
|
||||
...['restartConcurrency', 'createConcurrency', 'destroyConcurrency'].flatMap(key =>
|
||||
[0, -1, 1.5, '2', true, null, 9007199254740992].map(value => JSON.stringify({ ...defaults, [key]: value }))),
|
||||
...['deployKeepDays','observationKeepDays','failureKeepDays','matchKeepDays','gameFileGraceDays'].flatMap(key => [-1, 1.5, '2', true, null, 36501, 9007199254740992].map(value => JSON.stringify({ ...defaults, [key]: value }))),
|
||||
'null',
|
||||
'[]',
|
||||
'{}',
|
||||
'{',
|
||||
...['restartConcurrency', 'createConcurrency', 'destroyConcurrency'].flatMap((key) =>
|
||||
[0, -1, 1.5, '2', true, null, 9007199254740992].map((value) =>
|
||||
JSON.stringify({ ...defaults, [key]: value })
|
||||
)
|
||||
),
|
||||
...[
|
||||
'deployKeepDays',
|
||||
'observationKeepDays',
|
||||
'failureKeepDays',
|
||||
'matchKeepDays',
|
||||
'gameFileGraceDays'
|
||||
].flatMap((key) =>
|
||||
[-1, 1.5, '2', true, null, 36501, 9007199254740992].map((value) =>
|
||||
JSON.stringify({ ...defaults, [key]: value })
|
||||
)
|
||||
),
|
||||
JSON.stringify({ deployKeepDays: 7, restartConcurrency: 1, createConcurrency: 1 }),
|
||||
JSON.stringify({ deployKeepDays: 7, restartConcurrency: 1, createConcurrency: 1, destroyConcurrency: 1 }),
|
||||
JSON.stringify({
|
||||
deployKeepDays: 7,
|
||||
restartConcurrency: 1,
|
||||
createConcurrency: 1,
|
||||
destroyConcurrency: 1
|
||||
}),
|
||||
JSON.stringify({ ...defaults, deployKeepDays: 36501 }),
|
||||
JSON.stringify({ ...defaults, unknown: 1 })
|
||||
])('rejects invalid input without partially saving: %s', async body => {
|
||||
])('rejects invalid input without partially saving: %s', async (body) => {
|
||||
const { GET, PUT } = await import('../../src/routes/api/settings/+server.js');
|
||||
const response = await PUT({ request: new Request('http://localhost/api/settings', { method: 'PUT', body }) });
|
||||
const response = await PUT({
|
||||
request: new Request('http://localhost/api/settings', { method: 'PUT', body })
|
||||
});
|
||||
expect(response.status).toBe(400);
|
||||
expect(await response.json()).toHaveProperty('error');
|
||||
expect(await GET().json()).toEqual(defaults);
|
||||
@@ -112,11 +216,20 @@ it('applies saved retention to cleanup while protecting unfinished campaigns and
|
||||
const { sweep } = await import('../../src/lib/server/pipeline/sweep.js');
|
||||
const old = new Date(Date.now() - 10 * 86400000).toISOString();
|
||||
for (const id of ['completed', 'pending', 'failed', 'held']) {
|
||||
db().prepare("INSERT INTO deployments (id, action, started_at) VALUES (?, 'test', ?)").run(id, old);
|
||||
db().prepare('INSERT INTO deployment_servers (deployment_id, server, restart_state, files_changed) VALUES (?, ?, ?, 0)')
|
||||
db()
|
||||
.prepare("INSERT INTO deployments (id, action, started_at) VALUES (?, 'test', ?)")
|
||||
.run(id, old);
|
||||
db()
|
||||
.prepare(
|
||||
'INSERT INTO deployment_servers (deployment_id, server, restart_state, files_changed) VALUES (?, ?, ?, 0)'
|
||||
)
|
||||
.run(id, 'server-' + id, id === 'completed' ? 'done' : id);
|
||||
}
|
||||
db().prepare("INSERT INTO server_deployments (id, server, action, state, stage, started_at, triggered_by) VALUES ('history', 'removed', 'destroy', 'done', 'Complete', ?, 'ui')").run(old);
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO server_deployments (id, server, action, state, stage, started_at, triggered_by) VALUES ('history', 'removed', 'destroy', 'done', 'Complete', ?, 'ui')"
|
||||
)
|
||||
.run(old);
|
||||
saveFleetSettings({ ...fleetSettings(), deployKeepDays: 30 });
|
||||
await sweep();
|
||||
expect(db().prepare('SELECT id FROM deployments').all()).toHaveLength(4);
|
||||
@@ -124,8 +237,16 @@ it('applies saved retention to cleanup while protecting unfinished campaigns and
|
||||
await sweep();
|
||||
const { pruneHistory } = await import('../../src/lib/server/retention.js');
|
||||
pruneHistory();
|
||||
expect(db().prepare('SELECT id FROM deployments ORDER BY id').all()).toEqual([{ id: 'failed' }, { id: 'held' }, { id: 'pending' }]);
|
||||
expect(db().prepare("SELECT server FROM deployment_servers WHERE deployment_id = 'completed'").all()).toEqual([]);
|
||||
expect(db().prepare('SELECT id FROM deployments ORDER BY id').all()).toEqual([
|
||||
{ id: 'failed' },
|
||||
{ id: 'held' },
|
||||
{ id: 'pending' }
|
||||
]);
|
||||
expect(
|
||||
db()
|
||||
.prepare("SELECT server FROM deployment_servers WHERE deployment_id = 'completed'")
|
||||
.all()
|
||||
).toEqual([]);
|
||||
expect(db().prepare('SELECT id FROM server_deployments').all()).toEqual([{ id: 'history' }]);
|
||||
});
|
||||
|
||||
@@ -134,30 +255,51 @@ it('saves a section without overwriting changes in the other section', async ()
|
||||
const { concurrencyKeys } = await import('../../src/lib/fleet-settings.js');
|
||||
const { saveFleetSettings } = await import('../../src/lib/server/settings.js');
|
||||
saveFleetSettings({ ...defaults, deployKeepDays: 42 });
|
||||
const response = await PATCH({ request: new Request('http://localhost/api/settings', { method: 'PATCH', body: JSON.stringify({
|
||||
section: 'Concurrency', values: Object.fromEntries(concurrencyKeys.map(key => [key, key === 'rebuildConcurrency' ? 2 : defaults[key]]))
|
||||
}) }) });
|
||||
const response = await PATCH({
|
||||
request: new Request('http://localhost/api/settings', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
section: 'Concurrency',
|
||||
values: Object.fromEntries(
|
||||
concurrencyKeys.map((key) => [
|
||||
key,
|
||||
key === 'rebuildConcurrency' ? 2 : defaults[key]
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(await GET().json()).toMatchObject({ deployKeepDays: 42, rebuildConcurrency: 2 });
|
||||
});
|
||||
|
||||
describe('settings presentation semantics', () => {
|
||||
it('places every saved setting in exactly one searchable group', () => {
|
||||
const grouped = settingsGroups.flatMap(group => group.keys);
|
||||
const grouped = settingsGroups.flatMap((group) => group.keys);
|
||||
expect(grouped.length).toBe(Object.keys(defaults).length);
|
||||
expect(new Set(grouped).size).toBe(grouped.length);
|
||||
expect([...grouped].sort()).toEqual(Object.keys(defaults).sort());
|
||||
expect(settingsGroups.map(group => group.title)).toEqual([
|
||||
'Concurrency', 'Operation history', 'Matches & players', 'Telemetry & metrics', 'Files & versions', 'Accounts & audit'
|
||||
expect(settingsGroups.map((group) => group.title)).toEqual([
|
||||
'Concurrency',
|
||||
'Operation history',
|
||||
'Matches & players',
|
||||
'Telemetry & metrics',
|
||||
'Files & versions',
|
||||
'Accounts & audit'
|
||||
]);
|
||||
});
|
||||
|
||||
it('distinguishes an unlimited cap from permanent retention, age expiry, and minimum counts', () => {
|
||||
for (const key of [...additionalConcurrencyKeys, 'telemetryMaxPerServer', 'telemetryMaxTotal'] as (keyof FleetSettings)[]) {
|
||||
for (const key of [
|
||||
...additionalConcurrencyKeys,
|
||||
'telemetryMaxPerServer',
|
||||
'telemetryMaxTotal'
|
||||
] as (keyof FleetSettings)[]) {
|
||||
expect(settingZeroMode(key)).toBe('No limit');
|
||||
}
|
||||
for (const key of Object.keys(defaults) as (keyof FleetSettings)[]) {
|
||||
if (key.endsWith('Days') && !['versionKeepDays', 'gameFileGraceDays'].includes(key)) expect(settingZeroMode(key)).toBe('Forever');
|
||||
if (key.endsWith('Days') && !['versionKeepDays', 'gameFileGraceDays'].includes(key))
|
||||
expect(settingZeroMode(key)).toBe('Forever');
|
||||
if (settingMinimum(key) === 1) expect(settingZeroMode(key)).toBeNull();
|
||||
}
|
||||
expect(settingZeroMode('versionKeepDays')).toBe('No age expiry');
|
||||
@@ -165,7 +307,9 @@ describe('settings presentation semantics', () => {
|
||||
expect(settingZeroMode('versionKeepCount')).toBe('No minimum');
|
||||
expect(settingZeroMode('historyKeepCount')).toBe('No minimum');
|
||||
expect(settingHelp.versionKeepDays).toContain('version count policy may still remove');
|
||||
expect(settingHelp.versionKeepCount).toContain('a positive count removes older unused versions');
|
||||
expect(settingHelp.versionKeepCount).toContain(
|
||||
'a positive count removes older unused versions'
|
||||
);
|
||||
});
|
||||
|
||||
it('formats numeric values and defaults without changing their stored meaning', () => {
|
||||
@@ -186,6 +330,8 @@ describe('settings presentation semantics', () => {
|
||||
expect(settingDisplayValue('matchKeepDays', NaN)).toBe('—');
|
||||
expect(defaults).toEqual(original);
|
||||
expect(fleetSettingLabels.destroyConcurrency).toBe('Concurrent server removals');
|
||||
expect(fleetSettingLabels.destroyConcurrencyPerHost).toBe('Concurrent server removals per host');
|
||||
expect(fleetSettingLabels.destroyConcurrencyPerHost).toBe(
|
||||
'Concurrent server removals per host'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user