Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -1,37 +1,138 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { render } from 'svelte/server';
|
||||
import PolicyCard from '../../src/lib/ui/components/autoscaling/PolicyCard.svelte';
|
||||
import { lobbyGroups, lobbyLabel, policyStatus, scalingActivity, observationAge } from '../../src/lib/ui/autoscaling.js';
|
||||
import {
|
||||
lobbyGroups,
|
||||
lobbyLabel,
|
||||
policyStatus,
|
||||
scalingActivity,
|
||||
observationAge
|
||||
} from '../../src/lib/ui/autoscaling.js';
|
||||
import { zombieMaps } from '../../src/lib/map-aliases.js';
|
||||
import type { ScalingHistory, ScalingView } from '../../src/lib/autoscaling.js';
|
||||
|
||||
const now = Date.parse('2026-10-07T20:00:00Z');
|
||||
function view(): ScalingView {
|
||||
return {
|
||||
policy: { id: 'policy', game: 't5', mapSlug: '*', mode: 'create-destroy', minimum: 1, maximum: 3, spares: 1, emptyMinutes: 15, revision: 1, pausedReason: null, updatedAt: '' },
|
||||
snapshot: { members: [
|
||||
{ name: 't5_zombie_theater_1', mapSlug: 'theater', identity: 'one', state: 'empty', players: 0, observedAt: now, source: 'rcon', emptySince: now, blocker: null, operationId: null },
|
||||
{ name: 't5_zombie_cosmodrome_1', mapSlug: 'cosmodrome', identity: 'two', state: 'creating', players: null, observedAt: null, source: null, emptySince: null, blocker: 'Provisioning job-1: Checking container readiness', operationId: 'job-1' }
|
||||
], reservedNames: [], keyPool: { available: 13, total: 15 }, blockers: [] },
|
||||
decision: { action: 'wait', reason: 'Provisioning job-1: Checking container readiness', server: null, occupied: 0, empty: 1, pending: 1, unknown: 0, deficit: 9, desired: 10 },
|
||||
unsettled: [{ id: 'job-1', action: 'create', state: 'awaiting-health', stage: 'Checking container readiness' }],
|
||||
blockers: [], ownsMap: true, history: [], lastEvaluatedAt: now,
|
||||
policy: {
|
||||
id: 'policy',
|
||||
game: 't5',
|
||||
mapSlug: '*',
|
||||
mode: 'create-destroy',
|
||||
minimum: 1,
|
||||
maximum: 3,
|
||||
spares: 1,
|
||||
emptyMinutes: 15,
|
||||
revision: 1,
|
||||
pausedReason: null,
|
||||
updatedAt: ''
|
||||
},
|
||||
snapshot: {
|
||||
members: [
|
||||
{
|
||||
name: 't5_zombie_theater_1',
|
||||
mapSlug: 'theater',
|
||||
identity: 'one',
|
||||
state: 'empty',
|
||||
players: 0,
|
||||
observedAt: now,
|
||||
source: 'rcon',
|
||||
emptySince: now,
|
||||
blocker: null,
|
||||
operationId: null
|
||||
},
|
||||
{
|
||||
name: 't5_zombie_cosmodrome_1',
|
||||
mapSlug: 'cosmodrome',
|
||||
identity: 'two',
|
||||
state: 'creating',
|
||||
players: null,
|
||||
observedAt: null,
|
||||
source: null,
|
||||
emptySince: null,
|
||||
blocker: 'Provisioning job-1: Checking container readiness',
|
||||
operationId: 'job-1'
|
||||
}
|
||||
],
|
||||
reservedNames: [],
|
||||
keyPool: { available: 13, total: 15 },
|
||||
blockers: []
|
||||
},
|
||||
decision: {
|
||||
action: 'wait',
|
||||
reason: 'Provisioning job-1: Checking container readiness',
|
||||
server: null,
|
||||
occupied: 0,
|
||||
empty: 1,
|
||||
pending: 1,
|
||||
unknown: 0,
|
||||
deficit: 9,
|
||||
desired: 10
|
||||
},
|
||||
unsettled: [
|
||||
{
|
||||
id: 'job-1',
|
||||
action: 'create',
|
||||
state: 'awaiting-health',
|
||||
stage: 'Checking container readiness'
|
||||
}
|
||||
],
|
||||
blockers: [],
|
||||
ownsMap: true,
|
||||
history: [],
|
||||
lastEvaluatedAt: now,
|
||||
totals: { observedMinutes: 12, emptyLobbyHours: 0.2, deficitMinutes: 5, unknownMinutes: 0 },
|
||||
operations: { creates: 1, removals: 0, cancelled: 0, failed: 0, readinessSamples: 0, readinessMeanSeconds: null, readinessMaxSeconds: null }
|
||||
operations: {
|
||||
creates: 1,
|
||||
removals: 0,
|
||||
cancelled: 0,
|
||||
failed: 0,
|
||||
readinessSamples: 0,
|
||||
readinessMeanSeconds: null,
|
||||
readinessMaxSeconds: null
|
||||
}
|
||||
};
|
||||
}
|
||||
function body(data = view(), canOperate = true, globallyPaused = false) {
|
||||
return render(PolicyCard, { props: { view: data, canOperate, globallyPaused, busy: false, onedit: () => {}, ontoggle: () => {}, ondelete: () => {} } }).body;
|
||||
return render(PolicyCard, {
|
||||
props: {
|
||||
view: data,
|
||||
canOperate,
|
||||
globallyPaused,
|
||||
busy: false,
|
||||
onedit: () => {},
|
||||
ontoggle: () => {},
|
||||
ondelete: () => {}
|
||||
}
|
||||
}).body;
|
||||
}
|
||||
function history(id: string, at: number, action: ScalingHistory['decision']['action'], reason: string, operationId: string | null = null): ScalingHistory {
|
||||
return { id, at, lastSeenAt: at, decision: { ...view().decision, action, reason, server: action === 'create' ? 't5_zombie_cosmodrome_1' : null }, operationId, operationState: operationId ? 'done' : null };
|
||||
function history(
|
||||
id: string,
|
||||
at: number,
|
||||
action: ScalingHistory['decision']['action'],
|
||||
reason: string,
|
||||
operationId: string | null = null
|
||||
): ScalingHistory {
|
||||
return {
|
||||
id,
|
||||
at,
|
||||
lastSeenAt: at,
|
||||
decision: {
|
||||
...view().decision,
|
||||
action,
|
||||
reason,
|
||||
server: action === 'create' ? 't5_zombie_cosmodrome_1' : null
|
||||
},
|
||||
operationId,
|
||||
operationState: operationId ? 'done' : null
|
||||
};
|
||||
}
|
||||
|
||||
describe('autoscaling presentation', () => {
|
||||
it('shows a provisioning stage once and keeps diagnostic job links without routine deletion warnings', () => {
|
||||
const html = body();
|
||||
expect(html.split('Checking container readiness')).toHaveLength(2);
|
||||
expect(html).toContain('Creating Ascension · Lobby 1');
|
||||
expect(html.replace(/\s+/g, ' ')).toContain('Creating Ascension · Lobby 1');
|
||||
expect(html).toContain('href="/provisioning/job-1"');
|
||||
expect(html).not.toContain('Work is still settling');
|
||||
expect(html).not.toContain('Provisioning job-1:');
|
||||
@@ -41,11 +142,24 @@ describe('autoscaling presentation', () => {
|
||||
});
|
||||
it('includes uncovered maps and preserves lobbies with unrecognized identities', () => {
|
||||
const data = view();
|
||||
data.snapshot.members.push({ ...data.snapshot.members[0], name: 'legacy-server', mapSlug: undefined });
|
||||
data.snapshot.members.push({
|
||||
...data.snapshot.members[0],
|
||||
name: 'legacy-server',
|
||||
mapSlug: undefined
|
||||
});
|
||||
const groups = lobbyGroups(data);
|
||||
expect(groups.find(group => group.slug === 'theater')).toMatchObject({ ready: 1, target: 1 });
|
||||
expect(groups.find(group => group.slug === 'moon')).toMatchObject({ ready: 0, target: 1, members: [] });
|
||||
expect(groups.find(group => group.slug === 'unassigned')?.members[0].name).toBe('legacy-server');
|
||||
expect(groups.find((group) => group.slug === 'theater')).toMatchObject({
|
||||
ready: 1,
|
||||
target: 1
|
||||
});
|
||||
expect(groups.find((group) => group.slug === 'moon')).toMatchObject({
|
||||
ready: 0,
|
||||
target: 1,
|
||||
members: []
|
||||
});
|
||||
expect(groups.find((group) => group.slug === 'unassigned')?.members[0].name).toBe(
|
||||
'legacy-server'
|
||||
);
|
||||
expect(body()).toContain('1 / 10 maps meet spare target');
|
||||
});
|
||||
it('does not imply global readiness from one satisfied map', () => {
|
||||
@@ -61,11 +175,13 @@ describe('autoscaling presentation', () => {
|
||||
data.unsettled[0].state = 'failed';
|
||||
const html = body(data);
|
||||
expect(html).toContain(data.policy.pausedReason);
|
||||
expect(html).toContain('Review Ascension · Lobby 1 job');
|
||||
expect(html.replace(/\s+/g, ' ')).toContain('Review Ascension · Lobby 1 job');
|
||||
expect(body(view(), true, true)).toContain('Globally paused');
|
||||
data.policy.pausedReason = null;
|
||||
data.policy.mode = 'off';
|
||||
expect(body(data)).toContain('No new scaling requests. Outstanding jobs are still tracked.');
|
||||
expect(body(data)).toContain(
|
||||
'No new scaling requests. Outstanding jobs are still tracked.'
|
||||
);
|
||||
});
|
||||
it('groups job wait decisions with their original request without swallowing unrelated blockers', () => {
|
||||
const data = view();
|
||||
@@ -77,14 +193,31 @@ describe('autoscaling presentation', () => {
|
||||
];
|
||||
const activity = scalingActivity(data);
|
||||
expect(activity).toHaveLength(2);
|
||||
expect(activity[0]).toMatchObject({ operationId: 'job-1', action: 'create', at: now, lastSeenAt: now + 30, evaluations: 3, operationState: 'done' });
|
||||
expect(activity[0]).toMatchObject({
|
||||
operationId: 'job-1',
|
||||
action: 'create',
|
||||
at: now,
|
||||
lastSeenAt: now + 30,
|
||||
evaluations: 3,
|
||||
operationState: 'done'
|
||||
});
|
||||
expect(activity[1].reason).toBe('No available game keys.');
|
||||
});
|
||||
it('consolidates a fully covered game into one healthy summary', () => {
|
||||
const data = view();
|
||||
data.unsettled = [];
|
||||
data.snapshot.members = zombieMaps('t5').map(map => ({ ...data.snapshot.members[0], name: `t5_zombie_${map.slug}_1`, mapSlug: map.slug }));
|
||||
data.decision = { ...data.decision, reason: 'Empty spare target is satisfied.', deficit: 0, empty: 10, pending: 0 };
|
||||
data.snapshot.members = zombieMaps('t5').map((map) => ({
|
||||
...data.snapshot.members[0],
|
||||
name: `t5_zombie_${map.slug}_1`,
|
||||
mapSlug: map.slug
|
||||
}));
|
||||
data.decision = {
|
||||
...data.decision,
|
||||
reason: 'Empty spare target is satisfied.',
|
||||
deficit: 0,
|
||||
empty: 10,
|
||||
pending: 0
|
||||
};
|
||||
const html = body(data);
|
||||
expect(html).toContain('All 10 maps meet their spare target.');
|
||||
expect(html).not.toContain('10 / 10 maps meet spare target');
|
||||
@@ -95,8 +228,14 @@ describe('autoscaling presentation', () => {
|
||||
expect(observationAge(now - 5000, now)).toEqual({ label: '5s ago', stale: false });
|
||||
expect(observationAge(now - 90000, now).stale).toBe(false);
|
||||
expect(observationAge(now - 90001, now)).toEqual({ label: 'Stale · 1m ago', stale: true });
|
||||
expect(observationAge(now, now + 3600000)).toEqual({ label: 'Stale · 1h ago', stale: true });
|
||||
expect(observationAge(now + 5000, now)).toEqual({ label: 'Check observation time', stale: true });
|
||||
expect(observationAge(now, now + 3600000)).toEqual({
|
||||
label: 'Stale · 1h ago',
|
||||
stale: true
|
||||
});
|
||||
expect(observationAge(now + 5000, now)).toEqual({
|
||||
label: 'Check observation time',
|
||||
stale: true
|
||||
});
|
||||
});
|
||||
it('renders accessible tabs and hides mutation controls from viewers', () => {
|
||||
const html = body(view(), false);
|
||||
|
||||
Reference in New Issue
Block a user