Fix transient health checks and avoid unnecessary recovery
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
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<typeof fetch>();
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user