fix: keep teacher runtime prompts within interface boundary
This commit is contained in:
1 parent
998796d77c
commit
54a40d2636
12 files changed
+313
-32
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user