import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { komodoInspectHealthy } from '../../src/lib/server/integrations/komodo.js'; vi.mock('../../src/lib/server/config.js', () => ({ config: { komodo: { address: 'http://komodo.test', apiKey: 'test', apiSecret: 'test' }, httpTimeoutMs: 1000 }, komodoConfigured: () => true })); vi.mock('../../src/lib/server/fleet/servers.js', () => ({ getServer: () => ({ komodo_type: 'deployment', komodo_name: 'saved-resource' }) })); const fetchMock = vi.fn(); beforeEach(() => { fetchMock.mockReset(); vi.stubGlobal('fetch', fetchMock); }); afterEach(() => { vi.unstubAllGlobals(); vi.useRealTimers(); }); describe('Komodo health endpoint fallback', () => { it('limits an unreachable probe to one timeout and preserves its diagnosis', async () => { vi.useFakeTimers(); fetchMock.mockImplementation((_url, options) => new Promise((_resolve, reject) => { options?.signal?.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError'))); })); const result = komodoInspectHealthy('server'); await vi.advanceTimersByTimeAsync(1000); expect(fetchMock).toHaveBeenCalledTimes(1); expect(await result).toMatchObject({ ok: false, reason: 'timed out after 1000ms dialing http://komodo.test/read/GetDeploymentContainer' }); }); it('preserves the network cause without probing alternate endpoints', async () => { fetchMock.mockRejectedValue(new TypeError('fetch failed', { cause: Object.assign(new Error('connection refused'), { code: 'ECONNREFUSED' }) })); expect(await komodoInspectHealthy('server')).toMatchObject({ ok: false, reason: expect.stringContaining('ECONNREFUSED: connection refused') }); expect(fetchMock).toHaveBeenCalledTimes(1); }); it.each([400, 401, 403, 429, 500, 502, 503])('stops on HTTP %i without hiding the response', async status => { fetchMock.mockResolvedValue(new Response('request failed', { status })); expect(await komodoInspectHealthy('server')).toEqual({ ok: false, reason: `HTTP ${status}`, snippet: 'request failed' }); expect(fetchMock).toHaveBeenCalledTimes(1); }); it.each([ [404, 'Not Found'], [400, 'unknown variant `GetDeploymentContainer`'], [405, 'unsupported method'], [500, 'unknown method'], [501, 'method not found'] ])('tries a compatible endpoint after HTTP %i: %s', async (status, message) => { fetchMock .mockResolvedValueOnce(new Response(message as string, { status: status as number })) .mockResolvedValueOnce(Response.json({ State: { Status: 'running' } })); expect(await komodoInspectHealthy('server')).toEqual({ ok: true, state: 'running' }); expect(fetchMock.mock.calls.map(([url]) => url)).toEqual([ 'http://komodo.test/read/GetDeploymentContainer', 'http://komodo.test/read/InspectDeploymentContainer' ]); }); it('continues past successful responses with an unknown shape', async () => { fetchMock .mockResolvedValueOnce(Response.json({ config: { image: 'server' } })) .mockResolvedValueOnce(Response.json({ State: { Status: 'running' } })); expect(await komodoInspectHealthy('server')).toEqual({ ok: true, state: 'running' }); expect(fetchMock).toHaveBeenCalledTimes(2); }); it('does not overwrite an unhealthy container state with a later response', async () => { fetchMock.mockResolvedValueOnce(Response.json({ State: { Status: 'exited' } })); expect(await komodoInspectHealthy('server')).toMatchObject({ ok: false, state: 'exited', reason: 'container state: exited' }); expect(fetchMock).toHaveBeenCalledTimes(1); }); it('preserves malformed response diagnostics', async () => { fetchMock.mockResolvedValueOnce(new Response('not json')); expect(await komodoInspectHealthy('server')).toMatchObject({ ok: false, reason: expect.stringContaining('unparseable json:'), snippet: 'not json' }); expect(fetchMock).toHaveBeenCalledTimes(1); }); });