Files
traefik-llmgateway/webui/src/stores/catalog.spec.ts
T
lukaszraczylo ee49467e64 feat(webui): redesign the admin dashboard around admin tasks
A sidebar replaces the flat tabs with seven pages, and one filter bar
sets the time range, comparison period and scope for all of them.

- Home: spend against budget, forecast, request rate, error rate,
  open breakers, top users and models, recent events and warnings.
- Spend: spend over time with comparison, budget burn-down, an
  attribution drill-down from group to user to model, pricing health
  and cost avoided by free models.
- Consumers: a directory with last seen and limit bars, an access
  matrix, and a detail page per user.
- Models: the catalog with billing prices, fleet latency and error
  rates, and provider health.
- Reliability: error, timeout, failover, 429 and 402 trends, events,
  and latency per provider.
- Config: the redacted config and a change helper that emits YAML or
  users.json snippets to paste into git.
- MCP & Agents: callers per target.

Page state lives in the URL hash, and old tab links map to the new
pages. The replaced views are removed. admin_assets_gen.go is
regenerated.

Claude-Session: https://claude.ai/code/session_01V5jYJNxpDFdJkyyExpADrv
2026-09-23 16:41:01 +01:00

147 lines
4.7 KiB
TypeScript

import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@/lib/api', async () => {
const actual = await vi.importActual<typeof import('@/lib/api')>('@/lib/api')
return { ...actual, adminFetch: vi.fn() }
})
import { adminFetch, AdminApiError } from '@/lib/api'
import { useAuthStore } from '@/stores/auth'
import { useCatalogStore } from '@/stores/catalog'
import type { AdminCatalogResponse } from '@/types/api'
const mockedAdminFetch = vi.mocked(adminFetch)
const catalogFixture: AdminCatalogResponse = {
providers: [
{
name: 'openai',
type: 'openai',
lastRefresh: '2026-09-23T00:00:00Z',
openUntil: '0001-01-01T00:00:00Z',
healthState: 'closed',
discoveryEnabled: true,
models: [
{ id: 'openai/gpt-5', model: 'gpt-5', priceSource: 'builtin', inputPerMTokUsd: 1.25, outputPerMTokUsd: 10 },
{ id: 'openai/gpt-5-mini:free', model: 'gpt-5-mini:free', priceSource: 'unpriced', displayFree: true },
],
},
],
aliases: [],
}
describe('useCatalogStore.fetch', () => {
beforeEach(() => {
setActivePinia(createPinia())
mockedAdminFetch.mockReset()
useAuthStore().submit('test-admin-key')
})
it('is a no-op while unauthenticated', async () => {
setActivePinia(createPinia())
const catalog = useCatalogStore()
await catalog.fetch()
expect(mockedAdminFetch).not.toHaveBeenCalled()
expect(catalog.data).toBeNull()
})
it('populates data and fetchedAt on success, clearing any prior error', async () => {
mockedAdminFetch.mockResolvedValue(catalogFixture)
const catalog = useCatalogStore()
await catalog.fetch()
expect(catalog.data).toEqual(catalogFixture)
expect(catalog.fetchedAt).not.toBeNull()
expect(catalog.error).toBe('')
expect(catalog.loading).toBe(false)
})
it('indexes modelsById by canonical id across every provider', async () => {
mockedAdminFetch.mockResolvedValue(catalogFixture)
const catalog = useCatalogStore()
await catalog.fetch()
expect(catalog.modelsById.get('openai/gpt-5')?.priceSource).toBe('builtin')
expect(catalog.modelsById.get('openai/gpt-5-mini:free')?.displayFree).toBe(true)
expect(catalog.modelsById.size).toBe(2)
})
it('records a plain error message on a non-auth failure', async () => {
mockedAdminFetch.mockRejectedValue(new Error('boom'))
const catalog = useCatalogStore()
await catalog.fetch()
expect(catalog.error).toBe('boom')
expect(catalog.data).toBeNull()
})
it('does not surface an error for a 401/403 (the auth store owns that case)', async () => {
mockedAdminFetch.mockRejectedValue(new AdminApiError('unauthorized', 401))
const catalog = useCatalogStore()
await catalog.fetch()
expect(catalog.error).toBe('')
})
it('latest-request-wins: an older in-flight fetch never overwrites a newer one', async () => {
let resolveFirst!: (v: AdminCatalogResponse) => void
mockedAdminFetch.mockImplementationOnce(
() => new Promise<AdminCatalogResponse>((resolve) => (resolveFirst = resolve)),
)
const catalog = useCatalogStore()
const first = catalog.fetch()
mockedAdminFetch.mockResolvedValueOnce(catalogFixture)
await catalog.fetch()
expect(catalog.data).toEqual(catalogFixture)
resolveFirst({ providers: [], aliases: [] })
await first
// The first (older) fetch's empty result must not clobber the second's.
expect(catalog.data).toEqual(catalogFixture)
})
})
describe('useCatalogStore.ensureLoaded', () => {
beforeEach(() => {
setActivePinia(createPinia())
mockedAdminFetch.mockReset()
mockedAdminFetch.mockResolvedValue(catalogFixture)
useAuthStore().submit('test-admin-key')
})
it('fetches when never loaded', async () => {
const catalog = useCatalogStore()
await catalog.ensureLoaded()
expect(mockedAdminFetch).toHaveBeenCalledTimes(1)
})
it('does not refetch a fresh catalog', async () => {
const catalog = useCatalogStore()
await catalog.ensureLoaded()
await catalog.ensureLoaded()
expect(mockedAdminFetch).toHaveBeenCalledTimes(1)
})
it('refetches once stale', async () => {
vi.useFakeTimers()
try {
const catalog = useCatalogStore()
await catalog.ensureLoaded()
vi.advanceTimersByTime(5 * 60 * 1000 + 1)
await catalog.ensureLoaded()
expect(mockedAdminFetch).toHaveBeenCalledTimes(2)
} finally {
vi.useRealTimers()
}
})
it('force bypasses the staleness check', async () => {
const catalog = useCatalogStore()
await catalog.ensureLoaded()
await catalog.ensureLoaded(true)
expect(mockedAdminFetch).toHaveBeenCalledTimes(2)
})
})