import { afterEach, expect, it, vi } from 'vitest'; import { campaignServerStatus, fetchCampaigns } from '../../src/lib/ui/campaigns.js'; afterEach(() => vi.unstubAllGlobals()); it('distinguishes queued work, file application, and health verification without treating stage messages as failures', () => { const server = { restart_state: 'pending', restarted_at: null, error: 'Queued to restart; waiting for another server to recover' }; expect(campaignServerStatus(server, [{ apply_state: 'pending' }])).toEqual({ group: 'queued', label: 'Queued', tone: 'neutral' }); expect(campaignServerStatus(server, [{ apply_state: 'applying' }])).toMatchObject({ group: 'processing', label: 'Applying files' }); expect(campaignServerStatus({ ...server, restarted_at: '2026-10-06T20:08:29Z', error: 'Checking container state with host agent' }, [{ apply_state: 'applied' }])) .toEqual({ group: 'processing', label: 'Verifying health', tone: 'info' }); }); it.each(['failed', 'held', 'manual'])('keeps %s servers needing intervention ahead of ordinary progress', restart_state => { expect(campaignServerStatus({ restart_state, restarted_at: '2026-10-06T20:08:29Z' }, [{ apply_state: 'applying' }])) .toMatchObject({ group: 'attention', tone: restart_state === 'failed' ? 'danger' : 'warning' }); }); it('preserves completed and inactive outcomes without asking users to intervene on deleted servers', () => { const server = { restart_state: 'done', restarted_at: '2026-10-06T20:08:29Z' }; expect(campaignServerStatus(server)).toMatchObject({ group: 'complete', label: 'Complete' }); for (const restart_state of ['skipped', 'cancelled']) { expect(campaignServerStatus({ ...server, restart_state })).toMatchObject({ group: 'inactive', tone: 'neutral' }); } const deleted = { ...server, server_deleted_at: '2026-10-06T21:00:00Z' }; expect(campaignServerStatus(deleted)).toMatchObject({ group: 'complete' }); expect(campaignServerStatus({ ...deleted, restart_state: 'failed' })).toEqual({ group: 'inactive', label: 'Deleted before activation', tone: 'neutral' }); }); it('loads history beyond the API page limit without truncating or fetching unwanted older rows', async () => { const requests: URL[] = []; vi.stubGlobal('fetch', vi.fn(async (input: string) => { const url = new URL(input, 'http://localhost'); requests.push(url); const start = Number(url.searchParams.get('offset')); const count = Number(url.searchParams.get('limit')); return Response.json({ total: 900, deployments: Array.from({ length: count }, (_, i) => ({ id: start + i })) }); })); const result = await fetchCampaigns<{ id: number }>({ server: 'test-server', view: 'history' }, 525); expect(result.total).toBe(900); expect(result.deployments.map(row => row.id)).toEqual(Array.from({ length: 525 }, (_, i) => i)); expect(requests.map(url => Object.fromEntries(url.searchParams))).toEqual([ { server: 'test-server', view: 'history', offset: '0', limit: '500' }, { server: 'test-server', view: 'history', offset: '500', limit: '25' } ]); }); it('loads all active campaigns independently of the history limit', async () => { vi.stubGlobal('fetch', vi.fn(async (input: string) => { const url = new URL(input, 'http://localhost'); const start = Number(url.searchParams.get('offset')); return Response.json({ total: 550, deployments: Array.from({ length: start ? 50 : 500 }, (_, i) => ({ id: start + i })) }); })); const result = await fetchCampaigns<{ id: number }>({ active: 'true' }, Infinity); expect(result.deployments).toHaveLength(550); expect(result.deployments[549].id).toBe(549); });