diff --git a/src/api/__tests__/manage.spec.ts b/src/api/__tests__/manage.spec.ts new file mode 100644 index 00000000..579cb83d --- /dev/null +++ b/src/api/__tests__/manage.spec.ts @@ -0,0 +1,55 @@ +import { manageLlmProvider, manageNotificationChannel, manageStorage } from '@/api/manage' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + apiPost: vi.fn(), +})) + +vi.mock('@/api', () => ({ + default: createDataApiMock({ + post: (...args: unknown[]) => mocks.apiPost(...args), + }), +})) + +describe('manage API adapters', () => { + beforeEach(() => { + mocks.apiPost.mockReset() + mocks.apiPost.mockResolvedValue({ ok: true }) + }) + + it('透传通知渠道的目标、动作、参数和请求配置', async () => { + const config = { timeout: 1200 } + + await expect(manageNotificationChannel('telegram', 'refresh', { force: true }, config)).resolves.toEqual({ + ok: true, + }) + + expect(mocks.apiPost).toHaveBeenCalledWith( + 'notification/manage', + { target: 'telegram', action: 'refresh', params: { force: true } }, + config, + ) + }) + + it('为网盘存储动作补充空参数对象', async () => { + await manageStorage('alist', 'disconnect') + + expect(mocks.apiPost).toHaveBeenCalledWith( + 'storage/manage', + { target: 'alist', action: 'disconnect', params: {} }, + undefined, + ) + }) + + it('透传 LLM Provider 管理动作并返回已解包业务数据', async () => { + mocks.apiPost.mockResolvedValueOnce([{ id: 'openai' }]) + + await expect(manageLlmProvider('', 'list_providers')).resolves.toEqual([{ id: 'openai' }]) + + expect(mocks.apiPost).toHaveBeenCalledWith( + 'llm/manage', + { target: '', action: 'list_providers', params: {} }, + undefined, + ) + }) +}) diff --git a/src/composables/__tests__/useLlmProviderDirectory.spec.ts b/src/composables/__tests__/useLlmProviderDirectory.spec.ts index f2e739f9..d1917977 100644 --- a/src/composables/__tests__/useLlmProviderDirectory.spec.ts +++ b/src/composables/__tests__/useLlmProviderDirectory.spec.ts @@ -1,23 +1,28 @@ -import { useLlmProviderDirectory } from '@/composables/useLlmProviderDirectory' -import { mount } from '@vue/test-utils' -import { defineComponent, nextTick, ref } from 'vue' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + useLlmProviderDirectory, + type LlmModel, + type LlmProvider, + type LlmProviderAuthSession, +} from '@/composables/useLlmProviderDirectory' +import { mount, type VueWrapper } from '@vue/test-utils' +import { defineComponent, h, nextTick, ref } from 'vue' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ - apiPost: vi.fn(), + manageLlmProvider: vi.fn(), })) -vi.mock('@/api', () => ({ - default: createDataApiMock({ - post: (...args: unknown[]) => mocks.apiPost(...args), - }), +vi.mock('@/api/manage', () => ({ + manageLlmProvider: (...args: unknown[]) => mocks.manageLlmProvider(...args), })) -function createProvider(id: string, runtime: string) { +const wrappers: VueWrapper[] = [] + +function createProvider(id: string, overrides: Partial = {}): LlmProvider { return { id, name: id, - runtime, + runtime: 'openai_compatible', default_base_url: '', base_url_presets: [], base_url_editable: true, @@ -28,112 +33,542 @@ function createProvider(id: string, runtime: string) { supports_model_refresh: true, oauth_methods: [], auth_status: { connected: false }, + ...overrides, + } +} + +function createModel(id: string, overrides: Partial = {}): LlmModel { + return { + id, + name: id, + ...overrides, + } +} + +function mountDirectory( + initial: Partial<{ + provider: string + apiKey: string + baseUrl: string + baseUrlPreset: string + useProxy: boolean + userAgent: string + apiProtocol: string + model: string + maxContextTokens: number + authConnected: boolean + }> = {}, +) { + const provider = ref(initial.provider ?? '') + const apiKey = ref(initial.apiKey ?? '') + const baseUrl = ref(initial.baseUrl ?? '') + const baseUrlPreset = ref(initial.baseUrlPreset ?? '') + const useProxy = ref(initial.useProxy ?? false) + const userAgent = ref(initial.userAgent ?? '') + const apiProtocol = ref(initial.apiProtocol ?? 'auto') + const model = ref(initial.model ?? '') + const maxContextTokens = ref(initial.maxContextTokens ?? 0) + const authConnected = ref(initial.authConnected ?? false) + let directory!: ReturnType + + const wrapper = mount( + defineComponent({ + setup() { + directory = useLlmProviderDirectory({ + provider, + apiKey, + baseUrl, + baseUrlPreset, + useProxy, + userAgent, + apiProtocol, + model, + maxContextTokens, + authConnected, + }) + return () => h('div') + }, + }), + ) + wrappers.push(wrapper) + + return { + wrapper, + directory, + provider, + apiKey, + baseUrl, + baseUrlPreset, + useProxy, + userAgent, + apiProtocol, + model, + maxContextTokens, + authConnected, } } describe('useLlmProviderDirectory', () => { beforeEach(() => { - mocks.apiPost.mockReset() + mocks.manageLlmProvider.mockReset() }) - it('只为 OpenAI 兼容 runtime 或声明 Responses 工具能力的模型显示 API 协议字段', async () => { - mocks.apiPost.mockResolvedValue({ - success: true, - data: [ - createProvider('openai', 'openai_compatible'), - createProvider('deepseek', 'deepseek'), - createProvider('google', 'google'), - ], + afterEach(() => { + for (const wrapper of wrappers.splice(0)) { + if (wrapper.exists()) wrapper.unmount() + } + }) + + it('选择首个 Provider,并回填默认 URL、预设和认证状态', async () => { + const provider = createProvider('openai', { + default_base_url: 'https://api.example.com/v1', + base_url_presets: [{ id: 'official', label: '官方', value: 'https://api.example.com/v1' }], + auth_status: { connected: true, label: 'demo@example.com' }, + }) + mocks.manageLlmProvider.mockResolvedValue([provider]) + const state = mountDirectory({ provider: 'missing' }) + + await expect(state.directory.loadProviders()).resolves.toEqual([provider]) + + expect(mocks.manageLlmProvider).toHaveBeenCalledWith('', 'list_providers') + expect(state.provider.value).toBe('openai') + expect(state.baseUrl.value).toBe('https://api.example.com/v1') + expect(state.baseUrlPreset.value).toBe('official') + expect(state.authConnected.value).toBe(true) + expect(state.directory.providerItems.value).toEqual([{ title: 'openai', value: 'openai' }]) + expect(state.directory.loadingProviders.value).toBe(false) + }) + + it('默认保留已有 URL 和预设,并可显式恢复 Provider 默认值', async () => { + const provider = createProvider('openai', { + default_base_url: 'https://default.example.com/v1', + base_url_presets: [{ id: 'official', label: '官方', value: 'https://default.example.com/v1' }], + }) + mocks.manageLlmProvider.mockResolvedValue([provider]) + const state = mountDirectory({ + provider: 'openai', + baseUrl: 'https://custom.example.com/v1', + baseUrlPreset: 'custom', }) - const Harness = defineComponent({ - setup() { - const provider = ref('openai') - const model = ref('') - const directory = useLlmProviderDirectory({ - provider, - apiKey: ref(''), - baseUrl: ref(''), - model, - }) + await state.directory.loadProviders() + expect(state.baseUrl.value).toBe('https://custom.example.com/v1') + expect(state.baseUrlPreset.value).toBe('custom') - return { - loadProviders: directory.loadProviders, - selectProvider: (value: string) => { - provider.value = value - }, - selectModel: (value: string) => { - model.value = value - }, - loadModels: directory.loadModels, - showApiProtocolField: directory.showApiProtocolField, - supportsBuiltinWebSearch: directory.supportsBuiltinWebSearch, - } + await state.directory.loadProviders(false) + expect(state.baseUrl.value).toBe('https://default.example.com/v1') + expect(state.baseUrlPreset.value).toBe('official') + }) + + it('将非数组 Provider 响应规整为空列表,并在失败后恢复 loading', async () => { + const state = mountDirectory() + mocks.manageLlmProvider.mockResolvedValueOnce({ providers: [] }) + + await expect(state.directory.loadProviders()).resolves.toEqual([]) + expect(state.directory.providers.value).toEqual([]) + + mocks.manageLlmProvider.mockRejectedValueOnce(new Error('provider unavailable')) + await expect(state.directory.loadProviders()).rejects.toThrow('provider unavailable') + expect(state.directory.loadingProviders.value).toBe(false) + }) + + it('切换 Provider 时重置从属凭据、模型和协议状态', () => { + const state = mountDirectory({ + provider: 'oauth', + apiKey: 'secret', + baseUrl: 'https://custom.example.com', + baseUrlPreset: 'custom', + apiProtocol: 'responses', + model: 'old-model', + maxContextTokens: 256, + authConnected: false, + }) + state.directory.providers.value = [ + createProvider('oauth', { + default_base_url: 'https://oauth.example.com', + base_url_presets: [{ id: 'oauth-default', label: '默认', value: 'https://oauth.example.com' }], + auth_status: { connected: true }, + }), + ] + state.directory.models.value = [createModel('old-model')] + + state.directory.handleProviderSelection() + + expect(state.baseUrl.value).toBe('https://oauth.example.com') + expect(state.baseUrlPreset.value).toBe('oauth-default') + expect(state.apiKey.value).toBe('') + expect(state.apiProtocol.value).toBe('auto') + expect(state.model.value).toBe('') + expect(state.maxContextTokens.value).toBe(64) + expect(state.authConnected.value).toBe(true) + expect(state.directory.models.value).toEqual([]) + }) + + it('同步 URL 预设值,并为界面生成带说明的选项', () => { + const state = mountDirectory({ provider: 'openai' }) + state.directory.providers.value = [ + createProvider('openai', { + base_url_presets: [{ id: 'proxy', label: '代理', value: 'https://proxy.example.com' }], + }), + ] + + expect(state.directory.baseUrlPresetItems.value).toEqual([ + { + id: 'proxy', + title: 'https://proxy.example.com', + value: 'https://proxy.example.com', + subtitle: '代理', }, - template: '
', + ]) + + state.directory.setBaseUrlPreset(' proxy ', 'https://proxy.example.com') + expect(state.baseUrlPreset.value).toBe('proxy') + expect(state.baseUrl.value).toBe('https://proxy.example.com') + + state.directory.setBaseUrlPreset(undefined, '') + expect(state.baseUrlPreset.value).toBe('') + expect(state.baseUrl.value).toBe('') + }) + + it('按 Provider 能力、凭据和必填 URL 判断模型刷新资格', () => { + const state = mountDirectory({ provider: 'custom' }) + state.directory.providers.value = [ + createProvider('custom', { + requires_base_url: true, + oauth_methods: [{ id: 'device', type: 'device', label: '设备授权' }], + supports_api_key: false, + }), + ] + + expect(state.directory.showBaseUrlField.value).toBe(false) + expect(state.directory.showApiKeyField.value).toBe(false) + expect(state.directory.hasUsableCredential.value).toBe(false) + expect(state.directory.canRefreshModels.value).toBe(false) + + state.apiKey.value = ' key ' + expect(state.directory.canRefreshModels.value).toBe(false) + + state.baseUrl.value = ' https://custom.example.com ' + expect(state.directory.canRefreshModels.value).toBe(true) + + state.apiKey.value = '' + state.directory.providers.value[0] = { + ...state.directory.providers.value[0], + auth_status: { connected: true }, + } + expect(state.directory.hasUsableCredential.value).toBe(true) + expect(state.directory.canRefreshModels.value).toBe(true) + + state.directory.providers.value[0] = { + ...state.directory.providers.value[0], + supports_model_refresh: false, + } + expect(state.directory.canRefreshModels.value).toBe(false) + }) + + it('没有选中 Provider 时不请求模型', async () => { + const state = mountDirectory() + + await expect(state.directory.loadModels(true)).resolves.toEqual([]) + expect(mocks.manageLlmProvider).not.toHaveBeenCalled() + expect(state.directory.loadingModels.value).toBe(false) + }) + + it('规范化模型刷新 payload,选择首个模型并同步认证与上下文元数据', async () => { + const state = mountDirectory({ + provider: ' openai ', + apiKey: ' secret ', + baseUrl: ' https://api.example.com/v1 ', + baseUrlPreset: ' official ', + useProxy: false, + userAgent: ' MoviePilot-Test ', + maxContextTokens: 64, + }) + state.directory.providers.value = [createProvider('openai')] + mocks.manageLlmProvider.mockResolvedValue({ + auth_status: { connected: true, label: 'demo@example.com' }, + models: [createModel('gpt-test', { context_tokens_k: 128 })], }) - const wrapper = mount(Harness) - await wrapper.vm.loadProviders() + await expect(state.directory.loadModels(true)).resolves.toEqual([ + createModel('gpt-test', { context_tokens_k: 128 }), + ]) - expect(wrapper.vm.showApiProtocolField).toBe(true) + expect(mocks.manageLlmProvider).toHaveBeenCalledWith('openai', 'list_models', { + api_key: 'secret', + base_url: 'https://api.example.com/v1', + base_url_preset: 'official', + use_proxy: false, + user_agent: 'MoviePilot-Test', + force_refresh: true, + }) + expect(state.model.value).toBe('gpt-test') + expect(state.maxContextTokens.value).toBe(128) + expect(state.authConnected.value).toBe(true) + expect(state.directory.selectedModel.value?.id).toBe('gpt-test') + expect(state.directory.loadingModels.value).toBe(false) + }) - wrapper.vm.selectProvider('deepseek') - await nextTick() + it('保留仍存在的模型选择,并仅从匹配模型回填上下文', async () => { + const state = mountDirectory({ provider: 'openai', model: 'model-b', maxContextTokens: 32 }) + state.directory.providers.value = [createProvider('openai')] + mocks.manageLlmProvider.mockResolvedValue({ + models: [createModel('model-a', { context_tokens_k: 64 }), createModel('model-b', { context_tokens_k: 256 })], + }) - expect(wrapper.vm.showApiProtocolField).toBe(false) + await state.directory.loadModels() - mocks.apiPost.mockResolvedValueOnce({ - success: true, - data: { - models: [ + expect(state.model.value).toBe('model-b') + expect(state.maxContextTokens.value).toBe(256) + expect(state.directory.applyModelMetadata('missing')).toBeNull() + }) + + it('将非数组模型响应规整为空列表,并在失败后恢复 loading', async () => { + const state = mountDirectory({ provider: 'openai' }) + state.directory.providers.value = [createProvider('openai')] + mocks.manageLlmProvider.mockResolvedValueOnce({ models: { id: 'invalid' } }) + + await expect(state.directory.loadModels()).resolves.toEqual([]) + expect(state.directory.models.value).toEqual([]) + + mocks.manageLlmProvider.mockRejectedValueOnce(new Error('model unavailable')) + await expect(state.directory.loadModels()).rejects.toThrow('model unavailable') + expect(state.directory.loadingModels.value).toBe(false) + }) + + it('只为 OpenAI 兼容 runtime 或要求 Responses 的工具模型显示协议字段', async () => { + const state = mountDirectory({ provider: 'openai' }) + state.directory.providers.value = [ + createProvider('openai'), + createProvider('deepseek', { runtime: 'deepseek' }), + createProvider('google', { runtime: 'google' }), + ] + + expect(state.directory.showApiProtocolField.value).toBe(true) + + state.provider.value = 'deepseek' + state.directory.models.value = [ + createModel('deepseek-v4-flash', { + server_tools: [ { - id: 'deepseek-v4-flash', - name: 'deepseek-v4-flash', - server_tools: [ - { - id: 'web_search', - required_api_protocol: 'responses', - client_adapter: 'openai_responses', - }, - ], + id: 'web_search', + required_api_protocol: 'responses', + client_adapter: 'openai_responses', }, ], - }, + }), + ] + state.model.value = 'deepseek-v4-flash' + await nextTick() + expect(state.directory.supportsBuiltinWebSearch.value).toBe(true) + expect(state.directory.showApiProtocolField.value).toBe(true) + + state.provider.value = 'google' + state.directory.models.value = [ + createModel('gemini-test', { + server_tools: [{ id: 'web_search', required_api_protocol: 'native', client_adapter: 'google_native' }], + }), + ] + state.model.value = 'gemini-test' + await nextTick() + expect(state.directory.supportsBuiltinWebSearch.value).toBe(true) + expect(state.directory.showApiProtocolField.value).toBe(false) + }) + + it('没有 Provider 时拒绝开始 OAuth 授权', async () => { + const state = mountDirectory() + + await expect(state.directory.startAuth('device')).rejects.toThrow('LLM provider is required') + expect(mocks.manageLlmProvider).not.toHaveBeenCalled() + }) + + it('开始 OAuth 授权后打开页面、显示弹窗并安排首次轮询', async () => { + vi.useFakeTimers() + const open = vi.spyOn(window, 'open').mockReturnValue(null) + const state = mountDirectory({ provider: 'openai' }) + state.directory.providers.value = [createProvider('openai')] + const session: LlmProviderAuthSession = { + session_id: 'session-1', + provider_id: 'openai', + flow_type: 'device', + status: 'pending', + authorize_url: 'https://auth.example.com/device', + } + mocks.manageLlmProvider.mockResolvedValue(session) + + await expect(state.directory.startAuth('device')).resolves.toEqual(session) + + expect(mocks.manageLlmProvider).toHaveBeenCalledWith('openai', 'start_auth', { method: 'device' }) + expect(open).toHaveBeenCalledWith( + 'https://auth.example.com/device', + '_blank', + 'noopener,noreferrer,width=960,height=780', + ) + expect(state.directory.authDialogVisible.value).toBe(true) + expect(state.directory.authPopupBlocked.value).toBe(true) + expect(vi.getTimerCount()).toBe(1) + }) + + it('优先打开 authorize URL,并回退到 verification URL', () => { + const open = vi.spyOn(window, 'open').mockReturnValue({} as Window) + const state = mountDirectory({ provider: 'openai' }) + + state.directory.openAuthPage() + expect(open).not.toHaveBeenCalled() + + state.directory.authSession.value = { + session_id: 'session-1', + provider_id: 'openai', + flow_type: 'device', + status: 'pending', + verification_url: 'https://auth.example.com/verify', + } + state.directory.openAuthPage() + + expect(open).toHaveBeenCalledWith( + 'https://auth.example.com/verify', + '_blank', + 'noopener,noreferrer,width=960,height=780', + ) + expect(state.directory.authPopupBlocked.value).toBe(false) + }) + + it('待授权时合并会话状态,并按服务端间隔安排下一次静默轮询', async () => { + vi.useFakeTimers() + const state = mountDirectory({ provider: 'openai' }) + state.directory.authSession.value = { + session_id: 'session-1', + provider_id: 'openai', + flow_type: 'device', + status: 'pending', + interval_seconds: 5, + } + mocks.manageLlmProvider.mockResolvedValue({ status: 'pending', interval_seconds: 0.5, message: 'waiting' }) + + await expect(state.directory.pollAuthSession()).resolves.toMatchObject({ + status: 'pending', + interval_seconds: 0.5, + message: 'waiting', }) - await wrapper.vm.loadModels() - wrapper.vm.selectModel('deepseek-v4-flash') - await nextTick() - expect(wrapper.vm.supportsBuiltinWebSearch).toBe(true) - expect(wrapper.vm.showApiProtocolField).toBe(true) + expect(mocks.manageLlmProvider).toHaveBeenCalledWith( + 'openai', + 'poll_auth', + { session_id: 'session-1' }, + { feedback: 'silent' }, + ) + expect(state.directory.authPolling.value).toBe(false) + expect(vi.getTimerCount()).toBe(1) - wrapper.vm.selectProvider('google') - await nextTick() - mocks.apiPost.mockResolvedValueOnce({ - success: true, - data: { - models: [ - { - id: 'gemini-3.6-flash-preview', - name: 'gemini-3.6-flash-preview', - server_tools: [ - { - id: 'web_search', - required_api_protocol: 'native', - client_adapter: 'google_native', - }, - ], - }, - ], - }, + await vi.advanceTimersByTimeAsync(999) + expect(mocks.manageLlmProvider).toHaveBeenCalledTimes(1) + + await vi.advanceTimersByTimeAsync(1) + expect(mocks.manageLlmProvider).toHaveBeenNthCalledWith( + 2, + 'openai', + 'poll_auth', + { session_id: 'session-1' }, + { feedback: 'silent' }, + ) + }) + + it('授权成功后刷新 Provider 与模型,并将模型请求标记为强制刷新', async () => { + const state = mountDirectory({ provider: 'openai' }) + state.directory.providers.value = [createProvider('openai')] + state.directory.authSession.value = { + session_id: 'session-1', + provider_id: 'openai', + flow_type: 'device', + status: 'pending', + } + mocks.manageLlmProvider.mockImplementation((_provider: string, action: string) => { + if (action === 'poll_auth') return Promise.resolve({ status: 'authorized' }) + if (action === 'list_providers') { + return Promise.resolve([createProvider('openai', { auth_status: { connected: true } })]) + } + if (action === 'list_models') return Promise.resolve({ models: [createModel('gpt-test')] }) + return Promise.reject(new Error(`unexpected action: ${action}`)) }) - await wrapper.vm.loadModels() - wrapper.vm.selectModel('gemini-3.6-flash-preview') - await nextTick() - expect(wrapper.vm.supportsBuiltinWebSearch).toBe(true) - expect(wrapper.vm.showApiProtocolField).toBe(false) - wrapper.unmount() + await expect(state.directory.pollAuthSession()).resolves.toMatchObject({ status: 'authorized' }) + + expect(mocks.manageLlmProvider).toHaveBeenNthCalledWith( + 1, + 'openai', + 'poll_auth', + { session_id: 'session-1' }, + { feedback: 'silent' }, + ) + expect(mocks.manageLlmProvider).toHaveBeenNthCalledWith(2, '', 'list_providers') + expect(mocks.manageLlmProvider).toHaveBeenNthCalledWith(3, 'openai', 'list_models', { + api_key: undefined, + base_url: undefined, + base_url_preset: undefined, + use_proxy: false, + user_agent: undefined, + force_refresh: true, + }) + expect(state.authConnected.value).toBe(true) + expect(state.model.value).toBe('gpt-test') + expect(state.directory.authPolling.value).toBe(false) + }) + + it('轮询失败后恢复 polling 状态且不安排下一次请求', async () => { + vi.useFakeTimers() + const state = mountDirectory({ provider: 'openai' }) + state.directory.authSession.value = { + session_id: 'session-1', + provider_id: 'openai', + flow_type: 'device', + status: 'pending', + } + mocks.manageLlmProvider.mockRejectedValue(new Error('poll failed')) + + await expect(state.directory.pollAuthSession()).rejects.toThrow('poll failed') + expect(state.directory.authPolling.value).toBe(false) + expect(vi.getTimerCount()).toBe(0) + }) + + it('断开认证后刷新 Provider,并在没有 Provider 时直接返回', async () => { + const emptyState = mountDirectory() + await expect(emptyState.directory.disconnectAuth()).resolves.toBe(false) + + const state = mountDirectory({ provider: 'openai' }) + state.directory.providers.value = [createProvider('openai', { auth_status: { connected: true } })] + mocks.manageLlmProvider.mockImplementation((_provider: string, action: string) => { + if (action === 'disconnect') return Promise.resolve(undefined) + if (action === 'list_providers') return Promise.resolve([createProvider('openai')]) + return Promise.reject(new Error(`unexpected action: ${action}`)) + }) + + await expect(state.directory.disconnectAuth()).resolves.toBe(true) + expect(mocks.manageLlmProvider).toHaveBeenNthCalledWith(1, 'openai', 'disconnect') + expect(mocks.manageLlmProvider).toHaveBeenNthCalledWith(2, '', 'list_providers') + expect(state.authConnected.value).toBe(false) + }) + + it('关闭授权弹窗和组件卸载都会清理轮询 timer', async () => { + vi.useFakeTimers() + const state = mountDirectory({ provider: 'openai' }) + state.directory.providers.value = [createProvider('openai')] + mocks.manageLlmProvider.mockResolvedValue({ + session_id: 'session-1', + provider_id: 'openai', + flow_type: 'device', + status: 'pending', + }) + + await state.directory.startAuth('device') + expect(vi.getTimerCount()).toBe(1) + + state.directory.closeAuthDialog() + expect(state.directory.authDialogVisible.value).toBe(false) + expect(vi.getTimerCount()).toBe(0) + + await state.directory.startAuth('device') + expect(vi.getTimerCount()).toBe(1) + state.wrapper.unmount() + expect(vi.getTimerCount()).toBe(0) }) }) diff --git a/vite.config.ts b/vite.config.ts index e8eb90f4..c13bc24c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -304,6 +304,7 @@ export default defineConfig(({ command, mode, isPreview }) => ({ unstubGlobals: true, coverage: { include: [ + 'src/api/manage.ts', 'src/utils/recommendSources.ts', 'src/utils/permission.ts', 'src/utils/pluginSidebarNav.ts', @@ -334,6 +335,7 @@ export default defineConfig(({ command, mode, isPreview }) => ({ 'src/views/subscribe/SubscribePopularView.vue', 'src/views/subscribe/SubscribeShareView.vue', 'src/composables/useMediaSubscribe.ts', + 'src/composables/useLlmProviderDirectory.ts', 'src/composables/useTorrentFilter.ts', 'src/components/cards/SubscribeCard.vue', 'src/components/cards/UserCard.vue', @@ -411,6 +413,18 @@ export default defineConfig(({ command, mode, isPreview }) => ({ functions: 85, lines: 85, statements: 85, + 'src/api/manage.ts': { + branches: 75, + functions: 80, + lines: 80, + statements: 80, + }, + 'src/composables/useLlmProviderDirectory.ts': { + branches: 85, + functions: 90, + lines: 90, + statements: 90, + }, 'src/components/cards/SubscribeCard.vue': { branches: 75, functions: 80,