fix: keep teacher runtime prompts within interface boundary

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

View File

@@ -9,6 +9,13 @@ import {
import { applyTeacherReply, teacherReplyInstructions } from '../../electron/coding-teacher/reply';
import type { TeacherRequest } from '../../shared/coding-teacher';
const fourHistoricalReplies = [
'看懂规则|摸头会改变什么?',
'想想作用|我们聊聊摸头的作用。',
'试试结果|怎样知道变化发生了?',
'换个话题|我想先聊别的地方。',
];
describe('teacher text and suggestions transport', () => {
const envelope = { reply: '🍎 **先看看重力带来的变化。**', quickReplies: ['继续解释', '比较不同选择'] };
@@ -66,6 +73,17 @@ describe('teacher text and suggestions transport', () => {
.toEqual({ reply: '一个入口。', quickReplies: ['继续'] });
});
it('keeps four historical shortcuts intact when decoding either envelope format', () => {
const reply = '这些是之前留下的讨论入口。';
for (const historical of [
{ reply, quickReplies: fourHistoricalReplies },
{ intro: reply, questions: fourHistoricalReplies },
]) {
expect(parseTeacherReply(JSON.stringify(historical)))
.toEqual({ reply, quickReplies: fourHistoricalReplies });
}
});
it('preserves long ordinary and structured replies without the old 12000 character truncation', () => {
const reply = '🍏 云端前缀\n\n' + '这是一段完整的详细解释。'.repeat(10000);
expect(parseTeacherReply(reply)).toEqual({ reply, quickReplies: [] });
@@ -195,10 +213,47 @@ describe('Main reply application', () => {
expect(current.discussionSnapshot).toEqual({ kind: 'ideas', title: '历史记录' });
});
it.each([
{ quickReplies: [] },
{ quickReplies: ['看懂规则|摸头会改变什么?'] },
{ quickReplies: ['看懂规则|摸头会改变什么?', '看懂规则|摸头会改变什么?', '换个话题|我想聊别的地方。'] },
])('applies zero to three new shortcuts without stripping prefixes or deduplicating: $quickReplies', ({ quickReplies }) => {
const current = request();
current.replyParseError = '之前的格式错误';
current.unparsedResponse = '之前的原始输出';
applyTeacherReply(current, JSON.stringify({ reply: '先看看摸头的作用。', quickReplies }));
expect(current.response).toBe('先看看摸头的作用。');
expect(current.suggestedQuestions).toEqual(quickReplies);
expect(current.replyParseError).toBeUndefined();
expect(current.unparsedResponse).toBeUndefined();
});
it.each(['current', 'legacy'] as const)('rejects four newly generated shortcuts in a %s envelope without losing the reply or raw output', format => {
const current = request();
current.suggestedQuestions = ['上一轮的旧选项'];
const reply = '保留这一轮的完整正文。';
const envelope = format === 'current'
? { reply, quickReplies: fourHistoricalReplies }
: { intro: reply, questions: fourHistoricalReplies };
const raw = ' \n```json\n' + JSON.stringify(envelope, null, 2) + '\n```\n ';
expect(parseTeacherReply(raw)).toEqual({ reply, quickReplies: fourHistoricalReplies });
applyTeacherReply(current, raw);
expect(current.response).toBe(reply);
expect(current.suggestedQuestions).toEqual([]);
expect(current.replyParseError).toBeTruthy();
expect(current.unparsedResponse).toBe(raw);
expect(current.discussionSnapshot).toEqual({ kind: 'ideas', title: '历史记录' });
});
it('limits instructions to the text/suggestions shape and defers writing choices to cloud configuration', () => {
const instructions = teacherReplyInstructions();
expect(instructions).toContain('{"reply":string,"quickReplies":string[]}');
expect(instructions).toContain('云端配置');
expect(instructions).not.toMatch(/Alice|120|400|2–3|学生口吻|kind|tool|想法板|流程图/);
expect(instructions).toMatch(/最多\s*3\s*条|0[–-]3\s*条/);
expect(instructions).toContain('空数组');
expect(instructions).not.toMatch(/Alice|120|400|2–3|学生口吻|kind|tool|想法板|流程图|整理想法|理解关系|承接|共识|已确认内容|待定想法/);
});
});