/** * @vitest-environment jsdom */ import { describe, it, expect, vi } from 'vitest'; import { act, renderHook, waitFor } from '@testing-library/react'; import { http, HttpResponse, server, apiUrl } from '../../test/server'; import { useSwarmHosts } from './useSwarmHosts'; import type { SwarmHost } from './types'; const host = (over: Partial = {}): SwarmHost => ({ uuid: 'h-1', name: 'agent-1', address: '10.0.0.10', agent_port: 8443, status: 'active', last_heartbeat: '2026-05-01T00:00:00Z', client_cert_fingerprint: 'a'.repeat(64), updater_cert_fingerprint: null, enrolled_at: '2026-05-01T00:00:00Z', notes: null, ...over, }); describe('useSwarmHosts', () => { it('loads /swarm/hosts on mount', async () => { server.use( http.get(apiUrl('/swarm/hosts'), () => HttpResponse.json([host()])), ); const { result } = renderHook(() => useSwarmHosts()); expect(result.current.loading).toBe(true); await waitFor(() => expect(result.current.loading).toBe(false)); expect(result.current.hosts).toHaveLength(1); }); it('surfaces error on load failure', async () => { server.use( http.get(apiUrl('/swarm/hosts'), () => HttpResponse.json({ detail: 'forbidden' }, { status: 403 }), ), ); const { result } = renderHook(() => useSwarmHosts()); await waitFor(() => expect(result.current.loading).toBe(false)); expect(result.current.error).toBe('forbidden'); }); it('teardownHost reports ok and reloads', async () => { let calls = 0; server.use( http.get(apiUrl('/swarm/hosts'), () => { calls += 1; return HttpResponse.json([host()]); }), http.post(apiUrl('/swarm/hosts/h-1/teardown'), () => new HttpResponse(null, { status: 202 }), ), ); const { result } = renderHook(() => useSwarmHosts()); await waitFor(() => expect(result.current.loading).toBe(false)); const before = calls; let r: Awaited> | undefined; await act(async () => { r = await result.current.teardownHost('h-1'); }); expect(r).toEqual({ ok: true }); expect(calls).toBeGreaterThan(before); }); it('decommissionHost surfaces server reason on failure', async () => { server.use( http.get(apiUrl('/swarm/hosts'), () => HttpResponse.json([host()])), http.delete(apiUrl('/swarm/hosts/h-1'), () => HttpResponse.json({ detail: 'cannot remove last host' }, { status: 400 }), ), ); const { result } = renderHook(() => useSwarmHosts()); await waitFor(() => expect(result.current.loading).toBe(false)); let r: Awaited> | undefined; await act(async () => { r = await result.current.decommissionHost('h-1'); }); expect(r).toEqual({ ok: false, reason: 'cannot remove last host' }); }); it('generateBundle returns the bundle on success', async () => { server.use( http.get(apiUrl('/swarm/hosts'), () => HttpResponse.json([])), http.post(apiUrl('/swarm/enroll-bundle'), () => HttpResponse.json({ token: 'abc', host_uuid: 'h-9', command: 'curl … | bash', expires_at: '2026-05-09T08:05:00Z', }), ), ); const { result } = renderHook(() => useSwarmHosts()); await waitFor(() => expect(result.current.loading).toBe(false)); let r: Awaited> | undefined; await act(async () => { r = await result.current.generateBundle({ master_host: 'master.local', agent_name: 'a-1', with_updater: true, use_ipvlan: false, services_ini: null, }); }); expect(r?.ok).toBe(true); expect(r?.data?.host_uuid).toBe('h-9'); }); it('polls /swarm/hosts every 10s', async () => { vi.useFakeTimers({ shouldAdvanceTime: true }); try { let calls = 0; server.use( http.get(apiUrl('/swarm/hosts'), () => { calls += 1; return HttpResponse.json([]); }), ); const { result } = renderHook(() => useSwarmHosts()); await waitFor(() => expect(result.current.loading).toBe(false)); const initial = calls; await act(async () => { vi.advanceTimersByTime(10_500); }); await waitFor(() => expect(calls).toBeGreaterThan(initial)); } finally { vi.useRealTimers(); } }); });