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(/]*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(/]*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).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'); } });