149 lines
6.1 KiB
TypeScript
149 lines
6.1 KiB
TypeScript
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<string, CodingProjectConfig>,
|
|
knowledgeByProjectId: {} as Record<string, string[]>,
|
|
load: vi.fn().mockResolvedValue(undefined),
|
|
save: vi.fn<(projectId: string, config: CodingProjectConfig) => Promise<void>>().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(<MemoryRouter initialEntries={[route]}><ProjectConfiguration /></MemoryRouter>);
|
|
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();
|
|
});
|
|
});
|