From da4eb9c1b1791db3bd112a46a21ac6203f5ff3e5 Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 29 Dec 2025 16:54:06 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA=20fix:=20improve=20test=20infrastr?= =?UTF-8?q?ucture=20and=20mock=20configurations=20(#11028)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ๐Ÿงช fix: improve test infrastructure and mock configurations - Add vitest plugin to fix @lobehub/fluent-emoji style import issue - Update antd-style mocks to preserve actual exports while mocking specific functions - Switch from useClientDataSWR to useClientDataSWRWithSync in tests - Add @/utils/identifier alias in vitest config - Fix duplicate @lobehub/ui mock in ComfyUIForm test * ๐Ÿ› fix: use recommended-legacy for ESLint 8 compatibility The @next/eslint-plugin-next v16 changed to flat config format which is incompatible with ESLint 8. Using recommended-legacy to maintain compatibility. --- .eslintrc.js | 2 +- .../(main)/home/features/InputArea/index.tsx | 2 +- .../__tests__/ComfyUIForm.test.tsx | 40 ++++++---- .../store/slices/data/action.test.ts | 18 ++--- .../User/__tests__/PanelContent.test.tsx | 2 +- .../User/__tests__/UserAvatar.test.tsx | 3 +- src/server/manifest.test.ts | 2 +- .../modules/AssistantStore/index.test.ts | 2 +- .../__tests__/serverMessagesEngine.test.ts | 8 +- .../routers/lambda/__tests__/user.test.ts | 2 +- src/server/routers/lambda/aiAgent.ts | 78 ++++++++++--------- src/server/services/discover/index.test.ts | 15 +++- src/server/services/document/index.ts | 2 +- .../chat/mecha/agentConfigResolver.test.ts | 4 +- .../chat/mecha/agentConfigResolver.ts | 18 ++--- .../chat/mecha/contextEngineering.test.ts | 29 ++++--- .../agent/slices/knowledge/action.test.ts | 4 +- src/store/agentGroup/slices/curd.test.ts | 13 +--- src/store/agentGroup/slices/member.test.ts | 13 +--- .../aiInfra/slices/aiModel/action.test.ts | 7 +- src/store/chat/slices/plugin/action.test.ts | 8 +- .../home/slices/sidebarUI/action.test.ts | 21 ++--- .../slices/generationBatch/action.test.ts | 8 +- .../session/slices/session/action.test.ts | 2 +- .../__tests__/lobe-web-browsing.test.ts | 1 + src/utils/textLength.ts | 16 ++-- vitest.config.mts | 36 ++++++++- 27 files changed, 206 insertions(+), 150 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 556809bf81..5882206875 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,7 +1,7 @@ const config = require('@lobehub/lint').eslint; config.root = true; -config.extends.push('plugin:@next/next/recommended'); +config.extends.push('plugin:@next/next/recommended-legacy'); config.rules['unicorn/no-negated-condition'] = 0; config.rules['unicorn/prefer-type-error'] = 0; diff --git a/src/app/[variants]/(main)/home/features/InputArea/index.tsx b/src/app/[variants]/(main)/home/features/InputArea/index.tsx index 628da2cb21..9414d2d842 100644 --- a/src/app/[variants]/(main)/home/features/InputArea/index.tsx +++ b/src/app/[variants]/(main)/home/features/InputArea/index.tsx @@ -1,5 +1,5 @@ import { Flexbox } from '@lobehub/ui'; -import { memo, useMemo } from 'react'; +import { useMemo } from 'react'; import DragUploadZone, { useUploadFiles } from '@/components/DragUploadZone'; import { type ActionKeys, ChatInputProvider, DesktopChatInput } from '@/features/ChatInput'; diff --git a/src/components/InvalidAPIKey/__tests__/ComfyUIForm.test.tsx b/src/components/InvalidAPIKey/__tests__/ComfyUIForm.test.tsx index a3fef51c67..f25ff85ac1 100644 --- a/src/components/InvalidAPIKey/__tests__/ComfyUIForm.test.tsx +++ b/src/components/InvalidAPIKey/__tests__/ComfyUIForm.test.tsx @@ -18,12 +18,23 @@ vi.mock('react-i18next', () => ({ }), })); -vi.mock('antd-style', () => ({ - useTheme: () => ({ - colorTextSecondary: '#999', - }), - createStyles: vi.fn(() => () => ({ styles: {} })), -})); +vi.mock('antd-style', async (importOriginal) => { + const actual = await importOriginal(); + + return { + ...actual, + createStaticStyles: vi.fn((fn: any) => + fn({ + css: () => '', + cssVar: {}, + }), + ), + createStyles: vi.fn(() => () => ({ styles: {} })), + useTheme: () => ({ + colorTextSecondary: '#999', + }), + }; +}); vi.mock('@/components/FormInput', () => ({ FormInput: vi.fn(({ value, onChange, ...props }) => ( @@ -49,6 +60,13 @@ vi.mock('@/components/KeyValueEditor', () => ({ default: vi.fn(() =>
Key-Value Editor
), })); +vi.mock('@lobehub/icons', () => ({ + ComfyUI: { + Combine: vi.fn(() =>
ComfyUI Icon
), + }, + ProviderIcon: vi.fn(() =>
Provider Icon
), +})); + vi.mock('@lobehub/ui', () => ({ Icon: vi.fn(({ icon, ...props }) => (
@@ -75,16 +93,6 @@ vi.mock('@lobehub/ui', () => ({ )), ProviderIcon: vi.fn(() =>
Provider Icon
), -})); - -vi.mock('@lobehub/icons', () => ({ - ComfyUI: { - Combine: vi.fn(() =>
ComfyUI Icon
), - }, - ProviderIcon: vi.fn(() =>
Provider Icon
), -})); - -vi.mock('@lobehub/ui', () => ({ Center: vi.fn(({ children, ...props }) => (
{children} diff --git a/src/features/Conversation/store/slices/data/action.test.ts b/src/features/Conversation/store/slices/data/action.test.ts index e6c0770ff3..147adb7680 100644 --- a/src/features/Conversation/store/slices/data/action.test.ts +++ b/src/features/Conversation/store/slices/data/action.test.ts @@ -2,7 +2,7 @@ import { UIChatMessage } from '@lobechat/types'; import { act, waitFor } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import { useClientDataSWR } from '@/libs/swr'; +import { useClientDataSWRWithSync } from '@/libs/swr'; import { messageService } from '@/services/message'; import { createStore } from '../../index'; @@ -31,14 +31,14 @@ vi.mock('@/services/message', () => ({ // Mock SWR vi.mock('@/libs/swr', () => ({ - useClientDataSWR: vi.fn((key, fetcher, options) => { + useClientDataSWRWithSync: vi.fn((key, fetcher, options) => { // Simulate SWR behavior for testing if (key) { - // Execute fetcher and call onSuccess - fetcher?.(key).then((data: UIChatMessage[]) => { - options?.onSuccess?.(data); + fetcher?.().then((data: UIChatMessage[]) => { + options?.onData?.(data); }); } + return { data: undefined, isLoading: true }; }), })); @@ -506,7 +506,7 @@ describe('DataSlice', () => { }); // SWR should be called with null key (disabled) - expect(vi.mocked(useClientDataSWR)).toHaveBeenCalledWith( + expect(vi.mocked(useClientDataSWRWithSync)).toHaveBeenCalledWith( null, expect.any(Function), expect.any(Object), @@ -524,7 +524,7 @@ describe('DataSlice', () => { threadId: 'thread-1', }); - const firstCallKey = vi.mocked(useClientDataSWR).mock.calls[0][0]; + const firstCallKey = vi.mocked(useClientDataSWRWithSync).mock.calls[0][0]; const store2 = createStore({ context: { agentId: 'session-1', topicId: 'topic-1', threadId: 'thread-2' }, @@ -536,7 +536,7 @@ describe('DataSlice', () => { threadId: 'thread-2', }); - const secondCallKey = vi.mocked(useClientDataSWR).mock.calls[1][0]; + const secondCallKey = vi.mocked(useClientDataSWRWithSync).mock.calls[1][0]; // Keys should be different because threadIds are different expect(firstCallKey).not.toEqual(secondCallKey); @@ -555,7 +555,7 @@ describe('DataSlice', () => { threadId: 'test-thread', }); - const swrKey = vi.mocked(useClientDataSWR).mock.calls[0][0] as any[]; + const swrKey = vi.mocked(useClientDataSWRWithSync).mock.calls[0][0] as any[]; // Key should be an array with prefix and context object expect(Array.isArray(swrKey)).toBe(true); diff --git a/src/features/User/__tests__/PanelContent.test.tsx b/src/features/User/__tests__/PanelContent.test.tsx index 36e280c7a7..b843f07978 100644 --- a/src/features/User/__tests__/PanelContent.test.tsx +++ b/src/features/User/__tests__/PanelContent.test.tsx @@ -169,6 +169,6 @@ describe('PanelContent', () => { it('should render Menu with main items', () => { renderWithRouter(); - expect(screen.getByText('Mocked Menu')).toBeInTheDocument(); + expect(screen.getAllByText('Mocked Menu').length).toBeGreaterThan(0); }); }); diff --git a/src/features/User/__tests__/UserAvatar.test.tsx b/src/features/User/__tests__/UserAvatar.test.tsx index 9a09a356ec..b461d954b2 100644 --- a/src/features/User/__tests__/UserAvatar.test.tsx +++ b/src/features/User/__tests__/UserAvatar.test.tsx @@ -66,7 +66,8 @@ describe('UserAvatar', () => { }); render(); - expect(screen.getByAltText('testuser')).toHaveAttribute('src', DEFAULT_USER_AVATAR_URL); + // When user has no avatar url, falls back to initials rendering (not an ) + expect(screen.getByText('TE')).toBeInTheDocument(); }); it('should show LobeChat and default avatar when the user is not logged in and enable auth', () => { diff --git a/src/server/manifest.test.ts b/src/server/manifest.test.ts index dfd829dfb9..d9ed901f80 100644 --- a/src/server/manifest.test.ts +++ b/src/server/manifest.test.ts @@ -150,7 +150,7 @@ describe('Manifest', () => { immutable: 'true', max_age: 31536000, sizes: '1280x676', - src: 'https://example.com/logo.png?v=1', + src: 'https://example.com/screenshot.png?v=1', type: 'image/png', }); }); diff --git a/src/server/modules/AssistantStore/index.test.ts b/src/server/modules/AssistantStore/index.test.ts index 89e8e39db3..97e1a54b78 100644 --- a/src/server/modules/AssistantStore/index.test.ts +++ b/src/server/modules/AssistantStore/index.test.ts @@ -6,7 +6,7 @@ import { AssistantStore } from './index'; const baseURL = 'https://registry.npmmirror.com/@lobehub/agents-index/v1/files/public'; -vi.mock('@/server/modules/EdgeConfig', () => { +vi.mock('@lobechat/edge-config', () => { const EdgeConfigMock = vi.fn(); // @ts-expect-error: static mock for isEnabled EdgeConfigMock.isEnabled = vi.fn(); diff --git a/src/server/modules/Mecha/ContextEngineering/__tests__/serverMessagesEngine.test.ts b/src/server/modules/Mecha/ContextEngineering/__tests__/serverMessagesEngine.test.ts index 4c2eefdb86..bc3bec6615 100644 --- a/src/server/modules/Mecha/ContextEngineering/__tests__/serverMessagesEngine.test.ts +++ b/src/server/modules/Mecha/ContextEngineering/__tests__/serverMessagesEngine.test.ts @@ -237,9 +237,11 @@ describe('serverMessagesEngine', () => { }, }); - // Should have user memory in system message - const systemMessages = result.filter((m) => m.role === 'system'); - expect(systemMessages.length).toBeGreaterThan(0); + // User memories are injected as a consolidated user message before the first user message + // Note: meta/id fields are removed by the engine cleanup step, so assert via content. + const injection = result.find((m: any) => m.role === 'user' && String(m.content).includes('')); + expect(injection).toBeDefined(); + expect(injection!.role).toBe('user'); }); it('should skip user memory when memories is undefined', async () => { diff --git a/src/server/routers/lambda/__tests__/user.test.ts b/src/server/routers/lambda/__tests__/user.test.ts index bca25821bb..d4fa5c5c4e 100644 --- a/src/server/routers/lambda/__tests__/user.test.ts +++ b/src/server/routers/lambda/__tests__/user.test.ts @@ -118,7 +118,7 @@ describe('userRouter', () => { const result = await userRouter.createCaller({ ...mockCtx }).getUserState(); - expect(result).toEqual({ + expect(result).toMatchObject({ isOnboard: true, preference: { telemetry: true }, settings: {}, diff --git a/src/server/routers/lambda/aiAgent.ts b/src/server/routers/lambda/aiAgent.ts index 211e926902..9458c6dc14 100644 --- a/src/server/routers/lambda/aiAgent.ts +++ b/src/server/routers/lambda/aiAgent.ts @@ -460,6 +460,45 @@ export const aiAgentRouter = router({ } }), + + getOperationStatus: aiAgentProcedure + .input(GetOperationStatusSchema) + .query(async ({ input, ctx }) => { + const { historyLimit, includeHistory, operationId } = input; + + if (!operationId) { + throw new Error('operationId parameter is required'); + } + + log('Getting operation status for %s', operationId); + + // Get operation status using AgentRuntimeService + const operationStatus = await ctx.agentRuntimeService.getOperationStatus({ + historyLimit, + includeHistory, + operationId, + }); + + return operationStatus; + }), + + +getPendingInterventions: aiAgentProcedure + .input(GetPendingInterventionsSchema) + .query(async ({ input, ctx }) => { + const { operationId, userId } = input; + + log('Getting pending interventions for operationId: %s, userId: %s', operationId, userId); + + // Get pending interventions using AgentRuntimeService + const result = await ctx.agentRuntimeService.getPendingInterventions({ + operationId: operationId || undefined, + userId: userId || undefined, + }); + + return result; + }), + /** * Get SubAgent task execution status * @@ -474,7 +513,7 @@ export const aiAgentRouter = router({ * As a workaround, this endpoint also updates Thread metadata from Redis * when real-time status is available. */ - getSubAgentTaskStatus: aiAgentProcedure +getSubAgentTaskStatus: aiAgentProcedure .input( z.object({ /** Thread ID */ @@ -688,43 +727,6 @@ export const aiAgentRouter = router({ return result; }), - getOperationStatus: aiAgentProcedure - .input(GetOperationStatusSchema) - .query(async ({ input, ctx }) => { - const { historyLimit, includeHistory, operationId } = input; - - if (!operationId) { - throw new Error('operationId parameter is required'); - } - - log('Getting operation status for %s', operationId); - - // Get operation status using AgentRuntimeService - const operationStatus = await ctx.agentRuntimeService.getOperationStatus({ - historyLimit, - includeHistory, - operationId, - }); - - return operationStatus; - }), - - getPendingInterventions: aiAgentProcedure - .input(GetPendingInterventionsSchema) - .query(async ({ input, ctx }) => { - const { operationId, userId } = input; - - log('Getting pending interventions for operationId: %s, userId: %s', operationId, userId); - - // Get pending interventions using AgentRuntimeService - const result = await ctx.agentRuntimeService.getPendingInterventions({ - operationId: operationId || undefined, - userId: userId || undefined, - }); - - return result; - }), - /** * Interrupt a running task * diff --git a/src/server/services/discover/index.test.ts b/src/server/services/discover/index.test.ts index e193afa6ee..c349c19364 100644 --- a/src/server/services/discover/index.test.ts +++ b/src/server/services/discover/index.test.ts @@ -622,8 +622,14 @@ describe('DiscoverService', () => { it('should filter by search query', async () => { const result = await service.getProviderList({ q: 'openai' }); - expect(result.items).toHaveLength(1); - expect(result.items[0].identifier).toBe('openai'); + expect(result.items.length).toBeGreaterThan(0); + expect(result.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + identifier: 'openai', + }), + ]), + ); }); it('should sort by model count', async () => { @@ -632,7 +638,10 @@ describe('DiscoverService', () => { order: 'desc', }); - expect(result.items).toHaveLength(2); + expect(result.items.length).toBeGreaterThan(0); + for (let i = 1; i < result.items.length; i++) { + expect(result.items[i - 1].modelCount).toBeGreaterThanOrEqual(result.items[i].modelCount); + } }); }); diff --git a/src/server/services/document/index.ts b/src/server/services/document/index.ts index 8187e125b2..9810a490a5 100644 --- a/src/server/services/document/index.ts +++ b/src/server/services/document/index.ts @@ -250,7 +250,7 @@ export class DocumentService { // Clean up content - remove tags if present let cleanContent = fileDocument.content; if (cleanContent.includes(']*>([\s\S]*?)<\/page>/g, '$1').trim(); + cleanContent = cleanContent.replaceAll(/]*>([\S\s]*?)<\/page>/g, '$1').trim(); } const document = await this.documentModel.create({ diff --git a/src/services/chat/mecha/agentConfigResolver.test.ts b/src/services/chat/mecha/agentConfigResolver.test.ts index a3c1519f47..69f140fd07 100644 --- a/src/services/chat/mecha/agentConfigResolver.test.ts +++ b/src/services/chat/mecha/agentConfigResolver.test.ts @@ -563,7 +563,7 @@ describe('resolveAgentConfig', () => { // Should still inject PageAgentIdentifier but with empty systemRole expect(result.plugins).toContain(PageAgentIdentifier); - expect(result.agentConfig.systemRole).toBe('You are a helpful assistant'); + expect(result.agentConfig.systemRole.trim()).toBe('You are a helpful assistant'); expect(result.chatConfig.enableHistoryCount).toBe(false); }); @@ -579,7 +579,7 @@ describe('resolveAgentConfig', () => { }); expect(result.plugins).toContain(PageAgentIdentifier); - expect(result.agentConfig.systemRole).toBe('You are a helpful assistant'); + expect(result.agentConfig.systemRole.trim()).toBe('You are a helpful assistant'); expect(result.chatConfig.enableHistoryCount).toBe(false); }); }); diff --git a/src/services/chat/mecha/agentConfigResolver.ts b/src/services/chat/mecha/agentConfigResolver.ts index a7a3a2a848..58251ed872 100644 --- a/src/services/chat/mecha/agentConfigResolver.ts +++ b/src/services/chat/mecha/agentConfigResolver.ts @@ -44,20 +44,20 @@ export interface AgentConfigResolverContext { /** Agent ID to resolve config for */ agentId: string; - /** Message map scope (e.g., 'page', 'main', 'thread') */ - scope?: MessageMapScope; - // Builtin agent specific context - /** Document content for page-agent */ +/** Document content for page-agent */ documentContent?: string; + /** Current model being used (for template variables) */ model?: string; - /** Plugins enabled for the agent */ plugins?: string[]; /** Current provider */ provider?: string; + + /** Message map scope (e.g., 'page', 'main', 'thread') */ + scope?: MessageMapScope; /** Target agent config for agent-builder */ targetAgentConfig?: LobeAgentConfig; } @@ -98,8 +98,8 @@ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAge // Debug logging for page editor console.log('[agentConfigResolver] Resolving agent config:', { agentId, - scope: ctx.scope, plugins, + scope: ctx.scope, }); const agentStoreState = getAgentStoreState(); @@ -114,7 +114,7 @@ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAge // Check if this is a builtin agent const slug = agentSelectors.getAgentSlugById(agentId)(agentStoreState); - console.log('[agentConfigResolver] Agent type check:', { slug, isBuiltin: !!slug }); + console.log('[agentConfigResolver] Agent type check:', { isBuiltin: !!slug, slug }); if (!slug) { console.log('[agentConfigResolver] Taking CUSTOM AGENT branch'); @@ -157,9 +157,9 @@ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAge }; console.log('[agentConfigResolver] Page-agent injection complete:', { + chatConfig: finalChatConfig, plugins: pageAgentPlugins, systemRoleLength: mergedSystemRole.length, - chatConfig: finalChatConfig, }); return { @@ -252,8 +252,8 @@ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAge }; console.log('[agentConfigResolver] Page-agent injection complete for builtin agent:', { - slug, plugins: finalPlugins, + slug, systemRoleLength: resolvedSystemRole.length, }); } diff --git a/src/services/chat/mecha/contextEngineering.test.ts b/src/services/chat/mecha/contextEngineering.test.ts index 8b4d484e5d..169115c860 100644 --- a/src/services/chat/mecha/contextEngineering.test.ts +++ b/src/services/chat/mecha/contextEngineering.test.ts @@ -446,6 +446,14 @@ describe('contextEngineering', () => { meta: {}, updatedAt: Date.now(), }, + { + role: 'user', + content: 'Hello', + createdAt: Date.now(), + id: 'memory-placeholder-user', + meta: {}, + updatedAt: Date.now(), + }, ]; // Mock topic memories and global identities separately @@ -481,19 +489,20 @@ describe('contextEngineering', () => { provider: 'openai', }); + // Keep the original system message as-is expect(result[0].role).toBe('system'); - // Check the memory context is injected (memory_fetched_at is optional now) - expect(result[0].content).toContain('LobeHubWeekly syncs for LobeHub', - ); - expect(result[0].content).toContain('LobeHub'); - expect(result[1].content).toBe( + expect(result[0].content).toBe( 'Memory load: available={{memory_available}}, total contexts={{memory_contexts_count}}\n{{memory_summary}}', ); + + // Memory context is injected as a consolidated user message before the first user message + // Note: meta/id fields are removed by the engine cleanup step, so assert via content. + const injection = result.find((m: any) => m.role === 'user' && String(m.content).includes('')); + expect(injection).toBeDefined(); + expect(injection!.role).toBe('user'); + expect(injection!.content).toContain(''); + expect(injection!.content).toContain(''); + expect(injection!.content).toContain(''); }); it('should handle missing placeholder variables gracefully', async () => { diff --git a/src/store/agent/slices/knowledge/action.test.ts b/src/store/agent/slices/knowledge/action.test.ts index d22cfb3ec5..bdae595f40 100644 --- a/src/store/agent/slices/knowledge/action.test.ts +++ b/src/store/agent/slices/knowledge/action.test.ts @@ -279,7 +279,7 @@ describe('KnowledgeSlice Actions', () => { useAgentStore.setState({ activeAgentId: 'agent-1' }); }); - const { result } = renderHook(() => useAgentStore().useFetchFilesAndKnowledgeBases(), { + const { result } = renderHook(() => useAgentStore().useFetchFilesAndKnowledgeBases('agent-1'), { wrapper: withSWR, }); @@ -295,7 +295,7 @@ describe('KnowledgeSlice Actions', () => { useAgentStore.setState({ activeAgentId: 'agent-1' }); }); - const { result } = renderHook(() => useAgentStore().useFetchFilesAndKnowledgeBases(), { + const { result } = renderHook(() => useAgentStore().useFetchFilesAndKnowledgeBases('agent-1'), { wrapper: withSWR, }); diff --git a/src/store/agentGroup/slices/curd.test.ts b/src/store/agentGroup/slices/curd.test.ts index 90c167ab08..50166b854e 100644 --- a/src/store/agentGroup/slices/curd.test.ts +++ b/src/store/agentGroup/slices/curd.test.ts @@ -3,6 +3,7 @@ import { act, renderHook } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { DEFAULT_CHAT_GROUP_CHAT_CONFIG } from '@/const/settings'; +import { mutate } from '@/libs/swr'; import { chatGroupService } from '@/services/chatGroup'; import { useAgentGroupStore } from '../store'; @@ -14,12 +15,9 @@ vi.mock('@/services/chatGroup', () => ({ }, })); -vi.mock('swr', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...(actual as any), - mutate: vi.fn().mockResolvedValue(undefined), - }; +vi.mock('@/libs/swr', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, mutate: vi.fn().mockResolvedValue(undefined) }; }); // Helper to create mock AgentGroupDetail @@ -70,7 +68,6 @@ describe('ChatGroupCurdSlice', () => { }); it('should refresh group detail after update', async () => { - const { mutate } = await import('swr'); vi.mocked(chatGroupService.updateGroup).mockResolvedValue({} as any); const { result } = renderHook(() => useAgentGroupStore()); @@ -119,7 +116,6 @@ describe('ChatGroupCurdSlice', () => { }); it('should refresh group detail after config update', async () => { - const { mutate } = await import('swr'); vi.mocked(chatGroupService.updateGroup).mockResolvedValue({} as any); const { result } = renderHook(() => useAgentGroupStore()); @@ -166,7 +162,6 @@ describe('ChatGroupCurdSlice', () => { }); it('should refresh group detail after meta update', async () => { - const { mutate } = await import('swr'); vi.mocked(chatGroupService.updateGroup).mockResolvedValue({} as any); const { result } = renderHook(() => useAgentGroupStore()); diff --git a/src/store/agentGroup/slices/member.test.ts b/src/store/agentGroup/slices/member.test.ts index 7249bd27ed..6de0b20b00 100644 --- a/src/store/agentGroup/slices/member.test.ts +++ b/src/store/agentGroup/slices/member.test.ts @@ -1,6 +1,7 @@ import { act, renderHook } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { mutate } from '@/libs/swr'; import { chatGroupService } from '@/services/chatGroup'; import { useAgentGroupStore } from '../store'; @@ -14,12 +15,9 @@ vi.mock('@/services/chatGroup', () => ({ }, })); -vi.mock('swr', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...(actual as any), - mutate: vi.fn().mockResolvedValue(undefined), - }; +vi.mock('@/libs/swr', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, mutate: vi.fn().mockResolvedValue(undefined) }; }); describe('ChatGroupMemberSlice', () => { @@ -56,7 +54,6 @@ describe('ChatGroupMemberSlice', () => { }); it('should refresh group detail after adding agents', async () => { - const { mutate } = await import('swr'); vi.mocked(chatGroupService.addAgentsToGroup).mockResolvedValue({ added: [], existing: [] }); const { result } = renderHook(() => useAgentGroupStore()); @@ -86,7 +83,6 @@ describe('ChatGroupMemberSlice', () => { }); it('should refresh group detail after removing agent', async () => { - const { mutate } = await import('swr'); vi.mocked(chatGroupService.removeAgentsFromGroup).mockResolvedValue({ deletedVirtualAgentIds: [], removedFromGroup: 1, @@ -125,7 +121,6 @@ describe('ChatGroupMemberSlice', () => { }); it('should refresh group detail after reordering', async () => { - const { mutate } = await import('swr'); vi.mocked(chatGroupService.updateAgentInGroup).mockResolvedValue({} as any); const { result } = renderHook(() => useAgentGroupStore()); diff --git a/src/store/aiInfra/slices/aiModel/action.test.ts b/src/store/aiInfra/slices/aiModel/action.test.ts index 7b90729a1d..e12e57ccca 100644 --- a/src/store/aiInfra/slices/aiModel/action.test.ts +++ b/src/store/aiInfra/slices/aiModel/action.test.ts @@ -1,18 +1,17 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import { AiProviderModelListItem } from 'model-bank'; -import { mutate } from 'swr'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { withSWR } from '~test-utils'; +import { mutate } from '@/libs/swr'; import { aiModelService } from '@/services/aiModel'; import { useAiInfraStore as useStore } from '../../store'; vi.mock('zustand/traditional'); -// Mock SWR -vi.mock('swr', async () => { - const actual = await vi.importActual('swr'); +vi.mock('@/libs/swr', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, mutate: vi.fn(), diff --git a/src/store/chat/slices/plugin/action.test.ts b/src/store/chat/slices/plugin/action.test.ts index 6e9365c3c6..51252ceffc 100644 --- a/src/store/chat/slices/plugin/action.test.ts +++ b/src/store/chat/slices/plugin/action.test.ts @@ -64,6 +64,9 @@ describe('ChatPluginAction', () => { expect(internal_execAgentRuntimeMock).toHaveBeenCalledWith( expect.objectContaining({ + context: expect.objectContaining({ + agentId: 'session-id', + }), messages: [ { role: 'assistant', @@ -73,9 +76,6 @@ describe('ChatPluginAction', () => { id: toolMessage.id, content: toolMessage.content, role: 'assistant', - meta: expect.objectContaining({ - backgroundColor: 'rgba(0,0,0,0)', - }), }), ], parentMessageId: messageId, @@ -705,7 +705,7 @@ describe('ChatPluginAction', () => { error: ['Invalid setting'], message: '[plugin] your settings is invalid with plugin manifest setting schema', }, - message: undefined, + message: 'response.PluginSettingsInvalid', type: 'PluginSettingsInvalid', }); diff --git a/src/store/home/slices/sidebarUI/action.test.ts b/src/store/home/slices/sidebarUI/action.test.ts index b2cbcfe062..03aa7259a9 100644 --- a/src/store/home/slices/sidebarUI/action.test.ts +++ b/src/store/home/slices/sidebarUI/action.test.ts @@ -4,6 +4,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { INBOX_SESSION_ID } from '@/const/session'; import { agentService } from '@/services/agent'; import { chatGroupService } from '@/services/chatGroup'; +import { homeService } from '@/services/home'; import { sessionService } from '@/services/session'; import { useHomeStore } from '@/store/home'; import { getSessionStoreState } from '@/store/session'; @@ -129,7 +130,8 @@ describe('createSidebarUISlice', () => { await result.current.removeAgent(mockAgentId); }); - expect(mockSwitchSession).toHaveBeenCalledWith(INBOX_SESSION_ID); + // removeAgent only removes and refreshes the agent list; session switching is handled in SessionStore.removeSession + expect(mockSwitchSession).not.toHaveBeenCalledWith(INBOX_SESSION_ID); }); }); @@ -192,8 +194,11 @@ describe('createSidebarUISlice', () => { await result.current.duplicateAgent(mockAgentId); }); - // In test environment, t() returns undefined, so fallback to 'Copy' - expect(sessionService.cloneSession).toHaveBeenCalledWith(mockAgentId, 'Copy'); + // default title is i18n based + expect(sessionService.cloneSession).toHaveBeenCalledWith( + mockAgentId, + expect.stringContaining('Copy'), + ); }); }); @@ -201,7 +206,7 @@ describe('createSidebarUISlice', () => { it('should update agent group and refresh agent list', async () => { const mockAgentId = 'agent-123'; const mockGroupId = 'group-456'; - vi.spyOn(sessionService, 'updateSession').mockResolvedValueOnce(undefined as any); + vi.spyOn(homeService, 'updateAgentSessionGroupId').mockResolvedValueOnce(undefined as any); const spyOnRefresh = vi.spyOn(useHomeStore.getState(), 'refreshAgentList'); const { result } = renderHook(() => useHomeStore()); @@ -210,15 +215,13 @@ describe('createSidebarUISlice', () => { await result.current.updateAgentGroup(mockAgentId, mockGroupId); }); - expect(sessionService.updateSession).toHaveBeenCalledWith(mockAgentId, { - group: mockGroupId, - }); + expect(homeService.updateAgentSessionGroupId).toHaveBeenCalledWith(mockAgentId, mockGroupId); expect(spyOnRefresh).toHaveBeenCalled(); }); it('should set group to default when groupId is null', async () => { const mockAgentId = 'agent-123'; - vi.spyOn(sessionService, 'updateSession').mockResolvedValueOnce(undefined as any); + vi.spyOn(homeService, 'updateAgentSessionGroupId').mockResolvedValueOnce(undefined as any); vi.spyOn(useHomeStore.getState(), 'refreshAgentList'); const { result } = renderHook(() => useHomeStore()); @@ -227,7 +230,7 @@ describe('createSidebarUISlice', () => { await result.current.updateAgentGroup(mockAgentId, null); }); - expect(sessionService.updateSession).toHaveBeenCalledWith(mockAgentId, { group: 'default' }); + expect(homeService.updateAgentSessionGroupId).toHaveBeenCalledWith(mockAgentId, null); }); }); diff --git a/src/store/image/slices/generationBatch/action.test.ts b/src/store/image/slices/generationBatch/action.test.ts index 89b3f1edd8..8caaae10a8 100644 --- a/src/store/image/slices/generationBatch/action.test.ts +++ b/src/store/image/slices/generationBatch/action.test.ts @@ -1,8 +1,8 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import React from 'react'; -import { mutate } from 'swr'; import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { mutate } from '@/libs/swr'; import { generationService } from '@/services/generation'; import { generationBatchService } from '@/services/generationBatch'; import { useImageStore } from '@/store/image'; @@ -25,10 +25,10 @@ vi.mock('@/services/generationBatch', () => ({ }, })); -vi.mock('swr', async () => { - const actual = await vi.importActual('swr'); +vi.mock('@/libs/swr', async (importOriginal) => { + const actual = await importOriginal(); return { - ...(actual as any), + ...actual, mutate: vi.fn(), }; }); diff --git a/src/store/session/slices/session/action.test.ts b/src/store/session/slices/session/action.test.ts index ad9d90d8fc..885e0a6425 100644 --- a/src/store/session/slices/session/action.test.ts +++ b/src/store/session/slices/session/action.test.ts @@ -123,7 +123,7 @@ describe('SessionAction', () => { }); expect(message.loading).toHaveBeenCalled(); - expect(sessionService.cloneSession).toHaveBeenCalledWith(sessionId, undefined); + expect(sessionService.cloneSession).toHaveBeenCalledWith(sessionId, expect.any(String)); }); }); diff --git a/src/store/tool/slices/builtin/executors/__tests__/lobe-web-browsing.test.ts b/src/store/tool/slices/builtin/executors/__tests__/lobe-web-browsing.test.ts index fad3ebe0b5..d8b218fe43 100644 --- a/src/store/tool/slices/builtin/executors/__tests__/lobe-web-browsing.test.ts +++ b/src/store/tool/slices/builtin/executors/__tests__/lobe-web-browsing.test.ts @@ -161,6 +161,7 @@ describe('WebBrowsingExecutor', () => { { data: { title: 'Page 1', content: 'Content 1', url: 'https://example1.com' } }, { data: { title: 'Page 2', content: 'Content 2', url: 'https://example2.com' } }, ], + savedDocuments: [], }; mockCrawlPages.mockResolvedValue(mockResponse); diff --git a/src/utils/textLength.ts b/src/utils/textLength.ts index 2775a73bbd..9b6a63d828 100644 --- a/src/utils/textLength.ts +++ b/src/utils/textLength.ts @@ -7,21 +7,21 @@ const isCJKChar = (char: string): boolean => { return ( // CJK Unified Ideographs - (code >= 0x4e00 && code <= 0x9fff) || + (code >= 0x4E_00 && code <= 0x9F_FF) || // CJK Unified Ideographs Extension A - (code >= 0x3400 && code <= 0x4dbf) || + (code >= 0x34_00 && code <= 0x4D_BF) || // CJK Compatibility Ideographs - (code >= 0xf900 && code <= 0xfaff) || + (code >= 0xF9_00 && code <= 0xFA_FF) || // Hiragana - (code >= 0x3040 && code <= 0x309f) || + (code >= 0x30_40 && code <= 0x30_9F) || // Katakana - (code >= 0x30a0 && code <= 0x30ff) || + (code >= 0x30_A0 && code <= 0x30_FF) || // Hangul Syllables - (code >= 0xac00 && code <= 0xd7af) || + (code >= 0xAC_00 && code <= 0xD7_AF) || // Hangul Jamo - (code >= 0x1100 && code <= 0x11ff) || + (code >= 0x11_00 && code <= 0x11_FF) || // CJK Unified Ideographs Extension B-F - (code >= 0x20000 && code <= 0x2ebef) + (code >= 0x2_00_00 && code <= 0x2_EB_EF) ); }; diff --git a/vitest.config.mts b/vitest.config.mts index 83a8b66296..81e6770674 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -1,7 +1,38 @@ -import { join, resolve } from 'node:path'; +import { dirname, join, resolve } from 'node:path'; import { coverageConfigDefaults, defineConfig } from 'vitest/config'; export default defineConfig({ + plugins: [ + /** + * @lobehub/fluent-emoji@4.0.0 ships `es/FluentEmoji/style.js` but its `es/FluentEmoji/index.js` + * imports `./style/index.js` which doesn't exist. + * + * In app bundlers this can be tolerated/rewritten, but Vite/Vitest resolves it strictly and + * fails the whole test run. Redirect it to the real file. + */ + { + enforce: 'pre', + name: 'fix-lobehub-fluent-emoji-style-import', + resolveId(id, importer) { + if (!importer) return null; + + const isFluentEmojiEntry = + importer.endsWith('/@lobehub/fluent-emoji/es/FluentEmoji/index.js') || + importer.includes('/@lobehub/fluent-emoji/es/FluentEmoji/index.js?'); + + const isMissingStyleIndex = + id === './style/index.js' || + id.endsWith('/@lobehub/fluent-emoji/es/FluentEmoji/style/index.js') || + id.endsWith('/@lobehub/fluent-emoji/es/FluentEmoji/style/index.js?') || + id.endsWith('/FluentEmoji/style/index.js') || + id.endsWith('/FluentEmoji/style/index.js?'); + + if (isFluentEmojiEntry && isMissingStyleIndex) return resolve(dirname(importer), 'style.js'); + + return null; + }, + }, + ], optimizeDeps: { exclude: ['crypto', 'util', 'tty'], include: ['@lobehub/tts'], @@ -18,6 +49,7 @@ export default defineConfig({ '@/utils/unzipFile': resolve(__dirname, './src/utils/unzipFile'), '@/utils/server': resolve(__dirname, './src/utils/server'), '@/utils/electron': resolve(__dirname, './src/utils/electron'), + '@/utils/identifier': resolve(__dirname, './src/utils/identifier'), '@/utils': resolve(__dirname, './packages/utils/src'), '@/types': resolve(__dirname, './packages/types/src'), '@/const': resolve(__dirname, './packages/const/src'), @@ -58,7 +90,7 @@ export default defineConfig({ globals: true, server: { deps: { - inline: ['vitest-canvas-mock'], + inline: ['vitest-canvas-mock', '@lobehub/ui', '@lobehub/fluent-emoji'], }, }, setupFiles: join(__dirname, './tests/setup.ts'),