fix: toggle the teacher dock from the conversation header

This commit is contained in:
鲨鱼辣椒
2026-09-29 17:00:06 +08:00
parent 5de8ea823d
commit 2922f35e4b
6 changed files with 132 additions and 28 deletions

View File

@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { AppError } from '@/lib/error-model';
import type { ConversationSnapshot } from '@/types/coding-conversation';
import type { TeacherDefinition, TeacherTopic } from '../../shared/coding-teacher';
import type {
CodingConversationMetadata,
CodingProjectAgent,
@@ -320,6 +321,93 @@ describe('CodingChatPanel first Conversation', () => {
expect(conversationApi.submit).not.toHaveBeenCalled();
});
it('toggles the teacher dock from the header without changing tabs, drafts, history, or sending prompts', async () => {
localStorage.clear();
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
projectApi.config.mockResolvedValue({ project, config });
projectApi.conversations.mockResolvedValue([conversation]);
conversationApi.events.mockResolvedValue(new FakeEventSource() as unknown as EventSource);
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
conversationApi.snapshot.mockResolvedValue(createLocalConversationSnapshot(project.id, conversation));
const definition: TeacherDefinition = {
schema_version: 1, config_id: 'teacher-collapse', teacher_id: 'teacher', name: '设计老师',
description: '', avatar_id: 'avatar-01', welcome_message: '聊聊吧', suggested_questions: [],
system_prompt: '', skills: [], model: { model_id: 'model', reasoning_choice: { mode: 'default' } },
limits: { max_input_tokens: 8000, max_output_tokens: 1000 },
};
const topic: TeacherTopic = {
schemaVersion: 1, revision: 1, id: 'teacher-history', accountId: 'signed-out',
projectId: project.id, sourceConversationId: conversation.id, definition, version: 1,
createdAt: '2026-09-29T00:00:00.000Z', updatedAt: '2026-09-29T00:00:00.000Z',
requests: [{
id: 'teacher-reply-1', text: '怎样让规则更清楚?', references: [],
createdAt: '2026-09-29T00:00:00.000Z', sourceCursor: { seq: 0, workerGeneration: 0 },
sourceCapturedAt: '2026-09-29T00:00:00.000Z', includedSourceMessageIds: [], omittedMessages: 0,
status: 'completed', response: '先用一句话说明获胜条件。',
projectId: project.id, sourceConversationId: conversation.id, teacherVersion: 1,
}],
conversation: { agentId: 'teacher-collapse', segmentTurns: 1, discussions: {}, seenThrough: 'teacher-reply-1' },
};
const { teacherApi } = await import('@/lib/coding-teacher');
vi.spyOn(teacherApi, 'config').mockResolvedValue({ enabled: true, published_version: 1, revision: 1, definition });
vi.spyOn(teacherApi, 'catalog').mockResolvedValue({ items: [{ teacher_id: 'teacher-collapse', version: 1, definition, is_default: true }] });
vi.spyOn(teacherApi, 'conversation').mockResolvedValue({ topic, before: null });
vi.spyOn(teacherApi, 'events').mockResolvedValue(new FakeEventSource() as unknown as EventSource);
const consultationSend = vi.spyOn(teacherApi, 'sendConversation');
const legacySend = vi.spyOn(teacherApi, 'send');
const consultationCreate = vi.spyOn(teacherApi, 'create');
const browserApi = await import('@/lib/agent-browser');
const closedBrowser = {
browserId: null, projectId: project.id, projectPath: null, state: 'closed' as const,
generation: 0, url: '', title: '', visible: false, bounds: null,
canGoBack: false, canGoForward: false, eventCursor: 0,
};
vi.spyOn(browserApi, 'getAgentBrowserState').mockResolvedValue(closedBrowser);
vi.spyOn(browserApi, 'closeAgentBrowser').mockResolvedValue(closedBrowser);
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
render(<CodingChatPanel />);
const composer = within(await screen.findByTestId('coding-message-composer')).getByRole('textbox');
fireEvent.change(composer, { target: { value: '操作对话还没写完' } });
await waitFor(() => expect(screen.getByRole('button', { name: '智能体', exact: true })).toHaveTextContent('设计老师'));
const chatTab = screen.getByRole('tab', { name: '操作对话' });
const workTab = screen.getByRole('tab', { name: '作品', exact: true });
const toggle = screen.getByRole('button', { name: '展开老师对话栏' });
expect(toggle).toHaveAttribute('aria-expanded', 'false');
expect(toggle).toHaveAttribute('aria-controls', 'coding-consultation-dock');
fireEvent.click(toggle);
expect(screen.getByRole('button', { name: '收起老师对话栏' })).toHaveAttribute('aria-expanded', 'true');
expect(document.getElementById('coding-consultation-dock')).toBeVisible();
expect(chatTab).toHaveAttribute('aria-selected', 'true');
expect(composer).toBeVisible();
expect(await screen.findByText('先用一句话说明获胜条件。')).toBeVisible();
const teacherInput = screen.getByRole('textbox', { name: '向智能体提问' });
await waitFor(() => expect(teacherInput).toBeEnabled());
fireEvent.change(teacherInput, { target: { value: '老师对话也还没写完' } });
fireEvent.click(screen.getByRole('button', { name: '收起老师对话栏' }));
expect(document.getElementById('coding-consultation-dock')).toBeNull();
expect(screen.getByRole('button', { name: '展开老师对话栏' })).toHaveAttribute('aria-expanded', 'false');
expect(chatTab).toHaveAttribute('aria-selected', 'true');
expect(composer).toHaveValue('操作对话还没写完');
fireEvent.click(workTab);
fireEvent.click(screen.getByRole('button', { name: '展开老师对话栏' }));
expect(workTab).toHaveAttribute('aria-selected', 'true');
expect(screen.getByRole('tabpanel', { name: '作品', exact: true })).toBeVisible();
expect(screen.getByRole('textbox', { name: '向智能体提问' })).toHaveValue('老师对话也还没写完');
expect(await screen.findByText('先用一句话说明获胜条件。')).toBeVisible();
fireEvent.click(screen.getByRole('button', { name: '收起老师对话栏' }));
expect(workTab).toHaveAttribute('aria-selected', 'true');
expect(screen.queryByRole('textbox', { name: '向智能体提问' })).not.toBeInTheDocument();
fireEvent.click(chatTab);
expect(composer).toHaveValue('操作对话还没写完');
expect(consultationCreate).not.toHaveBeenCalled();
expect(consultationSend).not.toHaveBeenCalled();
expect(legacySend).not.toHaveBeenCalled();
expect(conversationApi.submit).not.toHaveBeenCalled();
});
it('archives the last conversation without creating another, then restores the same running conversation', async () => {
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
projectApi.config.mockResolvedValue({ project, config });