Simplify capacity summary and host comparison

This commit is contained in:
2026-10-06 17:30:12 -04:00
parent 5ad76e6590
commit f4094b7f5d
5 changed files with 213 additions and 69 deletions
+78
View File
@@ -0,0 +1,78 @@
import { expect, it, vi } from 'vitest';
import { render } from 'svelte/server';
import { writable } from 'svelte/store';
import type { CapacityData, CapacityHost, CapacityHistory } from '../../src/lib/capacity.js';
import CapacityPage from '../../src/routes/capacity/+page.svelte';
import CapacityHosts from '../../src/lib/ui/components/capacity/CapacityHosts.svelte';
import CapacityChart from '../../src/lib/ui/components/capacity/CapacityChart.svelte';
vi.mock('$app/state', () => ({ page: { url: new URL('http://localhost/capacity?host=ec-1&game=t5') } }));
vi.mock('$app/stores', () => ({ page: writable({ url: new URL('http://localhost/capacity') }) }));
vi.mock('$app/navigation', () => ({ beforeNavigate: vi.fn(), afterNavigate: vi.fn(), goto: vi.fn() }));
const allowance = { cpuCores: 0.5, memoryBytes: 1024 ** 3, source: 'host' as const, samples: 20, observedAt: '2026-10-06T12:00:00Z' };
const host = (changes: Partial<CapacityHost> = {}): CapacityHost => ({
id: 'ec-1', name: 'ec-1', connected: true, readiness: 'ready', updatedAt: '2026-10-06T12:00:00Z', eligible: true,
games: ['t5', 't6'], reasons: [], cpuCoresAvailable: 2, memoryBytesAvailable: 4 * 1024 ** 3,
portsAvailable: 1, totalPorts: 10, estimatedServers: 1, limits: { cpu: 4, memory: 4, ports: 1 }, limitingResource: 'Ports',
calibration: { running: allowance, startup: allowance }, ...changes
});
const history: CapacityHistory = { range: '24h', startAt: 1000, endAt: 5000, bucketMs: 1000, points: [
{ at: 1000, cpuCoresAvailable: 2, memoryBytesAvailable: 1024 ** 3, reportingHosts: 1, expectedHosts: 1 },
{ at: 2000, cpuCoresAvailable: null, memoryBytesAvailable: null, reportingHosts: 0, expectedHosts: 1 },
{ at: 3000, cpuCoresAvailable: 3, memoryBytesAvailable: 2 * 1024 ** 3, reportingHosts: 1, expectedHosts: 1 }
] };
const data = (): CapacityData => ({ query: { range: '24h', game: 't5', hostId: 'ec-1' }, history,
current: { updatedAt: '2026-10-06T12:00:00Z', hosts: [host()], hostOptions: [{ id: 'ec-1', name: 'ec-1' }], warnings: [], summary: {
estimatedServers: 1, cpuCoresAvailable: 2, memoryBytesAvailable: 4 * 1024 ** 3, portsAvailable: 1,
eligibleHosts: 1, totalHosts: 1, startupSlots: 3, startupLimit: 3, activeStartups: 0, queued: 0
} }
});
it('keeps current estimates and host comparison before history with scoped history controls', () => {
const body = render(CapacityPage, { props: { data: data() } }).body;
expect(body.indexOf('id="capacity-now-title"')).toBeLessThan(body.indexOf('id="host-capacity-title"'));
expect(body.indexOf('id="host-capacity-title"')).toBeLessThan(body.indexOf('id="capacity-history-title"'));
expect(body.indexOf('aria-label="History range"')).toBeGreaterThan(body.indexOf('id="capacity-history-title"'));
expect(body).toContain('range=7d&amp;game=t5&amp;host=ec-1');
expect(body).toContain('3 of 3 available');
expect(body).toContain('No queued instances');
expect(body).not.toContain('href="/provisioning"');
expect(body).not.toContain('Manage hosts');
});
it('keeps fleet-wide queue scope explicit and preserves unavailable estimates', () => {
const report = data(); report.current.summary.queued = 2; report.current.summary.estimatedServers = null;
const body = render(CapacityPage, { props: { data: report } }).body;
expect(body).toContain('Capacity estimate unavailable');
expect(body).toContain('href="/provisioning"');
expect(body).toContain('2 instances queued');
expect(body).toContain('Fleet-wide');
});
it('keeps ineligible hosts and their reasons visible while collapsing detailed assumptions', () => {
const body = render(CapacityHosts, { props: { returnTo: '/capacity', hosts: [host(), host({ id: 'missing-files', name: 'Missing files', eligible: false, estimatedServers: 0, limitingResource: null, reasons: ['Latest game files are not ready.'] }), host({ id: 'offline', name: 'Offline host', connected: false, eligible: false, estimatedServers: 0, cpuCoresAvailable: null, memoryBytesAvailable: null, limitingResource: null, reasons: ['Waiting for a fresh host report.'] })] } }).body;
expect(body).toContain('Additional instances</th>');
expect(body).toContain('Not eligible');
expect(body).toContain('Latest game files are not ready.');
expect(body).toContain('Offline');
expect(body).toContain('>—</td>');
expect(body).not.toContain('>Ready<');
expect(body).toMatch(/<tr hidden[^>]*id="capacity-host-ec-1"/);
expect(body).toContain('aria-expanded="false"');
expect(body).toContain('Running instance');
});
it('preserves chart gaps, keyboard inspection and reporting coverage in the compact chart', () => {
const body = render(CapacityChart, { props: { history, scope: 'All hosts' } }).body;
expect(body.match(/class="trend /g)).toHaveLength(2);
expect(body).toContain('role="slider"');
expect(body).toContain('1 of 1 hosts reporting');
expect(body).toContain('Reporting hosts');
expect(body).toContain('The game filter applies to current launch estimates only.');
expect(body).toContain('About this chart');
expect(body).not.toMatch(/<details[^>]*open/);
const empty = render(CapacityChart, { props: { history: { ...history, points: [history.points[1]] }, scope: 'All hosts' } }).body;
expect(empty).toContain('Waiting for capacity history');
expect(empty).toContain('No complete report');
});