import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ProjectConfiguration } from '@/pages/ProjectConfiguration'; import type { ProviderAccount, ProviderVendorInfo } from '@/lib/providers'; import type { CodingProjectAgent, CodingProjectConfig, CodingProjectSummary } from '@/types/coding-project'; const codingState = vi.hoisted(() => ({ activeProject: null as CodingProjectSummary | null, load: vi.fn().mockResolvedValue(undefined), removeProject: vi.fn(), })); const configState = vi.hoisted(() => ({ configsByProjectId: {} as Record, knowledgeByProjectId: {} as Record, load: vi.fn().mockResolvedValue(undefined), save: vi.fn<(projectId: string, config: CodingProjectConfig) => Promise>().mockResolvedValue(undefined), uploadKnowledge: vi.fn(), remove: vi.fn(), })); const providerState = vi.hoisted(() => ({ accounts: [] as ProviderAccount[], vendors: [] as ProviderVendorInfo[], refreshProviderSnapshot: vi.fn().mockResolvedValue(undefined), })); const getCodingSkills = vi.hoisted(() => vi.fn().mockResolvedValue({ skills: [] })); vi.mock('@/stores/coding-workspace', () => ({ useCodingWorkspaceStore: (selector: (state: typeof codingState) => unknown) => selector(codingState), })); vi.mock('@/stores/project-config', () => ({ useProjectConfigStore: (selector: (state: typeof configState) => unknown) => selector(configState), })); vi.mock('@/stores/providers', () => ({ useProviderStore: (selector: (state: typeof providerState) => unknown) => selector(providerState), })); vi.mock('@/lib/coding-product-tools', () => ({ getCodingSkills })); vi.mock('@/pages/Plugins', () => ({ Plugins: () => null })); vi.mock('@/components/works/ProjectPublishAction', () => ({ ProjectPublishAction: () => null })); const now = '2026-10-14T00:00:00.000Z'; const project: CodingProjectSummary = { id: 'project-config-controls', name: 'Local project', createdAt: now, updatedAt: now, lastOpenedAt: now, }; const defaultAgent: CodingProjectAgent = { id: 'default-agent', avatarId: 'default', roleName: '伙伴', name: 'Builder', builtIn: false, enabled: true, skillIds: ['project-skill', 'unknown-legacy-skill'], responsibility: { mission: 'Build', owns: [], boundaries: [], collaborators: [], principles: [] }, prompt: ' 原有项目提示词\n保留换行与空格。 ', archivedAt: null, pinned: true, model: { accountId: 'account-1', modelId: 'model-default', thinkingLevel: 'off' }, modelResolution: 'resolved', createdAt: now, updatedAt: now, }; const historicalAgent: CodingProjectAgent = { ...defaultAgent, id: 'historical-agent', name: 'Historical builder', prompt: '历史会话专用提示词\n不要替换为默认项目提示词。', skillIds: ['historical-only-skill'], model: { accountId: 'account-1', modelId: 'model-history', thinkingLevel: 'high' }, archivedAt: '2026-10-13T00:00:00.000Z', pinned: false, }; const config: CodingProjectConfig = { schemaVersion: 2, projectType: 'custom', initialized: true, defaultAgentId: defaultAgent.id, agents: [defaultAgent, historicalAgent], knowledgeDirectory: 'knowledge', createdAt: now, updatedAt: now, }; describe('ProjectConfiguration model-only controls', () => { beforeEach(() => { vi.clearAllMocks(); codingState.activeProject = project; configState.configsByProjectId = { [project.id]: structuredClone(config) }; providerState.accounts = [{ id: 'account-1', vendorId: 'custom', label: 'Work account', authMode: 'api_key', model: 'model-default', fallbackModels: ['model-history', 'model-new'], enabled: true, isDefault: true, createdAt: now, updatedAt: now, }]; }); it.each([ { route: '/project-config', heading: '默认编程配置', target: defaultAgent }, { route: '/project-config?agent=historical-agent', heading: '历史会话编程配置', target: historicalAgent }, ])('changes only the selected Agent model at $route and preserves hidden configuration', async ({ route, heading, target }) => { render(); expect(await screen.findByRole('heading', { name: heading })).toBeVisible(); const settings = within(screen.getByTestId('project-default-settings')); const model = settings.getByRole('combobox', { name: '默认模型' }); const save = screen.getByRole('button', { name: '保存项目配置' }); expect(model).toBeEnabled(); expect(model).toHaveValue(JSON.stringify([target.model!.accountId, target.model!.modelId])); expect(save).toBeEnabled(); expect(settings.getByText('默认模型用于新对话;已有对话的模型和消息记录保持不变。')).toBeVisible(); expect(settings.getAllByRole('combobox')).toHaveLength(1); expect(screen.queryByRole('textbox')).not.toBeInTheDocument(); expect(screen.queryByRole('checkbox')).not.toBeInTheDocument(); expect(screen.queryByText('项目提示词')).not.toBeInTheDocument(); expect(screen.queryByText('项目 Skills')).not.toBeInTheDocument(); expect(getCodingSkills).not.toHaveBeenCalled(); fireEvent.change(model, { target: { value: JSON.stringify(['account-1', 'model-new']) } }); fireEvent.click(save); await waitFor(() => expect(configState.save).toHaveBeenCalledOnce()); expect(configState.save).toHaveBeenCalledWith(project.id, { ...config, agents: config.agents.map((agent) => agent.id === target.id ? { ...agent, model: { accountId: 'account-1', modelId: 'model-new', thinkingLevel: 'off', reasoningChoice: { mode: 'default' }, }, updatedAt: expect.any(String), } : agent), }); await waitFor(() => expect(save).toBeEnabled()); expect(codingState.load).toHaveBeenCalledOnce(); expect(configState.configsByProjectId[project.id]).toEqual(config); expect(getCodingSkills).not.toHaveBeenCalled(); }); });