Pulse/frontend-modern/src/api/__tests__/ai.test.ts
2025-12-17 12:00:59 +00:00

130 lines
3.8 KiB
TypeScript

import { describe, expect, it, beforeEach, vi } from 'vitest';
vi.mock('@/utils/apiClient', () => ({
apiFetchJSON: vi.fn(),
apiFetch: vi.fn(),
}));
vi.mock('@/utils/logger', () => ({
logger: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
import { AIAPI } from '@/api/ai';
import { apiFetch, apiFetchJSON } from '@/utils/apiClient';
describe('AIAPI', () => {
const apiFetchJSONMock = vi.mocked(apiFetchJSON);
const apiFetchMock = vi.mocked(apiFetch);
beforeEach(() => {
apiFetchJSONMock.mockReset();
apiFetchMock.mockReset();
});
it('calls the expected endpoints for settings and models', async () => {
apiFetchJSONMock.mockResolvedValueOnce({ configured: true } as any);
await AIAPI.getSettings();
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/settings/ai');
apiFetchJSONMock.mockResolvedValueOnce({ configured: true } as any);
await AIAPI.updateSettings({ enabled: true });
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/settings/ai/update', {
method: 'PUT',
body: JSON.stringify({ enabled: true }),
});
apiFetchJSONMock.mockResolvedValueOnce({ models: [] } as any);
await AIAPI.getModels();
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/ai/models');
});
it('includes query parameters for cost and intelligence endpoints', async () => {
apiFetchJSONMock.mockResolvedValueOnce({} as any);
await AIAPI.getCostSummary();
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/ai/cost/summary?days=30');
apiFetchJSONMock.mockResolvedValueOnce({} as any);
await AIAPI.getCostSummary(7);
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/ai/cost/summary?days=7');
apiFetchJSONMock.mockResolvedValueOnce({} as any);
await AIAPI.getPatterns('vm:101');
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/ai/intelligence/patterns?resource_id=vm%3A101');
apiFetchJSONMock.mockResolvedValueOnce({} as any);
await AIAPI.getRecentChanges(12);
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/ai/intelligence/changes?hours=12');
});
it('sanitizes runCommand payload consistently', async () => {
apiFetchJSONMock.mockResolvedValueOnce({ output: 'ok', success: true } as any);
await AIAPI.runCommand({
command: 'echo hi',
target_type: 'vm',
target_id: 'vm-101',
run_on_host: undefined as any,
vmid: 101,
target_host: 'delly',
});
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/ai/run-command', {
method: 'POST',
body: JSON.stringify({
command: 'echo hi',
target_type: 'vm',
target_id: 'vm-101',
run_on_host: false,
vmid: '101',
target_host: 'delly',
}),
});
});
it('throws useful errors for investigateAlert failures', async () => {
apiFetchMock.mockResolvedValueOnce(new Response('backend error', { status: 500 }));
await expect(
AIAPI.investigateAlert(
{
alert_id: 'a1',
resource_id: 'r1',
resource_name: 'res',
resource_type: 'vm',
alert_type: 'cpu',
level: 'warning',
value: 1,
threshold: 2,
message: 'msg',
duration: '1m',
},
() => undefined
)
).rejects.toThrow('backend error');
});
it('throws when investigateAlert has no streaming body', async () => {
apiFetchMock.mockResolvedValueOnce(new Response(null, { status: 200 }));
await expect(
AIAPI.investigateAlert(
{
alert_id: 'a1',
resource_id: 'r1',
resource_name: 'res',
resource_type: 'vm',
alert_type: 'cpu',
level: 'warning',
value: 1,
threshold: 2,
message: 'msg',
duration: '1m',
},
() => undefined
)
).rejects.toThrow('No response body');
});
});