fix: isolate teacher references from unrelated selections
This commit is contained in:
@@ -321,6 +321,108 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
expect(conversationApi.submit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
async function renderQuotationWorkspace() {
|
||||
localStorage.clear();
|
||||
window.getSelection()?.removeAllRanges();
|
||||
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(withForkSource(createLocalConversationSnapshot(project.id, conversation), 'quote-source'));
|
||||
const definitions: TeacherDefinition[] = ['测试Alice对比', '产品工程师-Alice'].map((name, index) => ({
|
||||
schema_version: 1, config_id: `quote-agent-${index}`, 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 topics = definitions.map((definition, index): TeacherTopic => ({
|
||||
schemaVersion: 1, revision: 1, id: `quote-history-${index}`, accountId: 'signed-out',
|
||||
projectId: project.id, sourceConversationId: conversation.id, definition, version: 1,
|
||||
createdAt: '2026-09-30T00:00:00.000Z', updatedAt: '2026-09-30T00:00:00.000Z',
|
||||
requests: [{ id: `quote-reply-${index}`, text: '看看项目', references: [], createdAt: '2026-09-30T00:00:00.000Z',
|
||||
sourceCursor: { seq: 0, workerGeneration: 0 }, sourceCapturedAt: '2026-09-30T00:00:00.000Z',
|
||||
includedSourceMessageIds: [], omittedMessages: 0, status: 'completed',
|
||||
response: index ? '这是产品老师的回复' : '只有测试老师里的内容',
|
||||
projectId: project.id, sourceConversationId: conversation.id, teacherVersion: 1 }],
|
||||
conversation: { agentId: definition.config_id!, segmentTurns: 1, discussions: {}, seenThrough: `quote-reply-${index}` },
|
||||
}));
|
||||
const { teacherApi } = await import('@/lib/coding-teacher');
|
||||
vi.spyOn(teacherApi, 'config').mockResolvedValue({ enabled: true, published_version: 1, revision: 1, definition: definitions[0] });
|
||||
vi.spyOn(teacherApi, 'catalog').mockResolvedValue({ items: definitions.map((definition, index) => ({ teacher_id: definition.config_id!, version: 1, definition, is_default: index === 0 })) });
|
||||
vi.spyOn(teacherApi, 'conversation').mockImplementation(async (_projectId, id) => ({ topic: topics.find(topic => topic.definition.config_id === id)!, before: null }));
|
||||
vi.spyOn(teacherApi, 'events').mockResolvedValue(new FakeEventSource() as unknown as EventSource);
|
||||
const send = vi.spyOn(teacherApi, 'sendConversation').mockImplementation(async (_projectId, id, input) => {
|
||||
const topic = topics.find(topic => topic.definition.config_id === id)!;
|
||||
return { ...topic, revision: 2, requests: [...topic.requests, { ...topic.requests[0], id: input.requestId, text: input.text, references: input.references ?? [], response: '这次提问已收到' }] };
|
||||
});
|
||||
const browserApi = await import('@/lib/agent-browser');
|
||||
const browser = { 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(browser);
|
||||
vi.spyOn(browserApi, 'closeAgentBrowser').mockResolvedValue(browser);
|
||||
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
|
||||
render(<CodingChatPanel />);
|
||||
await screen.findByRole('button', { name: '与测试Alice对比聊天' });
|
||||
await screen.findByText('Fork source');
|
||||
return { send };
|
||||
}
|
||||
|
||||
function selectText(element: Element) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
window.getSelection()!.removeAllRanges();
|
||||
window.getSelection()!.addRange(range);
|
||||
}
|
||||
|
||||
it('does not capture one teacher message into another teacher draft or request', async () => {
|
||||
const { send } = await renderQuotationWorkspace();
|
||||
fireEvent.click(screen.getByRole('button', { name: '与测试Alice对比聊天' }));
|
||||
selectText(await screen.findByText('只有测试老师里的内容'));
|
||||
fireEvent.click(screen.getByRole('button', { name: '与产品工程师-Alice聊天' }));
|
||||
await screen.findByText('这是产品老师的回复');
|
||||
const input = screen.getByRole('textbox', { name: '向智能体提问' });
|
||||
await waitFor(() => expect(input).toBeEnabled());
|
||||
expect(screen.queryByRole('button', { name: '移除引用' })).not.toBeInTheDocument();
|
||||
expect(JSON.parse(localStorage.getItem(`makelore-consultation-draft:signed-out:project:${project.id}:agent:quote-agent-1`)!)).toMatchObject({ references: [] });
|
||||
fireEvent.change(input, { target: { value: '产品老师帮我看看' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
|
||||
await waitFor(() => expect(send).toHaveBeenCalledWith(project.id, 'quote-agent-1', expect.objectContaining({ text: '产品老师帮我看看', references: [] })));
|
||||
});
|
||||
|
||||
it('keeps an operation quote with its recipient and does not resurrect it after removal or sending', async () => {
|
||||
const { send } = await renderQuotationWorkspace();
|
||||
selectText(screen.getByText('Fork source'));
|
||||
fireEvent.click(screen.getByRole('button', { name: '与测试Alice对比聊天' }));
|
||||
await waitFor(() => expect(screen.getByRole('textbox', { name: '向智能体提问' })).toBeEnabled());
|
||||
expect(screen.getByRole('button', { name: '移除引用' })).toBeVisible();
|
||||
fireEvent.click(screen.getByRole('button', { name: '与产品工程师-Alice聊天' }));
|
||||
await screen.findByText('这是产品老师的回复');
|
||||
expect(screen.queryByRole('button', { name: '移除引用' })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '与测试Alice对比聊天' }));
|
||||
await screen.findByText('只有测试老师里的内容');
|
||||
fireEvent.click(screen.getByRole('button', { name: '移除引用' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '收起老师对话栏' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '展开老师对话栏' }));
|
||||
await waitFor(() => expect(screen.getByRole('textbox', { name: '向智能体提问' })).toBeEnabled());
|
||||
expect(screen.queryByRole('button', { name: '移除引用' })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('tab', { name: '操作对话' }));
|
||||
selectText(screen.getByText('Fork source'));
|
||||
fireEvent.click(screen.getByRole('button', { name: '与产品工程师-Alice聊天' }));
|
||||
const input = screen.getByRole('textbox', { name: '向智能体提问' });
|
||||
await waitFor(() => expect(input).toBeEnabled());
|
||||
fireEvent.change(input, { target: { value: '解释这段操作内容' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '提问', exact: true }));
|
||||
await waitFor(() => expect(send).toHaveBeenCalledWith(project.id, 'quote-agent-1', expect.objectContaining({ references: [{ kind: 'code', text: 'Fork source' }] })));
|
||||
await screen.findByText('这次提问已收到');
|
||||
fireEvent.click(screen.getByRole('button', { name: '收起老师对话栏' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '展开老师对话栏' }));
|
||||
await waitFor(() => expect(screen.getByRole('textbox', { name: '向智能体提问' })).toBeEnabled());
|
||||
expect(screen.queryByRole('button', { name: '移除引用' })).not.toBeInTheDocument();
|
||||
expect(send).toHaveBeenCalledTimes(1);
|
||||
window.getSelection()?.removeAllRanges();
|
||||
});
|
||||
|
||||
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 });
|
||||
|
||||
Reference in New Issue
Block a user