Files
curo1305 0db412d66c feat(07-05): frontend AI config client helpers + AdminAiConfigTab system section + Vitest tests
- Add getAiConfig/saveAiConfig/testAiConnection to frontend/src/api/client.js
- AdminAiConfigTab.vue: add System AI Providers section above existing per-user table
  - Per-provider accordion (10 providers from PROVIDER_DEFAULTS)
  - Write-only API key field (never pre-filled), base URL, model, context_chars inputs
  - Set Active (atomic flip), Save, Test Connection buttons with inline badges
  - Existing per-user ai-config table and saveConfig logic untouched (Pitfall 6)
- Create frontend/tests/api.spec.js: 4 Vitest tests for getAiConfig/saveAiConfig/testAiConnection
- All 127 frontend tests pass, build exits 0 (D-11 frontend coverage)
2026-06-04 23:22:08 +02:00

85 lines
3.3 KiB
JavaScript

/**
* Vitest unit tests for the system AI provider client helpers.
*
* Tests verify that getAiConfig, saveAiConfig, and testAiConnection
* construct the correct fetch payloads (path, method, body, encoding).
*
* D-11 frontend coverage — CLAUDE.md mandate: every new function has at least one test.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
// ── Mock Pinia auth store (request() lazy-imports it) ────────────────────────
// The request() helper in client.js does:
// const { useAuthStore } = await import('../stores/auth.js')
// const authStore = useAuthStore()
// We stub the module so it returns a store with no accessToken,
// preventing Pinia initialization in unit tests.
vi.mock('../src/stores/auth.js', () => ({
useAuthStore: () => ({ accessToken: null }),
}))
// ── Mock global fetch ─────────────────────────────────────────────────────────
let fetchMock
beforeEach(() => {
fetchMock = vi.fn(() =>
Promise.resolve(
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
)
)
vi.stubGlobal('fetch', fetchMock)
})
// ── Import helpers under test ─────────────────────────────────────────────────
import { getAiConfig, saveAiConfig, testAiConnection } from '../src/api/client.js'
// ── Tests ─────────────────────────────────────────────────────────────────────
describe('getAiConfig', () => {
it('issues GET /api/admin/ai-config', async () => {
await getAiConfig()
expect(fetchMock).toHaveBeenCalledOnce()
const [url, init] = fetchMock.mock.calls[0]
expect(url).toMatch(/\/api\/admin\/ai-config$/)
expect(init.method).toBe('GET')
})
})
describe('saveAiConfig', () => {
it('issues PUT with JSON body containing provider_id and api_key', async () => {
const payload = { provider_id: 'openai', api_key: 'sk-x' }
await saveAiConfig(payload)
expect(fetchMock).toHaveBeenCalledOnce()
const [url, init] = fetchMock.mock.calls[0]
expect(url).toMatch(/\/api\/admin\/ai-config$/)
expect(init.method).toBe('PUT')
expect(init.headers?.['Content-Type'] || init.headers?.get?.('Content-Type')).toBe('application/json')
expect(JSON.parse(init.body)).toEqual({ provider_id: 'openai', api_key: 'sk-x' })
})
})
describe('testAiConnection', () => {
it('issues GET with URL-encoded provider_id', async () => {
// Provider ID with a space to verify encodeURIComponent is applied
await testAiConnection('open ai')
expect(fetchMock).toHaveBeenCalledOnce()
const [url, init] = fetchMock.mock.calls[0]
expect(url).toContain('/api/admin/ai-config/test-connection?provider_id=open%20ai')
expect(init.method).toBe('GET')
})
it('issues GET with a plain provider_id that needs no encoding', async () => {
await testAiConnection('openai')
const [url] = fetchMock.mock.calls[0]
expect(url).toContain('/api/admin/ai-config/test-connection?provider_id=openai')
})
})