import { afterEach, describe, expect, it, vi } from 'vitest' import { api } from './api' const service = { id: 'svc-1', name: 'API', kind: 'http' as const, url: 'https://api.local', host: null, port: null, depends_on: [], created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', } afterEach(() => { vi.unstubAllGlobals() }) describe('ServiceMap API client', () => { it('creates services with the v1 JSON contract', async () => { const fetchMock = vi.fn().mockResolvedValue( new Response(JSON.stringify(service), { status: 201, headers: { 'Content-Type': 'application/json' }, }), ) vi.stubGlobal('fetch', fetchMock) const result = await api.createService({ name: 'API', kind: 'http', url: 'https://api.local', depends_on: [], }) expect(result.name).toBe('API') expect(fetchMock).toHaveBeenCalledWith( '/api/v1/services', expect.objectContaining({ method: 'POST', body: JSON.stringify({ name: 'API', kind: 'http', url: 'https://api.local', depends_on: [], }), }), ) }) it('uses dedicated bulk probing and incident endpoints', async () => { const fetchMock = vi.fn() .mockResolvedValueOnce(new Response(JSON.stringify({ requested: 0, completed: 0, results: [] }), { status: 200 })) .mockResolvedValueOnce(new Response(JSON.stringify({ generated_at: '2026-01-01T00:00:00Z', active: false, candidates: [], degraded_service_ids: [], down_service_ids: [] }), { status: 200 })) vi.stubGlobal('fetch', fetchMock) await api.probeAll() await api.incidents() expect(fetchMock.mock.calls[0][0]).toBe('/api/v1/probe-all') expect(fetchMock.mock.calls[1][0]).toBe('/api/v1/incidents') }) it('uses the safe synthetic demo session endpoints', async () => { const demoState = { active: true, synthetic: true, scenario_name: 'Northstar Platform', service_count: 9, root_cause_service_id: 'postgresql', root_cause_service_name: 'PostgreSQL', affected_service_ids: ['api', 'web'], affected_service_names: ['API Gateway', 'Web Frontend'], affected_count: 2, } const fetchMock = vi.fn() .mockResolvedValueOnce(new Response(JSON.stringify(demoState), { status: 200 })) .mockResolvedValueOnce(new Response(JSON.stringify({ ...demoState, created: 9, updated: 0, total: 9, restored: false }), { status: 200 })) .mockResolvedValueOnce(new Response(JSON.stringify({ ...demoState, created: 9, updated: 0, total: 9, restored: false }), { status: 200 })) .mockResolvedValueOnce(new Response(JSON.stringify({ active: false, synthetic: false, scenario_name: null, service_count: 0, root_cause_service_id: null, root_cause_service_name: null, affected_service_ids: [], affected_service_names: [], affected_count: 0, created: 0, updated: 0, total: 0, restored: true }), { status: 200 })) vi.stubGlobal('fetch', fetchMock) await api.demoState() await api.loadDemo() await api.resetDemo() await api.exitDemo() expect(fetchMock.mock.calls.map(([url]) => url)).toEqual([ '/api/v1/demo', '/api/v1/demo', '/api/v1/demo/reset', '/api/v1/demo/exit', ]) expect(fetchMock.mock.calls[1][1]).toEqual(expect.objectContaining({ method: 'POST' })) }) it('uses the production discovery and canonical edge contracts', async () => { const run = { run: { id: 'run-1', status: 'completed', started_at: '2026-01-01T00:00:00Z', completed_at: '2026-01-01T00:01:00Z', source_counts: {}, diff: { new: 1, changed: 0, missing: 0, unchanged: 0, stale: 0, new_suggestions: 1 }, error: null }, candidates: [], suggestions: [], observations: [], appops_available: true, appops_mappings: 1, proxy_routes: 2 } const fetchMock = vi.fn() .mockResolvedValueOnce(new Response(JSON.stringify(run), { status: 200 })) .mockResolvedValueOnce(new Response(JSON.stringify([]), { status: 200 })) .mockResolvedValueOnce(new Response(JSON.stringify({ id: 'edge-1' }), { status: 201 })) vi.stubGlobal('fetch', fetchMock) await api.runDiscovery('unix:///var/run/docker.sock', 'http://appops.local:1216') await api.discoverySuggestions('open') await api.createEdge({ source_service_id: 'dependent', target_service_id: 'dependency', lock: true }) expect(fetchMock.mock.calls[0][0]).toBe('/api/v1/discovery/run') expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ docker_host: 'unix:///var/run/docker.sock', appops_url: 'http://appops.local:1216', include_stopped: false }) expect(fetchMock.mock.calls[1][0]).toBe('/api/v1/discovery/suggestions?status=open') expect(fetchMock.mock.calls[2][0]).toBe('/api/v1/topology/edges') }) it('previews and applies reachability repair through the explicit safety endpoint', async () => { const report = { run_id: 'run-1', dry_run: true, inspected: 2, changed: 1, disabled_unsafe: 1, enabled_published: 0, docker_health_observed: 0, unmonitored: 1, resolved_incidents: 0, changes: [] } const fetchMock = vi.fn() .mockResolvedValueOnce(new Response(JSON.stringify(report), { status: 200 })) .mockResolvedValueOnce(new Response(JSON.stringify({ ...report, dry_run: false }), { status: 200 })) vi.stubGlobal('fetch', fetchMock) await api.repairDiscoveredProbes(true, 'run-1') await api.repairDiscoveredProbes(false, 'run-1') expect(fetchMock.mock.calls.map(([url]) => url)).toEqual([ '/api/v1/discovery/probes/repair?dry_run=true&run_id=run-1', '/api/v1/discovery/probes/repair?dry_run=false&run_id=run-1', ]) }) })