mateclaw/mateclaw-ui/src/composables/__tests__/useSearchProviderCatalog.test.ts
倪程伟 53a12ee18d
feat(plugin/settings): search-provider catalog endpoint + grouped settings UI + plugin config form
Adds a read-only GET /api/v1/settings/search-providers catalog (admin-gated, no secrets), grouped collapsible provider cards, and a schema-driven plugin config form. Breaks a SystemSettingService<->PluginManager circular dependency via parameter @Lazy (with a context smoke test), and fixes PluginManager.updateConfig to merge instead of overwrite so omitted/blank secret fields are preserved. Hardens plugin search-provider id validation (reject-not-trim, case-insensitive conflict) and insulates the provider bridge hot path from throwing plugin code.
2026-07-04 12:06:33 +08:00

100 lines
4.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
buildProviderOptions,
builtinFallbackCatalog,
resolveDefaultExpandedId,
resolveSourceLabelKey,
} from '../useSearchProviderCatalog'
import type { SearchProviderCatalog } from '@/types'
const catalog: SearchProviderCatalog = {
providers: [
{ id: 'serper', label: 'Serper (Google)', builtin: true, requiresCredential: true, available: false, pluginName: null },
{ id: 'duckduckgo', label: 'DuckDuckGo', builtin: true, requiresCredential: false, available: true, pluginName: null },
{ id: 'my-search', label: 'My Search', builtin: false, requiresCredential: true, available: true, pluginName: 'my-plugin' },
],
resolvedId: 'duckduckgo',
resolvedSource: 'keyless-fallback',
}
describe('buildProviderOptions', () => {
it('prepends an auto option with empty-string value', () => {
const options = buildProviderOptions(catalog, 'auto-label')
expect(options[0]).toEqual({ value: '', label: 'auto-label' })
expect(options).toHaveLength(4)
})
it('maps each catalog entry to a value/label pair preserving order', () => {
const options = buildProviderOptions(catalog, 'auto-label')
expect(options.slice(1)).toEqual([
{ value: 'serper', label: 'Serper (Google)' },
{ value: 'duckduckgo', label: 'DuckDuckGo' },
{ value: 'my-search', label: 'My Search' },
])
})
it('returns just the auto option when the catalog is empty', () => {
const options = buildProviderOptions({ providers: [], resolvedId: null, resolvedSource: null }, 'auto-label')
expect(options).toEqual([{ value: '', label: 'auto-label' }])
})
it('appends the saved provider id when it is missing from the catalog', () => {
const options = buildProviderOptions(catalog, 'auto-label', 'vanished-plugin-search')
expect(options[options.length - 1]).toEqual({ value: 'vanished-plugin-search', label: 'vanished-plugin-search' })
})
it('does not duplicate the saved provider id when it is already in the catalog', () => {
const options = buildProviderOptions(catalog, 'auto-label', 'serper')
expect(options.filter((o) => o.value === 'serper')).toHaveLength(1)
})
it('does not append anything for an empty or null saved value', () => {
expect(buildProviderOptions(catalog, 'auto-label', '')).toHaveLength(4)
expect(buildProviderOptions(catalog, 'auto-label', null)).toHaveLength(4)
})
})
describe('builtinFallbackCatalog', () => {
it('contains exactly the four built-in providers, all marked builtin with no resolution', () => {
const fallback = builtinFallbackCatalog()
expect(fallback.providers.map((p) => p.id)).toEqual(['searxng', 'duckduckgo', 'serper', 'tavily'])
expect(fallback.providers.every((p) => p.builtin && p.pluginName === null)).toBe(true)
expect(fallback.resolvedId).toBeNull()
expect(fallback.resolvedSource).toBeNull()
})
})
describe('resolveDefaultExpandedId', () => {
it('expands the resolved provider when present', () => {
expect(resolveDefaultExpandedId(catalog)).toBe('duckduckgo')
})
it('falls back to the first provider when nothing is resolved', () => {
const noneResolved = { ...catalog, resolvedId: null, resolvedSource: null }
expect(resolveDefaultExpandedId(noneResolved)).toBe('serper')
})
it('returns null when the catalog has no providers at all', () => {
expect(resolveDefaultExpandedId({ providers: [], resolvedId: null, resolvedSource: null })).toBeNull()
})
})
describe('resolveSourceLabelKey', () => {
it('maps "configured" to "configured"', () => {
expect(resolveSourceLabelKey('configured')).toBe('configured')
})
it('maps "auto-detect" to "autoDetect"', () => {
expect(resolveSourceLabelKey('auto-detect')).toBe('autoDetect')
})
it('falls back to "keylessFallback" for "keyless-fallback"', () => {
expect(resolveSourceLabelKey('keyless-fallback')).toBe('keylessFallback')
})
it('falls back to "keylessFallback" for null or unrecognized values', () => {
expect(resolveSourceLabelKey(null)).toBe('keylessFallback')
expect(resolveSourceLabelKey('something-new')).toBe('keylessFallback')
})
})