Files
makelore/tests/unit/agent-conversation-sidebar.test.tsx
inman 80e8386fa6
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled
feat: update Makelore modules and conversations
2026-07-31 10:08:41 +08:00

305 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 contacts 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();
});
});