415 lines
23 KiB
TypeScript
415 lines
23 KiB
TypeScript
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
import { MemoryRouter, useLocation } from 'react-router-dom';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { ProjectConfiguration } from '@/pages/ProjectConfiguration';
|
|
import { useOpencodeStore } from '@/stores/opencode';
|
|
import { useProjectConversationStore } from '@/stores/project-conversations';
|
|
import { useProjectConfigStore } from '@/stores/project-config';
|
|
import { useProviderStore } from '@/stores/providers';
|
|
import { createProjectConfig } from '../../shared/project-config';
|
|
import { createProjectConversationState, removeProjectSessionMetadata, upsertProjectSessionMetadata } from '../../shared/project-conversations';
|
|
|
|
const hostApiFetchMock = vi.fn();
|
|
vi.mock('@/lib/host-api', () => ({ hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args) }));
|
|
|
|
function LocationProbe() {
|
|
const location = useLocation();
|
|
return <output data-testid="current-route">{location.pathname}</output>;
|
|
}
|
|
|
|
describe('ProjectConfiguration', () => {
|
|
const project = { id: 'prj_config', path: 'D:/repo/config', name: 'config', createdAt: '', updatedAt: '', lastOpenedAt: '' };
|
|
const account = {
|
|
id: 'openai-account',
|
|
vendorId: 'openai',
|
|
label: 'OpenAI',
|
|
authMode: 'api_key' as const,
|
|
model: 'gpt-4o-mini',
|
|
enabled: true,
|
|
isDefault: true,
|
|
createdAt: '2026-07-11T00:00:00.000Z',
|
|
updatedAt: '2026-07-11T00:00:00.000Z',
|
|
};
|
|
const status = {
|
|
id: account.id,
|
|
name: account.label,
|
|
type: account.vendorId,
|
|
model: account.model,
|
|
enabled: true,
|
|
hasKey: true,
|
|
keyMasked: 'sk-***',
|
|
createdAt: account.createdAt,
|
|
updatedAt: account.updatedAt,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
Object.defineProperty(window, 'matchMedia', {
|
|
configurable: true,
|
|
value: vi.fn().mockImplementation((query: string) => ({
|
|
matches: query === '(prefers-reduced-motion: reduce)',
|
|
media: query,
|
|
onchange: null,
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
dispatchEvent: vi.fn(),
|
|
})),
|
|
});
|
|
const config = createProjectConfig(undefined, 'mini_game');
|
|
useOpencodeStore.setState({ activeProject: project, projects: [project] });
|
|
useProjectConfigStore.setState({ configsByProjectId: { [project.id]: config }, knowledgeByProjectId: { [project.id]: [] }, errorsByProjectId: {}, loadingProjectId: null });
|
|
useProviderStore.setState({
|
|
accounts: [account],
|
|
statuses: [status],
|
|
defaultAccountId: account.id,
|
|
loading: false,
|
|
error: null,
|
|
});
|
|
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
|
|
if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'valid', config, knowledgeFiles: [] };
|
|
if (path === '/api/opencode/skills') return { skills: [] };
|
|
if (path === '/api/provider-accounts') return [account];
|
|
if (path === '/api/provider-accounts/key-info') return [{ accountId: account.id, hasKey: true, keyMasked: 'sk-***' }];
|
|
if (path === '/api/provider-vendors') return [];
|
|
if (path === '/api/provider-accounts/default') return { accountId: account.id };
|
|
if (path === '/api/opencode/projects/config' && init?.method === 'PUT') return { success: true, config: { ...(JSON.parse(init.body as string).config), initialized: true }, knowledgeFiles: [] };
|
|
if (path === '/api/opencode/projects/remove' && init?.method === 'POST') return { success: true, removedProjectId: project.id, projects: [], activeProject: null };
|
|
throw new Error(`Unexpected path ${path}`);
|
|
});
|
|
});
|
|
|
|
it('shows an empty contact list for a new project', async () => {
|
|
render(<MemoryRouter><ProjectConfiguration /></MemoryRouter>);
|
|
|
|
expect(await screen.findByText('配置你的项目空间')).toBeInTheDocument();
|
|
expect(screen.getAllByRole('button', { name: '一键提交审核' })).toHaveLength(1);
|
|
expect(screen.queryByText(/新项目还没有伙伴/)).not.toBeInTheDocument();
|
|
expect(screen.getByText('还没有伙伴,点击右上角的 + 创建第一个。')).toBeInTheDocument();
|
|
expect(screen.queryAllByTestId(/^project-agent-/)).toHaveLength(0);
|
|
|
|
const actions = screen.getByTestId('project-configuration-actions');
|
|
const primaryActions = screen.getByTestId('project-configuration-primary-actions');
|
|
expect(actions).toHaveClass('mt-auto', 'sticky', 'sm:flex-row');
|
|
expect(screen.getByRole('button', { name: '删除项目' })).toHaveClass('h-9', 'bg-background');
|
|
expect(within(primaryActions).getByRole('button', { name: '一键提交审核' })).toHaveClass('h-9', 'bg-background', 'text-foreground');
|
|
expect(within(primaryActions).getByRole('button', { name: '确认并完成初始化' })).toHaveClass('h-10', 'bg-brand');
|
|
expect(within(primaryActions).getAllByRole('button').map((button) => button.textContent)).toEqual(['一键提交审核', '确认并完成初始化']);
|
|
});
|
|
|
|
it('opens an installed Skill detail with its structure and main file', async () => {
|
|
const fallback = hostApiFetchMock.getMockImplementation();
|
|
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
|
|
if (path === '/api/opencode/skills') {
|
|
return {
|
|
skills: [{
|
|
name: 'pm-project-plan',
|
|
description: 'Plan software projects.',
|
|
location: 'D:/managed/skills/pm-project-plan/SKILL.md',
|
|
content: '# Project planning\n\nPlan the work.',
|
|
entries: [
|
|
{ path: 'references', type: 'directory' },
|
|
{ path: 'references/guide.md', type: 'file' },
|
|
{ path: 'SKILL.md', type: 'file' },
|
|
],
|
|
}],
|
|
};
|
|
}
|
|
if (!fallback) throw new Error(`Unexpected path ${path}`);
|
|
return fallback(path, init);
|
|
});
|
|
|
|
render(<MemoryRouter><ProjectConfiguration /></MemoryRouter>);
|
|
fireEvent.click(await screen.findByTestId('resource-card-skills'));
|
|
|
|
const skillCard = await screen.findByTestId('skill-card-pm-project-plan');
|
|
fireEvent.click(skillCard);
|
|
|
|
expect(await screen.findByTestId('skill-detail-view')).toBeInTheDocument();
|
|
expect(screen.getByTestId('skill-structure')).toHaveTextContent('references/guide.md');
|
|
expect(screen.getByTestId('skill-main-file')).toHaveTextContent('# Project planning');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '返回技能列表' }));
|
|
expect(await screen.findByTestId('skill-card-pm-project-plan')).toBeInTheDocument();
|
|
});
|
|
|
|
it('returns to the previous page and closes each resource drawer from its header', async () => {
|
|
render(<MemoryRouter initialEntries={['/opencode-chat', '/project-config']} initialIndex={1}><ProjectConfiguration /><LocationProbe /></MemoryRouter>);
|
|
|
|
expect(await screen.findByRole('button', { name: '返回' })).toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole('button', { name: '返回' }));
|
|
await waitFor(() => expect(screen.getByTestId('current-route')).toHaveTextContent('/opencode-chat'));
|
|
|
|
const resourceDrawers = [
|
|
['resource-card-models', '大脑(大语言模型)'],
|
|
['resource-card-knowledge', '笔记本(知识库)'],
|
|
['resource-card-skills', '工具箱(技能)'],
|
|
] as const;
|
|
for (const [resourceId, title] of resourceDrawers) {
|
|
fireEvent.click(screen.getByTestId(resourceId));
|
|
expect(await screen.findByRole('dialog', { name: title })).toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole('button', { name: '关闭' }));
|
|
await waitFor(() => expect(screen.queryByRole('dialog', { name: title })).not.toBeInTheDocument());
|
|
}
|
|
});
|
|
|
|
it('shows configured models as read-only cards instead of a project selector', async () => {
|
|
const config = {
|
|
...createProjectConfig(undefined, 'mini_game'),
|
|
defaultModel: 'legacy/project-default',
|
|
};
|
|
useProjectConfigStore.setState({
|
|
configsByProjectId: { [project.id]: config },
|
|
knowledgeByProjectId: { [project.id]: [] },
|
|
errorsByProjectId: {},
|
|
loadingProjectId: null,
|
|
});
|
|
hostApiFetchMock.mockImplementation(async (path: string) => {
|
|
if (path === `/api/opencode/projects/config?projectId=${project.id}`) {
|
|
return { status: 'valid', config, knowledgeFiles: [] };
|
|
}
|
|
if (path === '/api/opencode/skills') return { skills: [] };
|
|
if (path === '/api/provider-accounts') return [account];
|
|
if (path === '/api/provider-accounts/key-info') return [{ accountId: account.id, hasKey: true, keyMasked: 'sk-***' }];
|
|
if (path === '/api/provider-vendors') return [];
|
|
if (path === '/api/provider-accounts/default') return { accountId: account.id };
|
|
throw new Error(`Unexpected path ${path}`);
|
|
});
|
|
|
|
render(<MemoryRouter><ProjectConfiguration /></MemoryRouter>);
|
|
fireEvent.click(await screen.findByTestId('resource-card-models'));
|
|
|
|
const drawer = await screen.findByRole('dialog', { name: '大脑(大语言模型)' });
|
|
expect(within(drawer).getByTestId('project-model-list')).toBeInTheDocument();
|
|
expect(within(drawer).getByTestId('model-card-openai-gpt-4o-mini')).toHaveTextContent('gpt-4o-mini');
|
|
expect(within(drawer).getByText('文本')).toBeInTheDocument();
|
|
expect(within(drawer).queryByRole('combobox')).not.toBeInTheDocument();
|
|
expect(screen.queryByLabelText('项目默认模型')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('does not offer one-click submission for custom projects', async () => {
|
|
const customConfig = createProjectConfig();
|
|
useProjectConfigStore.setState({
|
|
configsByProjectId: { [project.id]: customConfig },
|
|
knowledgeByProjectId: { [project.id]: [] },
|
|
errorsByProjectId: {},
|
|
loadingProjectId: null,
|
|
});
|
|
const fallback = hostApiFetchMock.getMockImplementation();
|
|
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
|
|
if (path === `/api/opencode/projects/config?projectId=${project.id}`) {
|
|
return { status: 'valid', config: customConfig, knowledgeFiles: [] };
|
|
}
|
|
if (!fallback) throw new Error(`Unexpected path ${path}`);
|
|
return fallback(path, init);
|
|
});
|
|
|
|
render(<MemoryRouter><ProjectConfiguration /></MemoryRouter>);
|
|
|
|
expect(screen.queryByRole('button', { name: '一键提交审核' })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('creates a contact with the required basics and leaves advanced settings empty', async () => {
|
|
render(<MemoryRouter><ProjectConfiguration /></MemoryRouter>);
|
|
const createButton = await screen.findByRole('button', { name: '创建伙伴' });
|
|
expect(createButton).toHaveClass('h-8', 'w-8', 'rounded-full', 'bg-brand-soft');
|
|
fireEvent.click(createButton);
|
|
|
|
expect(screen.getByRole('dialog', { name: '创建项目伙伴' })).toBeInTheDocument();
|
|
fireEvent.change(screen.getByLabelText(/伙伴名称/), { target: { value: '小明' } });
|
|
fireEvent.change(screen.getByLabelText(/职责简述/), { target: { value: '负责把想法拆成步骤。' } });
|
|
fireEvent.click(screen.getByRole('button', { name: '更换头像' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '选择头像:像素伙伴 3' }));
|
|
fireEvent.change(screen.getByLabelText('Agent 提示词'), { target: { value: '先拆解目标,再给出执行步骤。' } });
|
|
fireEvent.click(screen.getByRole('button', { name: '创建伙伴' }));
|
|
await waitFor(() => expect(screen.queryByRole('dialog', { name: '创建项目伙伴' })).not.toBeInTheDocument());
|
|
fireEvent.click(screen.getByRole('button', { name: '确认并完成初始化' }));
|
|
|
|
await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/config', expect.objectContaining({ method: 'PUT' })));
|
|
const saveCall = hostApiFetchMock.mock.calls.find(([path, init]) => path === '/api/opencode/projects/config' && (init as RequestInit | undefined)?.method === 'PUT');
|
|
const savedConfig = JSON.parse((saveCall?.[1] as RequestInit).body as string).config;
|
|
expect(savedConfig.initialized).toBe(true);
|
|
expect(savedConfig.agents).toEqual([expect.objectContaining({
|
|
name: '小明',
|
|
avatarId: 'avatar-03',
|
|
model: 'openai/gpt-4o-mini',
|
|
prompt: '先拆解目标,再给出执行步骤。',
|
|
skillIds: [],
|
|
responsibility: expect.objectContaining({ mission: '负责把想法拆成步骤。' }),
|
|
})]);
|
|
});
|
|
|
|
it('opens existing partners in the shared maintenance dialog and saves through project settings', async () => {
|
|
const config = createProjectConfig();
|
|
const contact = {
|
|
id: 'agent-edit',
|
|
avatarId: 'avatar-01',
|
|
roleName: '项目伙伴',
|
|
name: '小红',
|
|
builtIn: false,
|
|
enabled: true,
|
|
model: 'openai/gpt-4o-mini',
|
|
skillIds: [],
|
|
responsibility: { mission: '负责设计体验。', owns: [], boundaries: [], collaborators: [], principles: [] },
|
|
prompt: '',
|
|
archivedAt: null,
|
|
pinned: false,
|
|
};
|
|
const configWithContact = { ...config, initialized: true, agents: [contact] };
|
|
useProjectConfigStore.setState({ configsByProjectId: { [project.id]: configWithContact }, knowledgeByProjectId: { [project.id]: [] }, errorsByProjectId: {}, loadingProjectId: null });
|
|
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
|
|
if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'valid', config: configWithContact, knowledgeFiles: [] };
|
|
if (path === '/api/opencode/skills') return { skills: [] };
|
|
if (path === '/api/provider-accounts') return [account];
|
|
if (path === '/api/provider-accounts/key-info') return [{ accountId: account.id, hasKey: true, keyMasked: 'sk-***' }];
|
|
if (path === '/api/provider-vendors') return [];
|
|
if (path === '/api/provider-accounts/default') return { accountId: account.id };
|
|
if (path === '/api/opencode/projects/config' && init?.method === 'PUT') return { success: true, config: JSON.parse(init.body as string).config, knowledgeFiles: [] };
|
|
throw new Error(`Unexpected path ${path}`);
|
|
});
|
|
|
|
render(<MemoryRouter><ProjectConfiguration /></MemoryRouter>);
|
|
fireEvent.click(await screen.findByTestId('project-agent-agent-edit'));
|
|
|
|
const dialog = screen.getByRole('dialog', { name: '小红 · 伙伴维护' });
|
|
expect(within(dialog).getByLabelText(/伙伴名称/)).toHaveValue('小红');
|
|
expect(within(dialog).getByLabelText(/职责简述/)).toHaveValue('负责设计体验。');
|
|
expect(within(dialog).getByRole('button', { name: '保存伙伴' })).toBeInTheDocument();
|
|
expect(screen.queryByText('完成配置')).not.toBeInTheDocument();
|
|
|
|
fireEvent.change(within(dialog).getByLabelText(/伙伴名称/), { target: { value: '小蓝' } });
|
|
fireEvent.change(within(dialog).getByLabelText(/职责简述/), { target: { value: '负责新的视觉体验。' } });
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '保存伙伴' }));
|
|
|
|
await waitFor(() => expect(screen.queryByRole('button', { name: '保存伙伴' })).not.toBeInTheDocument());
|
|
expect(hostApiFetchMock.mock.calls.some(([path]) => path === '/api/opencode/projects/config')).toBe(false);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '保存项目配置' }));
|
|
await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/config', expect.objectContaining({ method: 'PUT' })));
|
|
const saveCall = hostApiFetchMock.mock.calls.find(([path, init]) => path === '/api/opencode/projects/config' && (init as RequestInit | undefined)?.method === 'PUT');
|
|
const savedConfig = JSON.parse((saveCall?.[1] as RequestInit).body as string).config;
|
|
expect(savedConfig.agents[0]).toEqual(expect.objectContaining({
|
|
name: '小蓝',
|
|
responsibility: expect.objectContaining({ mission: '负责新的视觉体验。' }),
|
|
}));
|
|
});
|
|
|
|
it('requires confirmation before archiving a partner in the maintenance dialog', async () => {
|
|
const config = createProjectConfig();
|
|
const contact = {
|
|
id: 'agent-settings',
|
|
avatarId: 'avatar-01',
|
|
roleName: '项目伙伴',
|
|
name: '小红',
|
|
builtIn: false,
|
|
enabled: true,
|
|
model: 'openai/gpt-4o-mini',
|
|
skillIds: [],
|
|
responsibility: { mission: '负责设计体验。', owns: [], boundaries: [], collaborators: [], principles: [] },
|
|
prompt: '',
|
|
archivedAt: null,
|
|
pinned: false,
|
|
};
|
|
const configWithContact = { ...config, initialized: true, agents: [contact] };
|
|
useProjectConfigStore.setState({ configsByProjectId: { [project.id]: configWithContact }, knowledgeByProjectId: { [project.id]: [] }, errorsByProjectId: {}, loadingProjectId: null });
|
|
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
|
|
if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'valid', config: configWithContact, knowledgeFiles: [] };
|
|
if (path === '/api/opencode/skills') return { skills: [] };
|
|
if (path === '/api/provider-accounts') return [account];
|
|
if (path === '/api/provider-accounts/key-info') return [{ accountId: account.id, hasKey: true, keyMasked: 'sk-***' }];
|
|
if (path === '/api/provider-vendors') return [];
|
|
if (path === '/api/provider-accounts/default') return { accountId: account.id };
|
|
if (path === '/api/opencode/projects/conversations') return { state: { schemaVersion: 1, sessions: [], updatedAt: new Date().toISOString() } };
|
|
if (path === '/api/opencode/projects/config' && init?.method === 'PUT') return { success: true, config: JSON.parse(init.body as string).config, knowledgeFiles: [] };
|
|
throw new Error(`Unexpected path ${path}`);
|
|
});
|
|
|
|
render(<MemoryRouter><ProjectConfiguration /></MemoryRouter>);
|
|
fireEvent.click(await screen.findByTestId('project-agent-agent-settings'));
|
|
fireEvent.click(screen.getByRole('button', { name: '归档伙伴' }));
|
|
expect(screen.getByRole('dialog', { name: '归档伙伴“小红”?' })).toBeInTheDocument();
|
|
expect(hostApiFetchMock.mock.calls.some(([path]) => path === '/api/opencode/projects/config')).toBe(false);
|
|
fireEvent.click(screen.getByRole('button', { name: '取消' }));
|
|
expect(screen.queryByRole('dialog', { name: '归档伙伴“小红”?' })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('requires explicit confirmation before removing a project', async () => {
|
|
render(<MemoryRouter><ProjectConfiguration /></MemoryRouter>);
|
|
fireEvent.click(await screen.findByRole('button', { name: '删除项目' }));
|
|
|
|
const dialog = screen.getByRole('dialog', { name: `删除项目“${project.name}”?` });
|
|
expect(dialog).toHaveTextContent('不会删除磁盘中的项目文件');
|
|
expect(hostApiFetchMock).not.toHaveBeenCalledWith('/api/opencode/projects/remove', expect.anything());
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '确认删除项目' }));
|
|
await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/remove', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ projectId: project.id }),
|
|
}));
|
|
expect(useOpencodeStore.getState().projects).toEqual([]);
|
|
expect(useOpencodeStore.getState().activeProject).toBeNull();
|
|
});
|
|
|
|
it('permanently deletes archived custom contacts and their sessions', async () => {
|
|
const contact = {
|
|
id: 'agent-archived-delete',
|
|
avatarId: 'avatar-01',
|
|
roleName: '项目伙伴',
|
|
name: '待删除伙伴',
|
|
builtIn: false,
|
|
enabled: true,
|
|
model: 'openai/gpt-4o-mini',
|
|
skillIds: [],
|
|
responsibility: { mission: '负责清理测试数据。', owns: [], boundaries: [], collaborators: [], principles: [] },
|
|
prompt: '',
|
|
archivedAt: '2026-07-29T00:00:00.000Z',
|
|
pinned: false,
|
|
};
|
|
const config = { ...createProjectConfig(), initialized: true, agents: [contact] };
|
|
let conversationState = upsertProjectSessionMetadata(
|
|
createProjectConversationState(),
|
|
'ses-archived-delete',
|
|
contact.id,
|
|
'2026-07-29T00:00:00.000Z',
|
|
);
|
|
conversationState = {
|
|
...conversationState,
|
|
sessions: conversationState.sessions.map((session) => ({ ...session, archivedAt: '2026-07-29T00:00:00.000Z' })),
|
|
};
|
|
useOpencodeStore.setState({ activeProject: project, projects: [project], sessions: [{ id: 'ses-archived-delete', title: '旧会话' }] });
|
|
useProjectConversationStore.setState({ statesByProjectId: { [project.id]: conversationState } });
|
|
useProjectConfigStore.setState({ configsByProjectId: { [project.id]: config }, knowledgeByProjectId: { [project.id]: [] }, errorsByProjectId: {}, loadingProjectId: null });
|
|
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
|
|
if (path === `/api/opencode/projects/config?projectId=${project.id}`) return { status: 'valid', config, knowledgeFiles: [] };
|
|
if (path === '/api/opencode/skills') return { skills: [] };
|
|
if (path === '/api/provider-accounts') return [account];
|
|
if (path === '/api/provider-accounts/key-info') return [{ accountId: account.id, hasKey: true, keyMasked: 'sk-***' }];
|
|
if (path === '/api/provider-vendors') return [];
|
|
if (path === '/api/provider-accounts/default') return { accountId: account.id };
|
|
if (path === `/api/opencode/projects/conversations?projectId=${project.id}`) return { state: conversationState };
|
|
if (path === '/api/opencode/sessions/ses-archived-delete' && init?.method === 'DELETE') return { success: true };
|
|
if (path === '/api/opencode/projects/conversations' && init?.method === 'POST') {
|
|
conversationState = removeProjectSessionMetadata(conversationState, 'ses-archived-delete');
|
|
return { success: true, state: conversationState };
|
|
}
|
|
if (path === '/api/opencode/projects/config' && init?.method === 'PUT') return { success: true, config: JSON.parse(init.body as string).config, knowledgeFiles: [] };
|
|
throw new Error(`Unexpected path ${path}`);
|
|
});
|
|
|
|
render(<MemoryRouter><ProjectConfiguration /></MemoryRouter>);
|
|
const deleteButton = await screen.findByRole('button', { name: '永久删除伙伴 待删除伙伴' });
|
|
fireEvent.click(deleteButton);
|
|
expect(await screen.findByRole('dialog', { name: '永久删除伙伴“待删除伙伴”?' })).toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole('button', { name: '永久删除' }));
|
|
|
|
await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/sessions/ses-archived-delete', { method: 'DELETE' }));
|
|
await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/conversations', expect.objectContaining({ method: 'POST' })));
|
|
await waitFor(() => {
|
|
const saveCall = hostApiFetchMock.mock.calls.find(([path, init]) => path === '/api/opencode/projects/config' && (init as RequestInit | undefined)?.method === 'PUT');
|
|
expect(saveCall).toBeTruthy();
|
|
expect(JSON.parse((saveCall?.[1] as RequestInit).body as string).config.agents).toEqual([]);
|
|
});
|
|
});
|
|
});
|