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 {location.pathname}; } function renderConfiguration(config: CodingProjectConfig) { useProjectConfigStore.setState({ configsByProjectId: { [project.id]: config }, knowledgeByProjectId: { [project.id]: [] }, }); return render( } /> } /> } /> } /> , ); } 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'); }); });