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 } from '../../src/lib/ui/autoscaling.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, 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 } }; } function body(data = view(), canOperate = true, globallyPaused = false) { 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 }; } 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).toContain('href="/provisioning/job-1"'); expect(html).not.toContain('Work is still settling'); expect(html).not.toContain('Provisioning job-1:'); expect(html).not.toContain('No current source'); expect(html).not.toContain('before deleting'); expect(html).not.toContain(' { const data = view(); 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(body()).toContain('1 / 10 maps meet spare target'); }); it('does not imply global readiness from one satisfied map', () => { const data = view(); data.decision.reason = 'Kino Der Toten: Empty spare target is satisfied.'; expect(policyStatus(data, false).label).not.toBe('Spare targets satisfied'); data.decision.deficit = 0; expect(policyStatus(data, false).label).toBe('Spare targets satisfied'); }); it('preserves attention and pause notices while a job is outstanding', () => { const data = view(); data.policy.pausedReason = 'Uncertain host command. Review Provisioning recovery.'; data.unsettled[0].state = 'failed'; const html = body(data); expect(html).toContain(data.policy.pausedReason); expect(html).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.'); }); it('groups job wait decisions with their original request without swallowing unrelated blockers', () => { const data = view(); data.history = [ history('wait-2', now + 30, 'wait', 'Provisioning job-1: Checking readiness'), history('blocked', now + 20, 'wait', 'No available game keys.'), history('wait-1', now + 10, 'wait', 'Provisioning job-1: Host command running'), history('create', now, 'create', 'Replenish empty lobby capacity.', 'job-1') ]; 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[1].reason).toBe('No available game keys.'); }); it('renders accessible tabs and hides mutation controls from viewers', () => { const html = body(view(), false); expect(html).toContain('role="tablist"'); expect(html).toContain('aria-labelledby="tab-policy-lobbies"'); expect(html).toContain('role="tabpanel"'); expect(html).not.toContain('Edit policy'); expect(html).not.toContain('More actions'); expect(lobbyLabel('legacy-server')).toBe('legacy-server'); }); });