Merge Yuxi teacher support with classroom discussions
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

This commit is contained in:
鲨鱼辣椒
2026-09-24 10:27:36 +08:00
34 changed files with 2333 additions and 105 deletions

View File

@@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { TeacherChatPanel } from '@/pages/Chat/TeacherChatPanel';
import type { TeacherDefinition, TeacherRequest, TeacherSend, TeacherTopic } from '../../shared/coding-teacher';
const api = vi.hoisted(() => ({
catalog: vi.fn(),
config: vi.fn(),
preview: vi.fn(),
list: vi.fn(),
@@ -67,6 +68,7 @@ beforeEach(() => {
localStorage.clear();
streams = new Map();
api.config.mockResolvedValue({ enabled: true, published_version: 1, revision: 1, definition });
api.catalog.mockResolvedValue({ items: [{ teacher_id: 'one', version: 1, definition, is_default: true }] });
api.list.mockResolvedValue({
items: [
{ id: 'first', title: '第一个' },
@@ -108,6 +110,7 @@ describe('teacher side chat', () => {
sourceCapturedAt: 'now',
includedSourceMessageIds: [],
omittedMessages: 0,
truncatedMessages: 1,
status: 'completed',
response: '教学回答',
},
@@ -121,6 +124,8 @@ describe('teacher side chat', () => {
expect(screen.getByLabelText('向老师提问')).toHaveValue('解释代码');
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
await screen.findByText('教学回答');
expect(screen.getByText('结合当前会话和项目文件答疑')).toBeVisible();
expect(screen.getByText('较长的上下文已节选,老师可按需读取原文。')).toBeVisible();
expect(api.send.mock.calls[0][2].requestId).toBe(api.send.mock.calls[1][2].requestId);
expect(bringBack).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: '带回主会话草稿' }));
@@ -446,7 +451,7 @@ describe('teacher side chat', () => {
expect(api.send).not.toHaveBeenCalled();
expect(screen.queryByRole('button', { name: '老师帮我看看', exact: true })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '老师新话题' }));
await waitFor(() => expect(api.create).toHaveBeenCalledWith('preview/sample', 7, '学生的练习代码'));
await waitFor(() => expect(api.create).toHaveBeenCalledWith('preview/sample', 7, '学生的练习代码', undefined));
});
});
@@ -551,3 +556,49 @@ it('comparison focuses and directly sends the selected difference while preservi
expect(screen.getByLabelText('向老师提问')).toHaveValue('自己还没说完的话');
expect(screen.getAllByTestId('teacher-discussion')).toHaveLength(1);
});
describe('merged cloud teacher classroom', () => {
it('selects a cloud teacher for the first project discussion without sending the operation draft', async () => {
const cloudDefinition = { ...definition, runtime: 'yuxi' as const, config_id: 'algorithm', name: '算法老师', yuxi: { agent_slug: 'algorithm', agent_version: 2 } };
api.list.mockResolvedValue({ items: [], lastSelectedTopicId: null });
api.catalog.mockResolvedValue({ items: [
{ teacher_id: 'one', version: 1, definition, is_default: true },
{ teacher_id: 'algorithm', version: 9, definition: cloudDefinition, is_default: false },
] });
const selected = { ...topic('selected'), sourceConversationId: 'project', role: 'teacher' as const, definition: cloudDefinition, version: 9 };
api.create.mockResolvedValue(selected);
api.send.mockImplementation(async (_base, _id, input: TeacherSend) => ({ ...selected, revision: 2, requests: [request({ id: input.requestId, text: input.text, response: '先看看玩家会做什么。' })] }));
render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
await waitFor(() => expect(screen.getByLabelText('新话题使用的老师')).toBeEnabled());
expect(api.create).not.toHaveBeenCalled();
fireEvent.change(screen.getByLabelText('新话题使用的老师'), { target: { value: '9' } });
fireEvent.change(screen.getByLabelText('向老师提问'), { target: { value: '我想做一个小游戏' } });
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
await screen.findByText('先看看玩家会做什么。');
expect(api.create).toHaveBeenCalledWith('p/teacher', undefined, undefined, 9);
expect(api.send.mock.calls[0][2]).toMatchObject({ sourceConversationId: 'c', presentation: 'discussion-v1' });
expect(screen.getByRole('heading', { name: '算法老师' })).toBeVisible();
});
it('does not enable a disabled cloud topic through another teacher or an Enter shortcut', async () => {
api.read.mockResolvedValue({ ...first, definition: { ...definition, config_id: 'disabled-teacher', runtime: 'yuxi', yuxi: { agent_slug: 'old', agent_version: 1 } } });
render(<TeacherChatPanel projectId="p" sourceId="c" role="teacher" />);
await waitFor(() => expect(screen.getByText('老师暂未开放,历史仍可查看。')).toBeVisible());
const input = screen.getByLabelText('向老师提问');
fireEvent.change(input, { target: { value: '继续讨论' } });
expect(screen.getByRole('button', { name: '提问', exact: true })).toBeDisabled();
expect(screen.getByRole('button', { name: '老师帮我看看', exact: true })).toBeDisabled();
fireEvent.keyDown(input, { key: 'Enter' });
expect(api.send).not.toHaveBeenCalled();
});
it('keeps the friend identity independent of the teacher catalog', async () => {
api.list.mockResolvedValue({ items: [], lastSelectedTopicId: null });
api.config.mockResolvedValue({ enabled: true, definition: { ...definition, teacher_id: 'coding-friend', name: '小麦', welcome_message: '一起看看你的作品' } });
render(<TeacherChatPanel projectId="p" sourceId="c" role="friend" />);
await waitFor(() => expect(screen.getByLabelText('向朋友提问')).toBeEnabled());
expect(screen.getByRole('heading', { name: '小麦', exact: true })).toBeVisible();
expect(screen.queryByLabelText('新话题使用的老师')).toBeNull();
});
});