Files
makelore/tests/unit/project-configuration-onboarding.test.tsx

212 lines
7.4 KiB
TypeScript

import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ProjectConfiguration } from '@/pages/ProjectConfiguration';
import { codingWorkspaceStore } from '@/stores/coding-workspace';
import { useProjectConfigStore } from '@/stores/project-config';
import { useProviderStore } from '@/stores/providers';
import type { ProviderAccount } from '@/lib/providers';
import type { CodingProjectAgent, CodingProjectConfig, CodingProjectSummary } from '@/types/coding-project';
import { createCodingProjectConfigV2 } from '@electron/coding-projects/project-config';
const getCodingSkillsMock = vi.hoisted(() => vi.fn());
vi.mock('@/lib/coding-product-tools', () => ({
getCodingSkills: (...args: unknown[]) => getCodingSkillsMock(...args),
}));
const NOW = '2026-09-06T00:00:00.000Z';
const project: CodingProjectSummary = {
id: 'project-1',
path: 'D:/projects/project-1',
name: 'Onboarding project',
createdAt: NOW,
updatedAt: NOW,
lastOpenedAt: NOW,
};
const configuredAccount: ProviderAccount = {
id: 'account-1',
vendorId: 'custom',
label: 'Test account',
authMode: 'api_key',
model: 'model-1',
fallbackModels: [],
enabled: true,
isDefault: true,
createdAt: NOW,
updatedAt: NOW,
};
const readyAgent: CodingProjectAgent = {
id: 'agent-1',
avatarId: 'avatar-01',
roleName: '项目智能体',
name: 'Builder',
builtIn: false,
enabled: true,
model: { accountId: configuredAccount.id, modelId: configuredAccount.model!, thinkingLevel: 'off' },
modelResolution: 'resolved',
skillIds: [],
responsibility: {
mission: '完成项目工作',
owns: [],
boundaries: [],
collaborators: [],
principles: [],
},
prompt: '',
archivedAt: null,
pinned: false,
createdAt: NOW,
updatedAt: NOW,
};
function LocationProbe() {
const location = useLocation();
return <output data-testid="location-path">{location.pathname}</output>;
}
function renderConfiguration(config: CodingProjectConfig) {
useProjectConfigStore.setState({
configsByProjectId: { [project.id]: config },
knowledgeByProjectId: { [project.id]: [] },
});
return render(
<MemoryRouter initialEntries={['/project-config']}>
<Routes>
<Route path="/project-config" element={<ProjectConfiguration />} />
<Route path="/models" element={<LocationProbe />} />
<Route path="/chat" element={<LocationProbe />} />
<Route path="/module-select" element={<LocationProbe />} />
</Routes>
</MemoryRouter>,
);
}
function setProviderModels() {
useProviderStore.setState({
accounts: [configuredAccount],
vendors: [],
});
}
describe('project configuration onboarding', () => {
beforeEach(() => {
vi.clearAllMocks();
getCodingSkillsMock.mockResolvedValue({ skills: [] });
const load = vi.fn().mockResolvedValue(undefined);
const reloadWorkspace = vi.fn().mockResolvedValue(undefined);
const refreshProviderSnapshot = vi.fn().mockResolvedValue(undefined);
codingWorkspaceStore.setState({
activeProject: project,
projects: [project],
conversations: [],
load: reloadWorkspace,
});
useProjectConfigStore.setState({ load });
useProviderStore.setState({
accounts: [],
vendors: [],
refreshProviderSnapshot,
});
});
it('strongly guides a project without models to model settings', async () => {
renderConfiguration(createCodingProjectConfigV2(NOW));
expect(await screen.findByTestId('project-agent-setup-callout')).toHaveTextContent('必需');
expect(screen.getByTestId('project-agent-setup-primary')).toHaveTextContent('先配置模型');
expect(screen.getByTestId('project-configuration-back-button')).toHaveAccessibleName('稍后设置');
fireEvent.click(screen.getByTestId('project-agent-setup-primary'));
expect(await screen.findByTestId('location-path')).toHaveTextContent('/models');
});
it('saves the first Agent as initialized and enters chat after creation', async () => {
setProviderModels();
const savedConfig = vi.fn((_projectId: string, nextConfig: CodingProjectConfig) => Promise.resolve(nextConfig));
const reloadWorkspace = vi.fn().mockResolvedValue(undefined);
useProjectConfigStore.setState({ save: savedConfig });
codingWorkspaceStore.setState({ load: reloadWorkspace });
renderConfiguration(createCodingProjectConfigV2(NOW));
fireEvent.click(await screen.findByTestId('project-agent-setup-primary'));
const dialog = await screen.findByRole('dialog', { name: '创建项目智能体' });
fireEvent.change(within(dialog).getByLabelText(/智能体名称/), { target: { value: 'Builder' } });
fireEvent.change(within(dialog).getByLabelText(/职责说明/), { target: { value: '完成项目工作' } });
fireEvent.click(within(dialog).getByRole('button', { name: '创建并进入对话' }));
await waitFor(() => expect(savedConfig).toHaveBeenCalledTimes(1));
expect(savedConfig).toHaveBeenCalledWith(project.id, expect.objectContaining({
initialized: true,
agents: [expect.objectContaining({
name: 'Builder',
modelResolution: 'resolved',
})],
}));
expect(reloadWorkspace).toHaveBeenCalledTimes(1);
expect(await screen.findByTestId('location-path')).toHaveTextContent('/chat');
});
it('labels the back action and returns a complete project to chat', async () => {
setProviderModels();
renderConfiguration({
...createCodingProjectConfigV2(NOW),
initialized: true,
agents: [readyAgent],
});
const backButton = await screen.findByTestId('project-configuration-back-button');
expect(backButton).toHaveAccessibleName('返回对话');
fireEvent.click(backButton);
expect(await screen.findByTestId('location-path')).toHaveTextContent('/chat');
});
it('offers a new Agent when the project contains only disabled Agents', async () => {
setProviderModels();
renderConfiguration({
...createCodingProjectConfigV2(NOW),
initialized: false,
agents: [{ ...readyAgent, enabled: false }],
});
const setupButton = await screen.findByTestId('project-agent-setup-primary');
expect(setupButton).toHaveTextContent('创建项目智能体');
fireEvent.click(setupButton);
expect(await screen.findByRole('dialog', { name: '创建项目智能体' })).toBeVisible();
});
it('confirms before the labelled back action discards Agent edits', async () => {
setProviderModels();
renderConfiguration({
...createCodingProjectConfigV2(NOW),
initialized: true,
agents: [readyAgent],
});
fireEvent.click(await screen.findByTestId(`project-agent-${readyAgent.id}`));
const editor = await screen.findByRole('dialog', { name: `${readyAgent.name} · 编辑智能体` });
fireEvent.change(within(editor).getByLabelText(/智能体名称/), { target: { value: 'Builder 2' } });
fireEvent.click(within(editor).getByRole('button', { name: '保存智能体' }));
await waitFor(() => expect(editor).not.toBeInTheDocument());
fireEvent.click(screen.getByTestId('project-configuration-back-button'));
const confirm = await screen.findByRole('dialog', { name: '放弃未保存的项目配置?' });
fireEvent.click(within(confirm).getByRole('button', { name: '返回对话' }));
expect(await screen.findByTestId('location-path')).toHaveTextContent('/chat');
});
});