Files
electric-cherry/tests/unit/ui-rebalancing.test.ts
T

112 lines
4.1 KiB
TypeScript

import { expect, it } from 'vitest';
import { render } from 'svelte/server';
import type {
RebalanceCandidate,
RebalancePreview,
RebalanceRun
} from '../../src/lib/rebalancing.js';
import RebalancePreviewView from '../../src/lib/ui/components/capacity/RebalancePreviewView.svelte';
import RebalanceRunView from '../../src/lib/ui/components/capacity/RebalanceRunView.svelte';
const at = '2026-10-08T12:00:00Z';
const move: RebalanceCandidate = {
server: 't5_zombie_theater_1',
instanceId: 'instance-a',
generation: 1,
sourceHostId: 'source-a',
sourceHostName: 'Source A',
targetHostId: 'target-b',
targetHostName: 'Target B',
improvement: 0.1
};
const preview: RebalancePreview = {
id: 'preview-a',
createdAt: at,
expiresAt: '2026-10-08T12:05:00Z',
settings: { hostIds: ['source-a', 'target-b'], game: 'all', maxMoves: 5 },
moves: [move],
hosts: [
{
id: 'source-a',
name: 'Source A',
before: { cpuPercent: 75, memoryPercent: 50, instances: 6 },
after: { cpuPercent: 62.5, memoryPercent: 42, instances: 5 }
}
],
exclusions: [{ server: 't6_zm_transit_1', reason: 'Lobby has players.' }],
warnings: ['Occupied lobbies stay in place.']
};
const run: RebalanceRun = {
id: 'run-a',
createdAt: at,
updatedAt: at,
settings: preview.settings,
state: 'running',
message: 'Moving one lobby.',
moves: [
{
...move,
id: 'move-a',
state: 'moving',
operationId: 'operation-a',
message: 'Waiting for destination health.',
updatedAt: at
}
]
};
const noop = () => {};
it('shows current and projected resources, the move destination, and exclusion reasons before starting', () => {
const body = render(RebalancePreviewView, {
props: { preview, expired: false, onstart: noop }
}).body;
expect(body).toContain('6 → 5');
expect(body).toContain('75% → 62.5%');
expect(body).toContain('50% → 42%');
expect(body).toContain('Source A → Target B');
expect(body).toContain('Lobby has players.');
expect(body).toContain('Occupied lobbies stay in place.');
expect(body).toContain('aria-label="Projected host distribution" tabindex="0"');
expect(body).not.toMatch(/<button[^>]*disabled/);
});
it.each(['expired', 'empty', 'blocked'] as const)('prevents starting a %s preview', (condition) => {
const body = render(RebalancePreviewView, {
props: {
preview: condition === 'empty' ? { ...preview, moves: [] } : preview,
expired: condition === 'expired',
disabled: condition === 'blocked',
onstart: noop
}
}).body;
expect(body).toMatch(/<button[^>]*disabled/);
if (condition === 'expired') expect(body).toContain('This preview has expired.');
if (condition === 'empty') expect(body).toContain('No eligible moves would improve');
});
it('keeps run progress tied to its provisioning record and explains stopping semantics', () => {
const body = render(RebalanceRunView, { props: { run, canManage: true, onstop: noop } }).body;
expect(body).toContain('0 of 1 moves completed');
expect(body).toContain('Waiting for destination health.');
expect(body).toContain(
'href="/provisioning/operation-a?returnTo=%2Fcapacity%3Frebalance%3D1%26rebalanceRun%3Drun-a"'
);
expect(body).toContain('Stop remaining moves');
expect(body.replace(/\s+/g, ' ')).toContain(
'A move already in progress continues through health verification.'
);
});
it('hides stop from viewers and terminal or already-stopping runs', () => {
expect(
render(RebalanceRunView, { props: { run, canManage: false, onstop: noop } }).body
).not.toContain('Stop remaining moves');
for (const state of ['stopping', 'done', 'stopped', 'failed'] as const) {
const body = render(RebalanceRunView, {
props: { run: { ...run, state }, canManage: true, onstop: noop }
}).body;
expect(body).not.toContain('Stop remaining moves');
expect(body).toContain('View provisioning');
}
});