🧪 fix: improve test infrastructure and mock configurations (#11028)

* 🧪 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.
This commit is contained in:
Innei 2025-12-29 16:54:06 +08:00 committed by GitHub
parent 8b67718158
commit da4eb9c1b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 206 additions and 150 deletions

View file

@ -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;

View file

@ -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';

View file

@ -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<typeof import('antd-style')>();
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(() => <div data-testid="key-value-editor">Key-Value Editor</div>),
}));
vi.mock('@lobehub/icons', () => ({
ComfyUI: {
Combine: vi.fn(() => <div data-testid="comfyui-icon">ComfyUI Icon</div>),
},
ProviderIcon: vi.fn(() => <div data-testid="provider-icon">Provider Icon</div>),
}));
vi.mock('@lobehub/ui', () => ({
Icon: vi.fn(({ icon, ...props }) => (
<div data-testid="icon" {...props}>
@ -75,16 +93,6 @@ vi.mock('@lobehub/ui', () => ({
</button>
)),
ProviderIcon: vi.fn(() => <div data-testid="provider-icon">Provider Icon</div>),
}));
vi.mock('@lobehub/icons', () => ({
ComfyUI: {
Combine: vi.fn(() => <div data-testid="comfyui-icon">ComfyUI Icon</div>),
},
ProviderIcon: vi.fn(() => <div data-testid="provider-icon">Provider Icon</div>),
}));
vi.mock('@lobehub/ui', () => ({
Center: vi.fn(({ children, ...props }) => (
<div data-testid="center" {...props}>
{children}

View file

@ -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);

View file

@ -169,6 +169,6 @@ describe('PanelContent', () => {
it('should render Menu with main items', () => {
renderWithRouter(<PanelContent closePopover={closePopover} />);
expect(screen.getByText('Mocked Menu')).toBeInTheDocument();
expect(screen.getAllByText('Mocked Menu').length).toBeGreaterThan(0);
});
});

View file

@ -66,7 +66,8 @@ describe('UserAvatar', () => {
});
render(<UserAvatar />);
expect(screen.getByAltText('testuser')).toHaveAttribute('src', DEFAULT_USER_AVATAR_URL);
// When user has no avatar url, <Avatar /> falls back to initials rendering (not an <img />)
expect(screen.getByText('TE')).toBeInTheDocument();
});
it('should show LobeChat and default avatar when the user is not logged in and enable auth', () => {

View file

@ -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',
});
});

View file

@ -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();

View file

@ -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('<user_memory>'));
expect(injection).toBeDefined();
expect(injection!.role).toBe('user');
});
it('should skip user memory when memories is undefined', async () => {

View file

@ -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: {},

View file

@ -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
*

View file

@ -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);
}
});
});

View file

@ -250,7 +250,7 @@ export class DocumentService {
// Clean up content - remove <page> tags if present
let cleanContent = fileDocument.content;
if (cleanContent.includes('<page')) {
cleanContent = cleanContent.replace(/<page[^>]*>([\s\S]*?)<\/page>/g, '$1').trim();
cleanContent = cleanContent.replaceAll(/<page[^>]*>([\S\s]*?)<\/page>/g, '$1').trim();
}
const document = await this.documentModel.create({

View file

@ -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);
});
});

View file

@ -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,
});
}

View file

@ -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('<user_memories');
expect(result[0].content).toContain('contexts="1"');
expect(result[0].content).toContain('experiences="0"');
expect(result[0].content).toContain('preferences="0"');
expect(result[0].content).toContain(
'<user_memories_context id="ctx-1"><context_title>LobeHub</context_title><context_description>Weekly syncs for LobeHub</context_description></user_memories_context>',
);
expect(result[0].content).toContain('<context_title>LobeHub</context_title>');
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('<user_memory>'));
expect(injection).toBeDefined();
expect(injection!.role).toBe('user');
expect(injection!.content).toContain('<user_memory>');
expect(injection!.content).toContain('<contexts count="1">');
expect(injection!.content).toContain('<context id="ctx-1" title="LobeHub">');
});
it('should handle missing placeholder variables gracefully', async () => {

View file

@ -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,
});

View file

@ -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<typeof import('@/libs/swr')>();
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());

View file

@ -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<typeof import('@/libs/swr')>();
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());

View file

@ -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<typeof import('@/libs/swr')>();
return {
...actual,
mutate: vi.fn(),

View file

@ -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',
});

View file

@ -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);
});
});

View file

@ -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<typeof import('@/libs/swr')>();
return {
...(actual as any),
...actual,
mutate: vi.fn(),
};
});

View file

@ -123,7 +123,7 @@ describe('SessionAction', () => {
});
expect(message.loading).toHaveBeenCalled();
expect(sessionService.cloneSession).toHaveBeenCalledWith(sessionId, undefined);
expect(sessionService.cloneSession).toHaveBeenCalledWith(sessionId, expect.any(String));
});
});

View file

@ -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);

View file

@ -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)
);
};

View file

@ -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'),