feat(teacher): stream Yuxi reasoning and answer previews

This commit is contained in:
2026-09-29 18:18:56 +08:00
parent f443f3bc66
commit 8ccc4650a2
12 changed files with 364 additions and 42 deletions

View File

@@ -26,6 +26,8 @@ type ConsultationReply = {
suggestedQuestions: string[];
replyParseError?: string;
unparsedResponse?: string;
status?: 'running' | 'completed';
reasoning?: string;
};
type TeacherCatalogFixture = {
@@ -2575,3 +2577,56 @@ test('work failure offers a useful recovery and retains the student draft after
expect(new Set(opens.map((item) => item.body?.request_id)).size).toBe(2);
expect(requests.filter((item) => item.path.endsWith('/prompt'))).toHaveLength(0);
});
test('teacher reasoning and answer stream separately before the final response', async ({ launchElectronApp }) => {
const app = await launchElectronApp({ skipSetup: true });
let page = await getStableWindow(app);
const connection = await page.evaluate(async () => ({
token: await window.electron.ipcRenderer.invoke('hostapi:token') as string,
baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string,
}));
await installCodingFirstChatHost(app, connection, true, false, false, undefined, false, {
response: '', suggestedQuestions: [], status: 'running', reasoning: '先观察项目代码。',
});
await settleSnapshot(app);
await disableCodingEventSource(page);
try {
await page.reload(); page = await getStableWindow(app);
await page.getByTestId('ai-module-option-programming').click();
await page.evaluate(() => { window.location.hash = '/chat'; });
await page.getByRole('button', { name: '与代码智能体聊天', exact: true }).click();
const teacher = page.getByTestId('teacher-chat-panel');
await teacher.getByRole('textbox', { name: '向智能体提问' }).fill('帮我看看项目');
await teacher.getByRole('button', { name: '提问', exact: true }).click();
const reasoning = teacher.getByTestId('teacher-reasoning');
await expect(reasoning).toHaveAttribute('open');
await expect(reasoning.getByText('先观察项目代码。', { exact: true })).toBeVisible();
await expect(teacher.getByTestId('teacher-reply')).toHaveCount(0);
const send = (await readState(app)).captured.find(item => item.path.endsWith('/messages') && item.method === 'POST')!;
const current = await page.evaluate(async path => {
const result = await window.electron.ipcRenderer.invoke('hostapi:fetch', { path, method: 'GET' }) as {
data: { json: { topic: import('../../shared/coding-teacher').TeacherTopic } };
};
return result.data.json.topic;
}, send.path.replace(/\/messages$/, ''));
current.requests[0].reasoning += '再检查交互。'; current.revision++;
await emitCodingEvent(page, 'snapshot', current);
await expect(reasoning).toContainText('先观察项目代码。再检查交互。');
current.requests[0].response = '可以先'; current.revision++;
await emitCodingEvent(page, 'snapshot', current);
await expect(teacher.getByTestId('teacher-reply')).toHaveText('可以先');
await expect(teacher.getByRole('button', { name: '停止回复', exact: true })).toBeVisible();
current.requests[0].response = '可以先观察玩家反馈。'; current.revision++;
await emitCodingEvent(page, 'snapshot', current);
await expect(teacher.getByTestId('teacher-reply')).toHaveText('可以先观察玩家反馈。');
await teacher.screenshot({ path: test.info().outputPath('teacher-streaming.png') });
current.requests[0].status = 'completed'; current.requests[0].suggestedQuestions = ['怎么观察?']; current.revision++;
await emitCodingEvent(page, 'snapshot', current);
await expect(reasoning).not.toHaveAttribute('open');
await reasoning.locator('summary').click();
await expect(reasoning.getByLabel('智能体思考过程')).toBeVisible();
await expect(teacher.getByRole('button', { name: '怎么观察?', exact: true })).toBeVisible();
await expect(teacher.getByTestId('teacher-reply')).not.toContainText('检查交互');
expect((await readState(app)).captured.filter(item => item.path.endsWith('/messages') && item.method === 'POST')).toHaveLength(1);
} finally { await releaseSnapshot(app); }
});