fix: keep teacher runtime prompts within interface boundary

This commit is contained in:
鲨鱼辣椒 committed 2026-09-28 14:38:58 +08:00
1 parent 998796d77c
commit 54a40d2636
12 files changed
+313 -32

No files matched your search

+62 -3
View File
@@ -20,6 +20,13 @@ type HostConnection = {
token: string;
};
type ConsultationReply = {
response: string;
suggestedQuestions: string[];
replyParseError?: string;
unparsedResponse?: string;
};
async function disableCodingEventSource(page: Page): Promise<void> {
await page.addInitScript(() => {
const sources = new Set<LocalEventSource>();
@@ -88,9 +95,10 @@ async function installCodingFirstChatHost(
managedCapabilities = false,
removedModel = false,
audioPreview?: { executionId: string; path: string; dataUrl: string },
consultationReply?: ConsultationReply,
): Promise<void> {
await electronApp.evaluate(async (_, payload) => {
const { connection, featureComplete, managedCapabilities, removedModel, audioPreview } = payload;
const { connection, featureComplete, managedCapabilities, removedModel, audioPreview, consultationReply } = payload;
const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron');
type MainState = {
captured: CapturedRequest[];
@@ -553,7 +561,7 @@ async function installCodingFirstChatHost(
if (path.endsWith(`${topicBase}/${topicId}/messages`) && method === 'POST') {
const suggestionRequest = body!.intent === 'suggestions';
const teacherResponse = suggestionRequest ? '我们可以从你最近试过的地方聊起。' : body!.intent === 'guided-help' ? '你最近做的哪一步,让你停下来想了一会儿?' : '先理解状态如何随点击变化,再修改代码。';
consultationTopics[consultationPath]={...currentTopic,revision:Number(currentTopic?.revision??0)+1,requests:[...(currentTopic?.requests as unknown[]??[]),{id:body!.requestId,text:body!.text,intent:body!.intent,references:body!.references??[],createdAt:now,sourceCursor:{workerGeneration:1,seq:1},sourceCapturedAt:now,includedSourceMessageIds:[],omittedMessages:0,truncatedMessages:1,status:'completed',response:teacherResponse,...(suggestionRequest?{suggestedQuestions:['怎样观察别人玩游戏?','我该先试哪个想法?']}:{})}]};
consultationTopics[consultationPath]={...currentTopic,revision:Number(currentTopic?.revision??0)+1,requests:[...(currentTopic?.requests as unknown[]??[]),{id:body!.requestId,text:body!.text,intent:body!.intent,references:body!.references??[],createdAt:now,sourceCursor:{workerGeneration:1,seq:1},sourceCapturedAt:now,includedSourceMessageIds:[],omittedMessages:0,truncatedMessages:1,status:'completed',response:teacherResponse,...(suggestionRequest?{suggestedQuestions:['怎样观察别人玩游戏?','我该先试哪个想法?']}:{}),...consultationReply}]};
return respond(consultationTopics[consultationPath],202);
}
if (path.endsWith(`${topicBase}/${topicId}`)) return respond(currentTopic);
@@ -798,7 +806,7 @@ async function installCodingFirstChatHost(
if (path === '/api/coding/runtime/diagnostics') return respond({ runtime: { revision: { provider: 1, resources: 1 }, workers: [{ conversationId: conversation.id, generation: 1, state: 'running', stage: 'running' }] } });
return respond({ success: false, error: `Unhandled E2E route: ${method} ${path}` }, 404);
});
}, { connection: hostConnection, featureComplete, managedCapabilities, removedModel, audioPreview });
}, { connection: hostConnection, featureComplete, managedCapabilities, removedModel, audioPreview, consultationReply });
}
test('saved Game Audio offers click-only playable local preview in Electron', async ({ launchElectronApp }, testInfo) => {
@@ -1869,6 +1877,57 @@ test('project consultations preserve student drafts and switch between work and
});
test('project consultations show reply protocol errors without exposing rejected shortcuts', async ({ launchElectronApp }) => {
const electronApp = await launchElectronApp({ skipSetup: true });
let page = await getStableWindow(electronApp);
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 }));
const response = '可以先观察玩家在哪里停下来,再聊聊你想改的地方。';
const quickReplies = ['看懂规则|怎样观察玩家?', '想想作用|我想聊聊按钮。', '试试结果|怎样看出变化?', '<button>原始选项</button> **不要渲染**'];
const replyParseError = '快捷回复超过当前界面最多 3 条的限制,正文已保留,快捷回复未展示。';
const unparsedResponse = '\n' + JSON.stringify({ reply: response, quickReplies }, null, 2) + '\n';
await installCodingFirstChatHost(electronApp, connection, true, false, false, undefined, {
response, suggestedQuestions: [], replyParseError, unparsedResponse,
});
await settleSnapshot(electronApp);
await disableCodingEventSource(page);
try {
await page.reload(); page = await getStableWindow(electronApp);
await page.getByTestId('ai-module-option-programming').click();
await page.evaluate(() => { window.location.hash = '/chat'; });
await expect(page.getByTestId('project-conversations')).toBeVisible();
const composer = page.getByTestId('coding-message-composer').getByRole('textbox', { includeHidden: true });
await composer.fill('保留操作草稿');
await page.getByRole('button', { name: '与朋友聊天', exact: true }).click();
const teacher = page.getByTestId('teacher-chat-panel');
const teacherComposer = teacher.getByRole('textbox', { name: '向智能体提问' });
await teacherComposer.fill('还没说完的困惑');
await teacher.getByRole('button', { name: '帮我看看', exact: true }).click();
const reply = teacher.locator('.consultation-messages > div').filter({ has: page.getByText(response, { exact: true }) });
await expect(reply.getByText(response, { exact: true })).toBeVisible();
await expect(reply.getByRole('status')).toHaveText(replyParseError);
await expect(reply.getByRole('status')).toBeVisible();
await expect(teacher.locator('.consultation-follow-up')).toHaveCount(0);
for (const question of quickReplies) {
await expect(teacher.getByRole('button', { name: question, exact: true })).toHaveCount(0);
}
const raw = reply.locator('details').filter({ has: page.getByText('查看收到的原始内容', { exact: true }) });
await expect(raw).not.toHaveAttribute('open');
await expect(raw.locator('pre')).toBeHidden();
await raw.locator('summary').click();
await expect(raw.locator('pre')).toBeVisible();
expect(await raw.locator('pre').textContent()).toBe(unparsedResponse);
await expect(raw.locator('button, strong')).toHaveCount(0);
await expect(teacherComposer).toHaveValue('还没说完的困惑');
await expect(composer).toHaveValue('保留操作草稿');
const requests = (await readState(electronApp)).captured;
expect(requests.filter(item => item.path.endsWith('/messages') && item.method === 'POST')).toHaveLength(1);
expect(requests.filter(item => item.path.endsWith('/prompt') && item.method === 'POST')).toHaveLength(0);
} finally {
await releaseSnapshot(electronApp);
}
});
test('settled prompt releases the composer without a client receipt and keeps the next draft', async ({ launchElectronApp }) => {
const electronApp = await launchElectronApp({ skipSetup: true });
let page = await getStableWindow(electronApp);