Make metrics reflect autoscaling policies (Refs #137)
CI / checks (push) Successful in 4m49s
CI / checks (push) Successful in 4m49s
This commit is contained in:
@@ -24,8 +24,12 @@ const map = (i: number, changes: Partial<MapMetrics> = {}): MapMetrics => ({
|
||||
idle: 5,
|
||||
unknown: 0,
|
||||
playerHours: i,
|
||||
full: 1,
|
||||
capacityObserved: 4,
|
||||
noEmpty: 1,
|
||||
availabilityObserved: 4,
|
||||
availabilityUnknown: 0,
|
||||
noEmptyAtMaximum: 0,
|
||||
noEmptyBelowMaximum: 0,
|
||||
policy: null,
|
||||
...changes
|
||||
});
|
||||
const server = (i: number): ServerMetrics => ({
|
||||
@@ -52,23 +56,33 @@ function report(): MetricsData {
|
||||
scopeFirstSampleAt: now - 14 * 86400000,
|
||||
scopeLatestSampleAt: now,
|
||||
summary,
|
||||
running: { average: 12, peak: 24, capturedHours: 1 },
|
||||
maps: Array.from({ length: 24 }, (_, i) => map(i + 1)),
|
||||
servers: Array.from({ length: 24 }, (_, i) => server(i + 1)),
|
||||
points: [
|
||||
{ ...summary, at: now - 86400000, startAt: now - 86400000, endAt: now, partial: true }
|
||||
{
|
||||
...summary,
|
||||
at: now - 86400000,
|
||||
startAt: now - 86400000,
|
||||
endAt: now,
|
||||
partial: true,
|
||||
capturedHours: 1
|
||||
}
|
||||
],
|
||||
previous: {
|
||||
since: now - 14 * 86400000,
|
||||
until: now - 7 * 86400000,
|
||||
summary: { ...summary, active: 4, idle: 6, playerHours: 10 },
|
||||
running: { average: 12, peak: 24, capturedHours: 1 },
|
||||
firstSampleAt: now - 14 * 86400000,
|
||||
lastSampleAt: now - 7 * 86400000,
|
||||
partialHistory: true
|
||||
},
|
||||
pooledPressure: {
|
||||
pooledAvailability: {
|
||||
value: 25,
|
||||
fullMapHours: 1,
|
||||
noEmptyMapHours: 1,
|
||||
knownMapHours: 4,
|
||||
unknownMapHours: 0,
|
||||
coveredMaps: 24,
|
||||
totalMaps: 24
|
||||
},
|
||||
@@ -95,6 +109,9 @@ it('shows a summary and one paginated breakdown, preserving scope on range links
|
||||
expect(body).toContain('game=t5&days=30&view=maps');
|
||||
expect(body).toContain('Partial previous history');
|
||||
expect(body).toContain('About this report');
|
||||
expect(body).toContain('Average lobbies running:');
|
||||
expect(body.replace(/\s+/g, ' ')).toContain('Captured wall time:');
|
||||
expect(body).not.toContain('Percentage points show');
|
||||
expect(body).not.toContain('Explore this period');
|
||||
expect(body).toContain('Updates every minute');
|
||||
expect(body).not.toContain('>Refresh<');
|
||||
@@ -107,28 +124,44 @@ it('shows a summary and one paginated breakdown, preserving scope on range links
|
||||
expect(body).not.toContain('class="weekly');
|
||||
});
|
||||
|
||||
it('keeps percentage points, relative changes and unavailable capacity distinct', () => {
|
||||
it('shows concurrent lobbies and empty spare share with unknown lobby-hours in the denominator', () => {
|
||||
const data = report();
|
||||
let body = render(MetricsSummary, { props: { report: data } }).body;
|
||||
expect(body).toContain('+10 percentage points');
|
||||
expect(body).toContain('12 avg');
|
||||
expect(body).toContain('Peak 24 lobbies at once');
|
||||
expect(body.replace(/\s+/g, ' ')).toContain(
|
||||
'5 h of 12 h captured lobby-hours were empty spares (41.7%)'
|
||||
);
|
||||
expect(body).toContain('+100%');
|
||||
data.pooledPressure = {
|
||||
expect(body).toContain('Time with no empty lobby');
|
||||
expect(body).toContain('25.0%');
|
||||
expect(body).not.toContain('capacity data');
|
||||
data.pooledAvailability = {
|
||||
value: null,
|
||||
fullMapHours: 0,
|
||||
noEmptyMapHours: 0,
|
||||
knownMapHours: 0,
|
||||
unknownMapHours: 4,
|
||||
coveredMaps: 0,
|
||||
totalMaps: 24
|
||||
};
|
||||
data.maps = [map(1, { capacityObserved: 0 })];
|
||||
data.maps = [map(1, { availabilityObserved: 0, availabilityUnknown: 4 })];
|
||||
data.previous.summary.playerHours = 0;
|
||||
body = render(MetricsSummary, { props: { report: data } }).body;
|
||||
expect(body).toContain('Capacity data unavailable');
|
||||
expect(body).toContain('Lobby availability unavailable');
|
||||
expect(body).toContain('New · no previous player-hours');
|
||||
expect(body.replace(/\s+/g, ' ')).toContain('1 of 1 maps lack capacity data');
|
||||
expect(body).not.toContain('Infinity');
|
||||
data.summary.observed = 0;
|
||||
data.summary = { observed: 0, active: 0, idle: 0, playerHours: 0, unknown: 12 };
|
||||
body = render(MetricsSummary, { props: { report: data } }).body;
|
||||
expect(body).toContain('Comparison unavailable');
|
||||
expect(body).toContain('12 avg');
|
||||
expect(body.replace(/\s+/g, ' ')).toContain(
|
||||
'0 h of 12 h captured lobby-hours were empty spares (0.0%)'
|
||||
);
|
||||
data.running = { average: null, peak: 0, capturedHours: 0 };
|
||||
data.summary.unknown = 0;
|
||||
body = render(MetricsSummary, { props: { report: data } }).body;
|
||||
expect(body).toContain('Peak unavailable');
|
||||
expect(body).not.toContain('Peak 0 lobbies');
|
||||
});
|
||||
|
||||
it('searches and sorts the complete map set before pagination and clamps invalid pages', () => {
|
||||
@@ -147,14 +180,20 @@ it('searches and sorts the complete map set before pagination and clamps invalid
|
||||
expect(metricPage([], '20')).toMatchObject({ page: 1, start: 0, end: 0, total: 0 });
|
||||
});
|
||||
|
||||
it('keeps missing capacity last when sorting and never presents it as zero full time', () => {
|
||||
scope('?mapSort=full&mapDirection=asc');
|
||||
const maps = [map(1, { capacityObserved: 0 }), map(2, { full: 0 }), map(3)];
|
||||
it('keeps unknown availability last when sorting and distinguishes it from known empty lobbies', () => {
|
||||
scope('?mapSort=noEmpty&mapDirection=asc');
|
||||
const maps = [
|
||||
map(1, { availabilityObserved: 0, availabilityUnknown: 4 }),
|
||||
map(2, { noEmpty: 0 }),
|
||||
map(3)
|
||||
];
|
||||
const body = render(MetricsTables, { props: { maps, servers: [] } }).body;
|
||||
expect(body.indexOf('Map 02')).toBeLessThan(body.indexOf('Map 03'));
|
||||
expect(body.indexOf('Map 03')).toBeLessThan(body.indexOf('Map 01'));
|
||||
expect(body).toContain('0.0%');
|
||||
expect(body).toContain('Capacity data unavailable');
|
||||
expect(body).toContain('Lobby availability unavailable');
|
||||
expect(body).toContain('No empty lobby');
|
||||
expect(body).not.toContain('All lobbies full');
|
||||
});
|
||||
|
||||
it('preserves instance return context and distinguishes historic names without current links', () => {
|
||||
@@ -170,18 +209,18 @@ it('preserves instance return context and distinguishes historic names without c
|
||||
expect(body).not.toContain('Open current server');
|
||||
});
|
||||
|
||||
it('highlights at most three supported trials and collapses tentative findings', () => {
|
||||
it('highlights at most three supported policy changes and collapses tentative findings', () => {
|
||||
const data = report();
|
||||
data.maps.push(map(25, { observed: 0.1, active: 0.1, idle: 0, capacityObserved: 0 }));
|
||||
data.maps.push(map(25, { observed: 0.1, active: 0.1, idle: 0, availabilityObserved: 0 }));
|
||||
const body = render(MetricsExplore, { props: { report: data } }).body;
|
||||
const fold = body.indexOf('Other findings');
|
||||
expect(body.slice(0, fold).match(/class="recommendation /g)).toHaveLength(3);
|
||||
expect(body.indexOf('Map 25')).toBeGreaterThan(fold);
|
||||
expect(body).toContain('Limited evidence');
|
||||
expect(body).not.toMatch(/<details[^>]*open/);
|
||||
data.maps = [map(1, { capacityObserved: 0 })];
|
||||
data.maps = [map(1, { availabilityObserved: 0 })];
|
||||
expect(render(MetricsExplore, { props: { report: data } }).body).toContain(
|
||||
'No hosting trials meet the evidence threshold'
|
||||
'No policy changes meet the evidence threshold'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -196,20 +235,65 @@ it('keeps accessible chart selection, partial periods and gaps while collapsing
|
||||
active: 0,
|
||||
idle: 0,
|
||||
unknown: 0,
|
||||
playerHours: 0
|
||||
playerHours: 0,
|
||||
capturedHours: 0
|
||||
});
|
||||
const body = render(MetricsChart, {
|
||||
props: { points: data.points, days: data.days, timeZone: data.scope.timeZone }
|
||||
}).body;
|
||||
expect(body).toContain('aria-label="Selected period summary"');
|
||||
expect(body).toContain('No captured time · not evidence of idle servers');
|
||||
expect(body).toContain('No captured time · not evidence of empty lobbies');
|
||||
expect(body).toContain('tabindex="0"');
|
||||
expect(body).toContain('Inspect a period');
|
||||
expect(body).toContain('Partial period');
|
||||
expect(body).not.toMatch(/<details[^>]*open/);
|
||||
expect(body).toContain('Weekdays show when each period starts in America/New York.');
|
||||
expect(body).toContain('Daily periods are 24-hour windows.');
|
||||
expect(body).toContain('Server activity over time');
|
||||
expect(body).toContain('Lobbies running over time');
|
||||
expect(body).toContain('Average lobbies running');
|
||||
expect(body).toContain('5 with players · 5 empty spares · 2 unknown on average');
|
||||
});
|
||||
|
||||
it('keeps average lobby bar heights comparable across partial periods and collection gaps', () => {
|
||||
const data = report();
|
||||
const point = data.points[0];
|
||||
data.points = [
|
||||
{
|
||||
...point,
|
||||
observed: 48,
|
||||
active: 12,
|
||||
idle: 36,
|
||||
unknown: 24,
|
||||
capturedHours: 12,
|
||||
partial: false
|
||||
},
|
||||
{
|
||||
...point,
|
||||
at: data.now,
|
||||
startAt: data.now,
|
||||
endAt: data.now + 2 * 3600000,
|
||||
observed: 8,
|
||||
active: 2,
|
||||
idle: 6,
|
||||
unknown: 4,
|
||||
capturedHours: 2,
|
||||
partial: true
|
||||
}
|
||||
];
|
||||
const body = render(MetricsChart, {
|
||||
props: { points: data.points, days: data.days, timeZone: data.scope.timeZone }
|
||||
}).body;
|
||||
const bars = body.match(/<button class="bar\b[\s\S]*?<\/button>/g) ?? [];
|
||||
expect(bars).toHaveLength(2);
|
||||
const heights = bars.map((bar) =>
|
||||
[...bar.matchAll(/style="height:\s*([^"]+)"/g)].map((match) => match[1])
|
||||
);
|
||||
expect(heights[0]).toHaveLength(3);
|
||||
expect(heights[0]).toEqual(heights[1]);
|
||||
expect(heights[0]).toContain('50%;');
|
||||
expect(
|
||||
bars.every((bar) => bar.includes('1 with players · 3 empty spares · 2 unknown on average'))
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('uses shallow URL state for the selected breakdown while retaining the common summary', () => {
|
||||
@@ -225,15 +309,46 @@ it('uses shallow URL state for the selected breakdown while retaining the common
|
||||
props: { data: { ...report(), user: null, setupRequired: false } }
|
||||
}).body;
|
||||
expect(body).toContain('id="metrics-explore-title"');
|
||||
expect(body).toContain('Time all lobbies were full');
|
||||
expect(body).toContain('Time with no empty lobby');
|
||||
expect(body).toContain('Autoscaling signals');
|
||||
expect(body).not.toContain('aria-label="Map metrics table"');
|
||||
expect(body).not.toContain('aria-label="Instance metrics table"');
|
||||
});
|
||||
|
||||
it('filters other insights without hiding the highlighted supported trials', () => {
|
||||
it('filters other signals without hiding the highlighted supported policy changes', () => {
|
||||
scope('?view=insights&insightQ=Map+24&insightSort=map');
|
||||
const body = render(MetricsExplore, { props: { report: report() } }).body;
|
||||
expect(body).toContain('Map 01');
|
||||
expect(body).toContain('Map 24');
|
||||
expect(body).not.toContain('Map 23');
|
||||
});
|
||||
|
||||
it('links autoscaling signals to current policies and explains their historical comparison', () => {
|
||||
const data = report();
|
||||
data.maps = [
|
||||
map(1, {
|
||||
noEmptyAtMaximum: 0.75,
|
||||
noEmptyBelowMaximum: 0.25,
|
||||
policy: {
|
||||
id: 'policy-1',
|
||||
mode: 'create-destroy',
|
||||
minimum: 1,
|
||||
maximum: 3,
|
||||
spares: 1,
|
||||
pausedReason: null
|
||||
}
|
||||
})
|
||||
];
|
||||
let body = render(MetricsExplore, { props: { report: data } }).body;
|
||||
expect(body).toContain('href="/autoscaling#policy-1"');
|
||||
expect(body).toContain('Review policy');
|
||||
expect(body).toContain('Current policy: 1–3 lobbies, 1 spare');
|
||||
expect(body.replace(/\s+/g, ' ')).toContain(
|
||||
'Current settings are applied to historical lobby counts'
|
||||
);
|
||||
expect(body).not.toContain('player limits, and observation');
|
||||
data.maps[0].policy = null;
|
||||
body = render(MetricsExplore, { props: { report: data } }).body;
|
||||
expect(body).toContain('href="/autoscaling"');
|
||||
expect(body).toContain('Add policy');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user