fix: render consultation replies and typed tool activity
This commit is contained in:
@@ -96,6 +96,19 @@ async function ready() {
|
||||
await waitFor(() => expect(streams.has('first')).toBe(true));
|
||||
}
|
||||
describe('teacher side chat', () => {
|
||||
it('renders assistant Markdown, tables, code and images instead of literal markup', async () => {
|
||||
const response = '## 项目建议\n\n**先验证跳跃**\n\n- 保留现有玩法\n\n| 文件 | 用途 |\n| --- | --- |\n| main.ts | 入口 |\n\n```json\n{"name":"flybot"}\n```\n\n[说明](https://example.com/readme)\n\n';
|
||||
api.read.mockResolvedValue({ ...first, requests: [request({ response })] });
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await ready();
|
||||
expect(screen.getByRole('heading', { name: '项目建议' })).toBeVisible();
|
||||
expect(screen.getByRole('table')).toHaveTextContent('main.ts');
|
||||
expect(screen.getByText('保留现有玩法').closest('li')).not.toBeNull();
|
||||
expect(screen.getByText('{"name":"flybot"}').closest('pre')).not.toBeNull();
|
||||
expect(screen.getByRole('link', { name: '说明' })).toHaveAttribute('href', 'https://example.com/readme');
|
||||
expect(screen.getByRole('img', { name: '项目截图' })).toHaveAttribute('src', 'https://example.com/preview.png');
|
||||
});
|
||||
|
||||
it('does not render a zero for an empty legacy friend draft', async () => {
|
||||
localStorage.setItem('makelore-consultation-draft:signed-out:p:friend', JSON.stringify({ text: '', references: [] }));
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
@@ -105,6 +118,68 @@ describe('teacher side chat', () => {
|
||||
expect(screen.queryByText('查看旧版朋友草稿')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps malformed originals folded and literal while rendering the recovered answer', async () => {
|
||||
const raw = '{"reply":"正文已保留","tool":<script>alert(1)</script>';
|
||||
api.read.mockResolvedValue({ ...first, requests: [request({
|
||||
response: '**正文已保留**', discussionError: '这次整理没有完成,先保留原来的内容。', unparsedResponse: raw,
|
||||
})] });
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await ready();
|
||||
expect(screen.getByText('正文已保留').tagName).toBe('STRONG');
|
||||
const details = screen.getByText('查看收到的原始内容').closest('details');
|
||||
expect(details).not.toHaveAttribute('open');
|
||||
expect(details?.querySelector('pre')?.textContent).toBe(raw);
|
||||
expect(details?.querySelector('script')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders formulas and preserves code escapes while rejecting executable links and HTML', async () => {
|
||||
api.read.mockResolvedValue({ ...first, requests: [request({
|
||||
response: '$E=mc^2$\n\n```js\nconst value = "\\n";\n```\n\n[危险](javascript:alert)\n\n<script>alert(1)</script>\n\n\n\n',
|
||||
})] });
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await ready();
|
||||
const body = screen.getByTestId('teacher-reply');
|
||||
expect(body.querySelector('.katex')).not.toBeNull();
|
||||
expect(body.querySelector('pre')?.textContent).toContain('const value = "\\n";');
|
||||
expect(body.querySelector('script')).toBeNull();
|
||||
expect(screen.queryByRole('link', { name: '危险' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('img', { name: '本地图片' })).not.toBeInTheDocument();
|
||||
fireEvent.error(screen.getByRole('img', { name: '坏图片' }));
|
||||
expect(screen.getByText('[图片未能显示:坏图片]')).toBeVisible();
|
||||
});
|
||||
|
||||
it('updates streamed Markdown without duplicating the final answer', async () => {
|
||||
const running = { ...first, requests: [request({ status: 'running', response: '## 正在回答\n\n**先检查' })] };
|
||||
api.read.mockResolvedValue(running);
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await ready();
|
||||
expect(screen.getByRole('heading', { name: '正在回答' })).toBeVisible();
|
||||
const completed = { ...running, revision: 2, requests: [request({ response: '## 正在回答\n\n**先检查文件**' })] };
|
||||
await act(async () => streams.get('first')!.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify(completed) })));
|
||||
expect(screen.getAllByTestId('teacher-reply')).toHaveLength(1);
|
||||
expect(screen.getByText('先检查文件').tagName).toBe('STRONG');
|
||||
});
|
||||
|
||||
it('keeps tool activity separate from the answer and stops stale running indicators', async () => {
|
||||
api.read.mockResolvedValue({ ...first, requests: [request({
|
||||
response: '已经看过项目文件。',
|
||||
toolActivity: [
|
||||
{ id: 'one', name: 'read_project_file', status: 'completed' },
|
||||
{ id: 'two', name: 'read_conversation', status: 'failed' },
|
||||
{ id: 'three', name: 'cloud-search', status: 'running' },
|
||||
],
|
||||
})] });
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await ready();
|
||||
const details = screen.getByText('使用了 3 项工具').closest('details')!;
|
||||
expect(details).not.toHaveAttribute('open');
|
||||
expect(details).toHaveTextContent('读取项目文件已完成');
|
||||
expect(details).toHaveTextContent('读取会话原文未完成');
|
||||
expect(details).toHaveTextContent('未收到执行结果');
|
||||
expect(screen.getByTestId('teacher-reply')).toHaveTextContent('已经看过项目文件。');
|
||||
expect(screen.getByTestId('teacher-reply')).not.toHaveTextContent('读取会话原文');
|
||||
});
|
||||
|
||||
it('retains failed input and retries with the same request id, then appends a reply only on click', async () => {
|
||||
const bringBack = vi.fn();
|
||||
api.send
|
||||
|
||||
Reference in New Issue
Block a user