feat: update Makelore modules and conversations
This commit is contained in:
304
tests/unit/agent-conversation-sidebar.test.tsx
Normal file
304
tests/unit/agent-conversation-sidebar.test.tsx
Normal file
@@ -0,0 +1,304 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { AgentConversationSidebar } from '@/components/opencode/AgentConversationSidebar';
|
||||
import type { ConfiguredModelOption } from '@/lib/model-options';
|
||||
import type { ProjectAgentConfig } from '../../shared/project-config';
|
||||
import type { ProjectSessionMetadata } from '../../shared/project-conversations';
|
||||
import type { OpencodeSession } from '@/types/opencode';
|
||||
|
||||
const modelOption: ConfiguredModelOption = {
|
||||
modelRef: 'openai/gpt-4o-mini',
|
||||
label: 'gpt-4o-mini',
|
||||
runtimeProviderKey: 'openai',
|
||||
accountId: 'account-openai',
|
||||
capability: 'text',
|
||||
};
|
||||
|
||||
function agent(id: string, name: string, overrides: Partial<ProjectAgentConfig> = {}): ProjectAgentConfig {
|
||||
return {
|
||||
id,
|
||||
avatarId: 'avatar-01',
|
||||
roleName: '项目伙伴',
|
||||
name,
|
||||
builtIn: false,
|
||||
enabled: true,
|
||||
model: modelOption.modelRef,
|
||||
skillIds: [],
|
||||
responsibility: { mission: `${name}的职责`, owns: [], boundaries: [], collaborators: [], principles: [] },
|
||||
prompt: '',
|
||||
archivedAt: null,
|
||||
pinned: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function session(id: string, title: string): OpencodeSession {
|
||||
return { id, title, updatedAt: '2026-07-30T00:02:00.000Z' } as OpencodeSession;
|
||||
}
|
||||
|
||||
function metadata(sessionId: string, agentId: string, overrides: Partial<ProjectSessionMetadata> = {}): ProjectSessionMetadata {
|
||||
return {
|
||||
sessionId,
|
||||
agentId,
|
||||
archivedAt: null,
|
||||
unreadCount: 0,
|
||||
createdAt: '2026-07-30T00:01:00.000Z',
|
||||
updatedAt: '2026-07-30T00:02:00.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function renderSidebar(overrides: Partial<ComponentProps<typeof AgentConversationSidebar>> = {}) {
|
||||
const callbacks = {
|
||||
onSelectAgent: vi.fn(),
|
||||
onSelectSession: vi.fn(),
|
||||
onNewSession: vi.fn(),
|
||||
onCreateAgent: vi.fn().mockResolvedValue(undefined),
|
||||
onArchiveAgent: vi.fn().mockResolvedValue(undefined),
|
||||
onRestoreAgent: vi.fn().mockResolvedValue(undefined),
|
||||
onDeleteAgent: vi.fn().mockResolvedValue(undefined),
|
||||
onArchiveSession: vi.fn().mockResolvedValue(undefined),
|
||||
onRestoreSession: vi.fn().mockResolvedValue(undefined),
|
||||
onDeleteSession: vi.fn().mockResolvedValue(undefined),
|
||||
onTogglePinAgent: vi.fn().mockResolvedValue(undefined),
|
||||
onOpenProjectSettings: vi.fn(),
|
||||
};
|
||||
render(
|
||||
<AgentConversationSidebar
|
||||
agents={[agent('agent-a', '小明'), agent('agent-b', '小红', { pinned: true })]}
|
||||
sessions={[session('ses-a', '第一次聊天')]}
|
||||
sessionMessagesBySessionId={{}}
|
||||
conversationSessions={[metadata('ses-a', 'agent-a', { unreadCount: 2 })]}
|
||||
selectedAgentId={null}
|
||||
selectedSessionId={null}
|
||||
sessionStatuses={{}}
|
||||
pendingQuestionCountBySession={{}}
|
||||
pendingPermissionCountBySession={{}}
|
||||
startingAgentId={null}
|
||||
modelOptions={[modelOption]}
|
||||
{...callbacks}
|
||||
{...overrides}
|
||||
/>,
|
||||
);
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
describe('AgentConversationSidebar', () => {
|
||||
it('uses a flush square sidebar surface without an outer card frame', () => {
|
||||
renderSidebar();
|
||||
|
||||
const sidebar = screen.getByTestId('agent-conversation-sidebar');
|
||||
expect(sidebar).toHaveClass('rounded-none', 'border-r');
|
||||
expect(sidebar).not.toHaveClass('rounded-xl', 'shadow-soft');
|
||||
expect(screen.getByTestId('agent-conversation-sidebar-content')).toHaveClass('p-0');
|
||||
});
|
||||
|
||||
it('opens project settings from the conversation header', () => {
|
||||
const callbacks = renderSidebar();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '项目设置' }));
|
||||
expect(callbacks.onOpenProjectSettings).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('shows only the centered project-partner heading in the conversation header', () => {
|
||||
renderSidebar();
|
||||
|
||||
expect(screen.queryByText('项目联系人')).not.toBeInTheDocument();
|
||||
const heading = screen.getByRole('heading', { name: '我的项目空间' });
|
||||
expect(heading).toHaveClass('text-left');
|
||||
expect(heading.parentElement).toHaveClass('relative', 'px-3', 'pr-24');
|
||||
expect(heading.parentElement).not.toHaveClass('justify-center');
|
||||
});
|
||||
|
||||
it('uses square corners for each project contact card', () => {
|
||||
renderSidebar();
|
||||
|
||||
const contactButton = screen.getByTestId('project-agent-chat-agent-a');
|
||||
const contactCard = contactButton.parentElement?.parentElement;
|
||||
expect(contactCard).toHaveClass('rounded-none');
|
||||
expect(contactCard).not.toHaveClass('rounded-xl');
|
||||
});
|
||||
|
||||
it('shows contacts, keeps pinned contacts first, and opens that contact’s session list', () => {
|
||||
const callbacks = renderSidebar();
|
||||
const contacts = screen.getAllByTestId(/^project-agent-chat-/);
|
||||
expect(contacts[0]).toHaveAttribute('data-testid', 'project-agent-chat-agent-b');
|
||||
expect(screen.getByText('小明')).toBeInTheDocument();
|
||||
expect(screen.getByText('2')).toBeInTheDocument();
|
||||
expect(screen.getByText('有新消息')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('agent-status-row-agent-a')).toContainElement(screen.getByText('2'));
|
||||
expect(screen.queryByText('等待开始')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId('project-agent-chat-agent-a'));
|
||||
expect(callbacks.onSelectAgent).toHaveBeenCalledWith('agent-a');
|
||||
});
|
||||
|
||||
it('expands sessions directly below the selected contact and removes the back/settings actions', () => {
|
||||
const callbacks = renderSidebar({ selectedAgentId: 'agent-a' });
|
||||
|
||||
expect(screen.getByTestId('project-agent-chat-agent-a')).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(screen.getByLabelText('小明的会话列表')).toBeInTheDocument();
|
||||
expect(screen.getByText('第一次聊天')).toBeInTheDocument();
|
||||
expect(screen.queryByText('小明的对话')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '返回联系人列表' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '联系人设置' })).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId('project-agent-chat-agent-a'));
|
||||
expect(callbacks.onSelectAgent).toHaveBeenCalledWith('agent-a');
|
||||
});
|
||||
|
||||
it('marks the selected session so the active conversation remains visible', () => {
|
||||
renderSidebar({ selectedAgentId: 'agent-a', selectedSessionId: 'ses-a' });
|
||||
|
||||
expect(screen.getByText('第一次聊天').closest('button')).toHaveAttribute('aria-current', 'page');
|
||||
});
|
||||
|
||||
it('keeps the contact disabled while opening without showing a transient work status', () => {
|
||||
renderSidebar({ startingAgentId: 'agent-a' });
|
||||
|
||||
const contact = screen.getByTestId('project-agent-chat-agent-a');
|
||||
expect(screen.queryByText('处理中')).not.toBeInTheDocument();
|
||||
expect(contact).toBeDisabled();
|
||||
expect(contact).toHaveAttribute('aria-busy', 'true');
|
||||
});
|
||||
|
||||
it('hides the recent conversation status after the session has been read', () => {
|
||||
renderSidebar({
|
||||
conversationSessions: [metadata('ses-a', 'agent-a', { unreadCount: 0 })],
|
||||
});
|
||||
|
||||
expect(screen.queryByText('最近有对话')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('等待开始')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('centers the pin and archive actions vertically on the contact card', () => {
|
||||
renderSidebar();
|
||||
|
||||
const pinButton = screen.getByRole('button', { name: '置顶联系人' });
|
||||
const actionRail = pinButton.parentElement;
|
||||
expect(actionRail).toHaveClass('top-1/2', '-translate-y-1/2', 'flex-col');
|
||||
});
|
||||
|
||||
it('lists sessions compactly, shows five by default, and expands the rest', () => {
|
||||
const longMessage = '这是最新的一条消息,内容很长,超出侧栏宽度后应该隐藏并显示省略号。';
|
||||
const allSessions = Array.from({ length: 6 }, (_, index) => session(`ses-${index + 1}`, `会话${index + 1}`));
|
||||
renderSidebar({
|
||||
sessions: allSessions,
|
||||
sessionMessagesBySessionId: {
|
||||
'ses-1': [{ role: 'user', content: longMessage }],
|
||||
},
|
||||
conversationSessions: allSessions.map((item, index) => metadata(item.id as string, 'agent-a', {
|
||||
updatedAt: `2026-07-${String(30 - index).padStart(2, '0')}T00:02:00.000Z`,
|
||||
})),
|
||||
selectedAgentId: 'agent-a',
|
||||
});
|
||||
|
||||
expect(screen.getByTitle(longMessage)).toHaveTextContent(/…$/u);
|
||||
expect(screen.queryByText('其他会话')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('更多会话(1)')).toBeInTheDocument();
|
||||
const moreSessions = screen.getByText('更多会话(1)').closest('details');
|
||||
expect(moreSessions).not.toHaveAttribute('open');
|
||||
|
||||
fireEvent.click(screen.getByText('更多会话(1)'));
|
||||
expect(moreSessions).toHaveAttribute('open');
|
||||
});
|
||||
|
||||
it('removes the session bubble icon and keeps archive hidden until the card is hovered', () => {
|
||||
renderSidebar({ selectedAgentId: 'agent-a' });
|
||||
|
||||
const preview = screen.getByText('第一次聊天');
|
||||
const sessionButton = preview.closest('button');
|
||||
const archiveButton = screen.getByRole('button', { name: '归档会话 第一次聊天' });
|
||||
expect(sessionButton?.querySelector('svg')).toBeNull();
|
||||
expect(archiveButton).toHaveClass('opacity-0', 'group-hover/session-card:opacity-100');
|
||||
});
|
||||
|
||||
it('creates a contact from the required basics and confirms archive actions', async () => {
|
||||
const callbacks = renderSidebar();
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建联系人' }));
|
||||
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.click(screen.getByRole('button', { name: '创建并开始对话' }));
|
||||
await waitFor(() => expect(callbacks.onCreateAgent).toHaveBeenCalledWith({
|
||||
name: '小李',
|
||||
avatarId: 'avatar-03',
|
||||
model: modelOption.modelRef,
|
||||
responsibility: '负责整理资料。',
|
||||
prompt: '',
|
||||
skillIds: [],
|
||||
}));
|
||||
|
||||
fireEvent.click(screen.getByLabelText('归档联系人 小明'));
|
||||
expect(screen.getByRole('dialog', { name: '归档联系人“小明”?' })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '取消' }));
|
||||
expect(callbacks.onArchiveAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows advanced creation settings directly below the basics', () => {
|
||||
renderSidebar();
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建联系人' }));
|
||||
|
||||
expect(screen.getByText('高级设置(提示词与技能)')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Agent 提示词')).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: '绑定技能' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('places archived contacts and sessions below active items and exposes restore actions', () => {
|
||||
const archivedAgent = agent('agent-archived', '旧伙伴', { archivedAt: '2026-07-29T00:00:00.000Z' });
|
||||
renderSidebar({
|
||||
agents: [agent('agent-a', '小明'), archivedAgent],
|
||||
conversationSessions: [
|
||||
metadata('ses-a', 'agent-a'),
|
||||
metadata('ses-old', 'agent-a', { archivedAt: '2026-07-29T00:00:00.000Z' }),
|
||||
],
|
||||
selectedAgentId: 'agent-a',
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /已归档联系人/ }));
|
||||
expect(screen.getByText('旧伙伴')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '恢复联系人 旧伙伴' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '永久删除联系人 旧伙伴' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '恢复会话 新对话' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '永久删除会话 新对话' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('requires explicit confirmation before permanently deleting an archived session', async () => {
|
||||
const callbacks = renderSidebar({
|
||||
selectedAgentId: 'agent-a',
|
||||
conversationSessions: [metadata('ses-old', 'agent-a', { archivedAt: '2026-07-29T00:00:00.000Z' })],
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('已归档会话(1)'));
|
||||
fireEvent.click(screen.getByRole('button', { name: '永久删除会话 新对话' }));
|
||||
expect(screen.getByRole('dialog', { name: '永久删除会话“新对话”?' })).toBeInTheDocument();
|
||||
expect(callbacks.onDeleteSession).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button', { name: '永久删除' }));
|
||||
await waitFor(() => expect(callbacks.onDeleteSession).toHaveBeenCalledWith('ses-old'));
|
||||
});
|
||||
|
||||
it('requires explicit confirmation before permanently deleting archived records', async () => {
|
||||
const archivedAgent = agent('agent-archived', '旧伙伴', { archivedAt: '2026-07-29T00:00:00.000Z' });
|
||||
const callbacks = renderSidebar({
|
||||
agents: [archivedAgent],
|
||||
conversationSessions: [metadata('ses-old', 'agent-archived', { archivedAt: '2026-07-29T00:00:00.000Z' })],
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /已归档联系人/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '永久删除联系人 旧伙伴' }));
|
||||
expect(screen.getByRole('dialog', { name: '永久删除联系人“旧伙伴”?' })).toBeInTheDocument();
|
||||
expect(callbacks.onDeleteAgent).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button', { name: '永久删除' }));
|
||||
await waitFor(() => expect(callbacks.onDeleteAgent).toHaveBeenCalledWith(archivedAgent));
|
||||
});
|
||||
|
||||
it('keeps permanent deletion unavailable for archived built-in contacts', () => {
|
||||
renderSidebar({
|
||||
agents: [agent('built-in-archived', '内置伙伴', { builtIn: true, archivedAt: '2026-07-29T00:00:00.000Z' })],
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /已归档联系人/ }));
|
||||
expect(screen.queryByRole('button', { name: '永久删除联系人 内置伙伴' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user