feat: integrate project setup onboarding
This commit is contained in:
@@ -35,7 +35,11 @@ test.describe('Project configuration skills', () => {
|
||||
await expect(page.getByTestId('project-configuration-actions')).toBeVisible();
|
||||
const actionBarBottomGap = await page.getByTestId('project-configuration-actions').evaluate((element) => window.innerHeight - element.getBoundingClientRect().bottom);
|
||||
expect(actionBarBottomGap).toBeLessThan(40);
|
||||
await expect(page.getByRole('button', { name: '创建智能体' })).toBeVisible();
|
||||
const setupCallout = page.getByTestId('project-agent-setup-callout');
|
||||
await expect(setupCallout).toContainText('项目已创建,还差 1 步');
|
||||
await expect(setupCallout).toContainText('必需');
|
||||
await expect(page.getByTestId('project-agent-setup-primary')).toHaveText('先配置模型');
|
||||
await expect(page.getByTestId('project-configuration-back-button')).toHaveAccessibleName('稍后设置');
|
||||
|
||||
for (const [resourceId, title] of [
|
||||
['resource-card-models', '可用模型'],
|
||||
@@ -48,6 +52,7 @@ test.describe('Project configuration skills', () => {
|
||||
if (resourceId === 'resource-card-models') {
|
||||
await expect(drawer.getByRole('combobox')).toHaveCount(0);
|
||||
await expect(drawer.locator('[data-testid="project-model-list"], [data-testid="project-model-empty-state"]')).toHaveCount(1);
|
||||
await expect(drawer.getByRole('button', { name: '前往模型设置' })).toBeVisible();
|
||||
}
|
||||
const closeButton = drawer.getByRole('button', { name: '关闭' });
|
||||
const closeGeometry = await closeButton.evaluate((button) => {
|
||||
@@ -73,6 +78,15 @@ test.describe('Project configuration skills', () => {
|
||||
|
||||
const now = new Date().toISOString();
|
||||
await mkdir(path.join(parentPath, '.makelore'), { recursive: true });
|
||||
await writeFile(path.join(parentPath, '.makelore', 'project.json'), JSON.stringify({
|
||||
...createCodingProjectConfigV2(now, 'interactive_ai_app'),
|
||||
initialized: true,
|
||||
agents: [],
|
||||
}, null, 2));
|
||||
await page.evaluate(() => { window.location.hash = '#/chat'; });
|
||||
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
|
||||
await expect(page.getByTestId('project-agent-setup-callout')).toBeVisible();
|
||||
|
||||
await writeFile(path.join(parentPath, '.makelore', 'project.json'), JSON.stringify({
|
||||
...createCodingProjectConfigV2(now, 'interactive_ai_app'),
|
||||
initialized: true,
|
||||
@@ -96,6 +110,11 @@ test.describe('Project configuration skills', () => {
|
||||
}, null, 2));
|
||||
await page.reload();
|
||||
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
|
||||
await expect(page.getByTestId('project-configuration-back-button')).toHaveAccessibleName('返回对话');
|
||||
await page.getByTestId('project-configuration-back-button').click();
|
||||
await expect(page.getByTestId('chat-operation-page')).toBeVisible();
|
||||
await page.evaluate(() => { window.location.hash = '#/project-config'; });
|
||||
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
|
||||
|
||||
await page.getByTestId('resource-card-skills').click();
|
||||
await expect(page.getByTestId('superpowers-card')).toHaveCount(0);
|
||||
@@ -163,6 +182,7 @@ test.describe('Project configuration skills', () => {
|
||||
await expect(avatarDialog).toHaveCount(0);
|
||||
await expect(maintenanceDialog.getByAltText('当前上传头像')).toBeVisible();
|
||||
await expect(page.getByText('完成配置')).toHaveCount(0);
|
||||
await maintenanceDialog.getByRole('button', { name: '取消' }).click();
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
await rm(parentPath, { recursive: true, force: true });
|
||||
|
||||
@@ -6,6 +6,10 @@ import { useAuthStore } from '@/stores/auth';
|
||||
import { useProviderStore } from '@/stores/providers';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { useUserSyncStore } from '@/stores/user-sync';
|
||||
import { codingWorkspaceStore } from '@/stores/coding-workspace';
|
||||
import { useProjectConfigStore } from '@/stores/project-config';
|
||||
import { createCodingProjectConfigV2 } from '@electron/coding-projects/project-config';
|
||||
import type { CodingProjectAgent } from '@/types/coding-project';
|
||||
|
||||
vi.mock('@/components/layout/MainLayout', () => ({
|
||||
MainLayout: () => (
|
||||
@@ -39,11 +43,41 @@ vi.mock('@/pages/Settings', () => ({
|
||||
Settings: () => <div>Global settings</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@/pages/ProjectConfiguration', () => ({
|
||||
ProjectConfiguration: () => <div>Project configuration</div>,
|
||||
}));
|
||||
|
||||
const readyAgent: CodingProjectAgent = {
|
||||
id: 'agent-1',
|
||||
avatarId: 'avatar-01',
|
||||
roleName: '项目智能体',
|
||||
name: 'Builder',
|
||||
builtIn: false,
|
||||
enabled: true,
|
||||
model: { accountId: 'account-1', modelId: 'model-1', thinkingLevel: 'off' },
|
||||
modelResolution: 'resolved',
|
||||
skillIds: [],
|
||||
responsibility: {
|
||||
mission: '完成项目工作',
|
||||
owns: [],
|
||||
boundaries: [],
|
||||
collaborators: [],
|
||||
principles: [],
|
||||
},
|
||||
prompt: '',
|
||||
archivedAt: null,
|
||||
pinned: false,
|
||||
createdAt: '2026-09-06T00:00:00.000Z',
|
||||
updatedAt: '2026-09-06T00:00:00.000Z',
|
||||
};
|
||||
|
||||
describe('App programming provider initialization gate', () => {
|
||||
const initProviders = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
codingWorkspaceStore.setState(codingWorkspaceStore.getInitialState(), true);
|
||||
useProjectConfigStore.setState(useProjectConfigStore.getInitialState(), true);
|
||||
useSettingsStore.setState({
|
||||
setupComplete: true,
|
||||
init: vi.fn(),
|
||||
@@ -95,6 +129,56 @@ describe('App programming provider initialization gate', () => {
|
||||
await waitFor(() => expect(initProviders).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it('redirects a legacy initialized project without a usable Agent to configuration', async () => {
|
||||
const project = {
|
||||
id: 'local-project',
|
||||
path: '/tmp/local-project',
|
||||
name: 'local-project',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
lastOpenedAt: '',
|
||||
};
|
||||
const config = { ...createCodingProjectConfigV2(), initialized: true, agents: [] };
|
||||
codingWorkspaceStore.setState({
|
||||
activeProjectId: project.id,
|
||||
activeProject: project,
|
||||
load: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
useProjectConfigStore.setState({
|
||||
load: vi.fn().mockResolvedValue({ status: 'valid', config, knowledgeFiles: [] }),
|
||||
});
|
||||
|
||||
await renderAt('/chat');
|
||||
|
||||
expect(await screen.findByText('Project configuration')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Programming workspace')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens chat when the project has a fully configured Agent', async () => {
|
||||
const project = {
|
||||
id: 'local-project',
|
||||
path: '/tmp/local-project',
|
||||
name: 'local-project',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
lastOpenedAt: '',
|
||||
};
|
||||
const config = { ...createCodingProjectConfigV2(), initialized: true, agents: [readyAgent] };
|
||||
codingWorkspaceStore.setState({
|
||||
activeProjectId: project.id,
|
||||
activeProject: project,
|
||||
load: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
useProjectConfigStore.setState({
|
||||
load: vi.fn().mockResolvedValue({ status: 'valid', config, knowledgeFiles: [] }),
|
||||
});
|
||||
|
||||
await renderAt('/chat');
|
||||
|
||||
expect(await screen.findByText('Programming workspace')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Project configuration')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('redirects an orphaned token without a user identity away from a direct workspace route', async () => {
|
||||
useAuthStore.setState({
|
||||
initialized: true,
|
||||
|
||||
@@ -80,8 +80,8 @@ const agent: CodingProjectAgent = {
|
||||
pinned: true,
|
||||
createdAt: '2026-08-23T00:00:00.000Z',
|
||||
updatedAt: '2026-08-23T00:00:00.000Z',
|
||||
model: null,
|
||||
modelResolution: 'required',
|
||||
model: { accountId: 'account-1', modelId: 'model-1', thinkingLevel: 'off' },
|
||||
modelResolution: 'resolved',
|
||||
};
|
||||
const config: CodingProjectConfig = {
|
||||
schemaVersion: 2,
|
||||
@@ -199,6 +199,24 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('shows an actionable setup state when the active project has no usable Agent', async () => {
|
||||
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
|
||||
projectApi.config.mockResolvedValue({ project, config: configForAgents([]) });
|
||||
projectApi.conversations.mockResolvedValue([]);
|
||||
const onOpenProjectSettings = vi.fn();
|
||||
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
|
||||
|
||||
render(<CodingChatPanel onOpenProjectSettings={onOpenProjectSettings} />);
|
||||
|
||||
expect(await screen.findByTestId('coding-chat-agent-setup-empty-state')).toBeVisible();
|
||||
expect(screen.getByText('创建项目智能体后开始对话')).toBeVisible();
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
||||
expect(projectApi.create).not.toHaveBeenCalled();
|
||||
expect(conversationApi.snapshot).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建或恢复项目智能体' }));
|
||||
expect(onOpenProjectSettings).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('makes the first-Conversation textarea editable while runtime metadata is held', async () => {
|
||||
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
|
||||
projectApi.config.mockResolvedValue({ project, config });
|
||||
|
||||
113
tests/unit/coding-project-readiness.test.ts
Normal file
113
tests/unit/coding-project-readiness.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type {
|
||||
CodingProjectAgent,
|
||||
CodingProjectConfig,
|
||||
} from '../../shared/coding-project-contracts';
|
||||
import {
|
||||
hasReadyCodingProjectAgent,
|
||||
isCodingProjectAgentReady,
|
||||
isCodingProjectConversationReady,
|
||||
} from '../../shared/coding-project-contracts';
|
||||
|
||||
const createdAt = '2026-09-06T00:00:00.000Z';
|
||||
|
||||
function createAgent(overrides: Partial<CodingProjectAgent> = {}): CodingProjectAgent {
|
||||
return {
|
||||
id: 'agent-1',
|
||||
avatarId: 'spark',
|
||||
roleName: '编程助手',
|
||||
name: '小洛',
|
||||
builtIn: false,
|
||||
enabled: true,
|
||||
skillIds: [],
|
||||
responsibility: {
|
||||
mission: '帮助完成项目',
|
||||
owns: [],
|
||||
boundaries: [],
|
||||
collaborators: [],
|
||||
principles: [],
|
||||
},
|
||||
prompt: '',
|
||||
archivedAt: null,
|
||||
pinned: false,
|
||||
model: {
|
||||
accountId: 'account-1',
|
||||
modelId: 'model-1',
|
||||
thinkingLevel: 'off',
|
||||
},
|
||||
modelResolution: 'resolved',
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createConfig(overrides: Partial<CodingProjectConfig> = {}): CodingProjectConfig {
|
||||
return {
|
||||
schemaVersion: 2,
|
||||
projectType: 'custom',
|
||||
initialized: true,
|
||||
agents: [createAgent()],
|
||||
knowledgeDirectory: 'knowledge',
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('coding project Conversation readiness', () => {
|
||||
it('accepts an enabled, unarchived, fully configured Agent', () => {
|
||||
const agent = createAgent();
|
||||
const config = createConfig({ agents: [agent] });
|
||||
|
||||
expect(isCodingProjectAgentReady(agent)).toBe(true);
|
||||
expect(hasReadyCodingProjectAgent(config)).toBe(true);
|
||||
expect(isCodingProjectConversationReady(config)).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a project with no Agents', () => {
|
||||
const config = createConfig({ agents: [] });
|
||||
|
||||
expect(hasReadyCodingProjectAgent(config)).toBe(false);
|
||||
expect(isCodingProjectConversationReady(config)).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['disabled', { enabled: false }],
|
||||
['archived', { archivedAt: createdAt }],
|
||||
] satisfies Array<[string, Partial<CodingProjectAgent>]>)('rejects a %s Agent', (_label, overrides) => {
|
||||
const agent = createAgent(overrides);
|
||||
|
||||
expect(isCodingProjectAgentReady(agent)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects an Agent whose model still requires resolution', () => {
|
||||
const agent = createAgent({ model: null, modelResolution: 'required' });
|
||||
|
||||
expect(isCodingProjectAgentReady(agent)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects an Agent with a resolved but missing model', () => {
|
||||
const agent = createAgent({ model: null });
|
||||
|
||||
expect(isCodingProjectAgentReady(agent)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects an Agent with an empty responsibility mission', () => {
|
||||
const agent = createAgent({
|
||||
responsibility: {
|
||||
...createAgent().responsibility,
|
||||
mission: ' ',
|
||||
},
|
||||
});
|
||||
|
||||
expect(isCodingProjectAgentReady(agent)).toBe(false);
|
||||
});
|
||||
|
||||
it('requires the project to be initialized', () => {
|
||||
const config = createConfig({ initialized: false });
|
||||
|
||||
expect(hasReadyCodingProjectAgent(config)).toBe(true);
|
||||
expect(isCodingProjectConversationReady(config)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -6,9 +6,34 @@ import { codingWorkspaceStore } from '@/stores/coding-workspace';
|
||||
import { useProjectConfigStore } from '@/stores/project-config';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { createCodingProjectConfigV2 } from '@electron/coding-projects/project-config';
|
||||
import type { CodingProjectAgent } from '@/types/coding-project';
|
||||
|
||||
const learningIpcMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
const readyAgent: CodingProjectAgent = {
|
||||
id: 'agent-1',
|
||||
avatarId: 'avatar-01',
|
||||
roleName: '项目智能体',
|
||||
name: 'Builder',
|
||||
builtIn: false,
|
||||
enabled: true,
|
||||
model: { accountId: 'account-1', modelId: 'model-1', thinkingLevel: 'off' },
|
||||
modelResolution: 'resolved',
|
||||
skillIds: [],
|
||||
responsibility: {
|
||||
mission: '完成项目工作',
|
||||
owns: [],
|
||||
boundaries: [],
|
||||
collaborators: [],
|
||||
principles: [],
|
||||
},
|
||||
prompt: '',
|
||||
archivedAt: null,
|
||||
pinned: false,
|
||||
createdAt: '2026-09-06T00:00:00.000Z',
|
||||
updatedAt: '2026-09-06T00:00:00.000Z',
|
||||
};
|
||||
|
||||
vi.mock('@/components/layout/Sidebar', () => ({
|
||||
Sidebar: ({ sidebarCollapsedOverride }: { sidebarCollapsedOverride?: boolean }) => (
|
||||
<aside data-testid="sidebar-stub" data-sidebar-collapsed={sidebarCollapsedOverride === undefined ? 'unset' : String(sidebarCollapsedOverride)} />
|
||||
@@ -105,6 +130,34 @@ describe('MainLayout module isolation', () => {
|
||||
expect(screen.getByTestId('titlebar-stub')).toHaveAttribute('data-overlay', 'false');
|
||||
});
|
||||
|
||||
it('keeps the gate when a legacy config says initialized but has no usable Agent', () => {
|
||||
const project = codingWorkspaceStore.getState().activeProject!;
|
||||
useProjectConfigStore.setState({
|
||||
configsByProjectId: {
|
||||
[project.id]: { ...createCodingProjectConfigV2(), initialized: true, agents: [] },
|
||||
},
|
||||
});
|
||||
|
||||
renderLayout('/chat');
|
||||
|
||||
expect(screen.getByTestId('project-initialization-gate')).toBeVisible();
|
||||
expect(screen.getByText('项目还差一步')).toBeVisible();
|
||||
});
|
||||
|
||||
it('allows programming routes when the project has a fully configured Agent', () => {
|
||||
const project = codingWorkspaceStore.getState().activeProject!;
|
||||
useProjectConfigStore.setState({
|
||||
configsByProjectId: {
|
||||
[project.id]: { ...createCodingProjectConfigV2(), initialized: true, agents: [readyAgent] },
|
||||
},
|
||||
});
|
||||
|
||||
renderLayout('/chat');
|
||||
|
||||
expect(screen.queryByTestId('project-initialization-gate')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('route-content')).toBeVisible();
|
||||
});
|
||||
|
||||
it('keeps the canonical plugin workspace outside the local project initialization gate', () => {
|
||||
renderLayout('/plugins');
|
||||
|
||||
|
||||
211
tests/unit/project-configuration-onboarding.test.tsx
Normal file
211
tests/unit/project-configuration-onboarding.test.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user