- 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)
85 lines
3.3 KiB
JavaScript
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')
|
|
})
|
|
})
|