Merge cloud-owned teaching policy and bounded new shortcuts
This commit is contained in:
@@ -21,6 +21,13 @@ type HostConnection = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
type ConsultationReply = {
|
||||
response: string;
|
||||
suggestedQuestions: string[];
|
||||
replyParseError?: string;
|
||||
unparsedResponse?: string;
|
||||
};
|
||||
|
||||
type TeacherCatalogFixture = {
|
||||
nextTeacherReplyIncomplete?: boolean;
|
||||
teacherCatalog: TeacherCatalog;
|
||||
@@ -97,9 +104,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 = TeacherCatalogFixture & {
|
||||
captured: CapturedRequest[];
|
||||
@@ -575,6 +583,13 @@ async function installCodingFirstChatHost(
|
||||
const suggestions = body!.intent === 'suggestions';
|
||||
const response = suggestions ? '我们可以从你最近试过的地方聊起。' : body!.intent === 'guided-help' ? '你最近做的哪一步,让你停下来想了一会儿?' : '先理解状态如何随点击变化,再修改代码。';
|
||||
consultationTopics[agentId] = {...current,revision:Number(current.revision)+1,requests:[...(current.requests as unknown[]),{id:body!.requestId,projectId:body!.projectId,sourceConversationId:body!.sourceConversationId,text:body!.text,intent:body!.intent,references:body!.references??[],createdAt:new Date().toISOString(),sourceCursor:{workerGeneration:1,seq:1},sourceCapturedAt:now,includedSourceMessageIds:[],omittedMessages:0,truncatedMessages:1,status:'completed',response,...(suggestions?{suggestedQuestions:['怎样观察别人玩游戏?','我该先试哪个想法?']}:{})}]};
|
||||
const last = (consultationTopics[agentId].requests as Record<string, unknown>[]).at(-1)!;
|
||||
Object.assign(last, consultationReply);
|
||||
if (state.nextTeacherReplyIncomplete) {
|
||||
state.nextTeacherReplyIncomplete = false;
|
||||
Object.assign(last, { response: '它早就不是', replyIncomplete: true, replyParseError: '格式不完整',
|
||||
unparsedResponse: '{"reply":"它早就不是"刚搭好架子' });
|
||||
}
|
||||
return respond(consultationTopics[agentId],202);
|
||||
}
|
||||
}
|
||||
@@ -843,7 +858,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) => {
|
||||
@@ -1965,6 +1980,57 @@ test('incomplete consultation replies retry only on click and preserve both draf
|
||||
} finally { await releaseSnapshot(app); }
|
||||
});
|
||||
|
||||
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('回答正文已保留,部分附加内容未能显示。');
|
||||
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('manual agent refresh updates the catalog and retries failures without replacing the ongoing chat or drafts', async ({ launchElectronApp }) => {
|
||||
const electronApp = await launchElectronApp({ skipSetup: true });
|
||||
let page = await getStableWindow(electronApp);
|
||||
|
||||
@@ -418,8 +418,8 @@ it('submits a nonempty proactive check-in without inventing a user message', asy
|
||||
f.access.source.messages = [];
|
||||
const { body, compiled } = await submitCompiledContext(f, 'check-in');
|
||||
expect(compiled.messages.map(message => message.role)).toEqual(['system', 'system']);
|
||||
expect(compiled.messages.at(-1)?.content).toContain('本轮是一次项目进展提醒,不是用户提问');
|
||||
expect(body.query).toContain('仅依据已有证据');
|
||||
expect(compiled.messages.at(-1)?.content).toContain('程序触发的项目进展检查,学生没有在本轮主动提问');
|
||||
expect(body.query).toContain('回应方式遵循当前智能体的云端配置');
|
||||
});
|
||||
|
||||
it('gives a server-defined friend the same scoped read tools', async () => {
|
||||
|
||||
@@ -277,6 +277,29 @@ describe('teacher side chat', () => {
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows the format error alongside the preserved reply while keeping excess shortcuts noninteractive', async () => {
|
||||
const response = '先看看摸头会带来什么变化。';
|
||||
const quickReplies = ['看懂规则|摸头会改变什么?', '想想作用|我们聊聊摸头。', '试试结果|怎样看出变化?', '换个话题|我想聊别的地方。'];
|
||||
const replyParseError = '快捷回复超过当前界面最多 3 条的限制,正文已保留,快捷回复未展示。';
|
||||
const raw = '\n' + JSON.stringify({ reply: response, quickReplies }, null, 2) + '\n';
|
||||
api.read.mockResolvedValue({ ...first, requests: [suggestionsRequest({
|
||||
presentation: 'reply-v1', response, suggestedQuestions: [], replyParseError, unparsedResponse: raw,
|
||||
})] });
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await ready();
|
||||
|
||||
expect(screen.getByText(response)).toBeVisible();
|
||||
expect(screen.getByText('回答正文已保留,部分附加内容未能显示。')).toBeVisible();
|
||||
for (const shortcut of quickReplies) {
|
||||
expect(screen.queryByRole('button', { name: shortcut, exact: true })).not.toBeInTheDocument();
|
||||
}
|
||||
const details = screen.getByText('查看收到的原始内容').closest('details');
|
||||
expect(details).not.toHaveAttribute('open');
|
||||
expect(details?.querySelector('pre')?.textContent).toBe(raw);
|
||||
expect(screen.getByLabelText('向智能体提问')).toBeEnabled();
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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',
|
||||
@@ -771,6 +794,22 @@ describe('teacher side chat', () => {
|
||||
|
||||
|
||||
describe('legacy discussion history compatibility', () => {
|
||||
it('shows every historical shortcut even when the old reply has more than three', async () => {
|
||||
const shortcuts = ['看懂规则|摸头会改变什么?', '想想作用|我们聊聊摸头。', '试试结果|怎样看出变化?', '换个话题|我想聊别的地方。'];
|
||||
api.read.mockResolvedValue({ ...first, requests: [request({
|
||||
presentation: 'discussion-v1', response: '这些是之前留下的讨论入口。', suggestedQuestions: shortcuts,
|
||||
})] });
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await ready();
|
||||
|
||||
expect(screen.getByText('这些是之前留下的讨论入口。')).toBeVisible();
|
||||
for (const shortcut of shortcuts) {
|
||||
expect(screen.getByRole('button', { name: shortcut, exact: true })).toBeEnabled();
|
||||
}
|
||||
expect(screen.queryByText('查看收到的原始内容')).not.toBeInTheDocument();
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const contents = [
|
||||
{ kind: 'ideas', title: '旧想法组件', items: [{ id: 'dog', text: '养只小狗', state: 'kept' }] },
|
||||
{ kind: 'structure', title: '旧结构组件', nodes: [{ id: 'dog', label: '照顾小狗' }] },
|
||||
|
||||
@@ -625,6 +625,28 @@ describe('teacher contextual discussion entry points', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([false, true])('persists prose and exact raw diagnostics instead of excess new shortcuts (cloud=%s)', async (cloudTeacher) => {
|
||||
const f = await fixture(cloudTeacher ? { cloudTeacher: true, mockCloud: true } : {});
|
||||
const scope = newScope(f);
|
||||
const topic = await f.service.create(scope);
|
||||
const reply = '我们先看看规则会怎样影响结局。';
|
||||
const quickReplies = ['看懂规则|现在是什么规则?', '想想影响|摸头会改变什么?', '试试结果|怎样验证?', '换个话题|先聊别的。'];
|
||||
const raw = JSON.stringify({ reply, quickReplies }, null, 2);
|
||||
f.replyWith(raw);
|
||||
await f.service.send(scope, topic.id, { requestId, intent: 'suggestions', text: '帮我看看' });
|
||||
const completed = await finishRequest(f, scope, topic.id);
|
||||
expect(completed.requests[0]).toMatchObject({
|
||||
status: 'completed', response: reply, suggestedQuestions: [],
|
||||
replyParseError: expect.stringContaining('最多 3 条'), unparsedResponse: raw,
|
||||
});
|
||||
const disk = JSON.parse(await readFile(path.join(
|
||||
f.created.project.path, '.makelore/teacher-conversations', topic.accountId, 'project', topic.id + '.json'
|
||||
), 'utf8'));
|
||||
expect(disk.requests[0]).toMatchObject({
|
||||
response: reply, suggestedQuestions: [], replyParseError: completed.requests[0].replyParseError, unparsedResponse: raw,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not treat a cancelled response as valid suggestions even when its JSON is complete', async () => {
|
||||
const f = await fixture();
|
||||
const scope = newScope(f);
|
||||
@@ -695,7 +717,7 @@ describe('teacher contextual discussion entry points', () => {
|
||||
expect(saved.requests[0].suggestedQuestions).toBeUndefined();
|
||||
});
|
||||
|
||||
it('adds a one-question guided opening for this round without changing the teacher system instructions', async () => {
|
||||
it('marks the guided-help entry without choosing a teaching strategy or changing the system instructions', async () => {
|
||||
const f = await fixture();
|
||||
const scope = newScope(f);
|
||||
const topic = await f.service.create(scope);
|
||||
@@ -707,7 +729,7 @@ describe('teacher contextual discussion entry points', () => {
|
||||
});
|
||||
expect(completed.requests[0].suggestedQuestions).toEqual([]);
|
||||
const messages = f.run.mock.calls[0][0];
|
||||
expect(messages.at(-1).content).toContain('推荐一个具体切入点');
|
||||
expect(messages.at(-1).content).toContain('学生主动求助,表示暂时说不清想问什么');
|
||||
expect(messages[0]).toEqual(compileTeacherContext(definition, context, [], '问题', [], undefined, 'question', undefined, true).messages[0]);
|
||||
});
|
||||
|
||||
@@ -752,26 +774,27 @@ describe('teacher contextual discussion entry points', () => {
|
||||
expect(f.run).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('budgets the round-specific instructions and honestly starts from ideas with no source', () => {
|
||||
it('budgets the entry facts even when no source conversation is available', () => {
|
||||
const empty = { ...context, messages: [] };
|
||||
const ordinary = compileTeacherContext(definition, empty, [], '老师,帮我看看', []);
|
||||
const suggestions = compileTeacherContext(definition, empty, [], '老师,帮我看看', [], 8000, 'suggestions');
|
||||
expect(suggestions.messages[0]).toEqual(ordinary.messages[0]);
|
||||
expect(suggestions.messages.at(-1)?.content).toContain('没有可用上下文时');
|
||||
expect(suggestions.messages.at(-1)?.content).toContain('不编造');
|
||||
expect(suggestions.messages.at(-1)?.content).toContain('学生通过“帮我看看”主动请求帮助');
|
||||
expect(suggestions.messages.some(message => message.content.startsWith('来源编程会话'))).toBe(false);
|
||||
expect(() => compileTeacherContext(definition, empty, [], '老师,帮我看看', [], estimateTeacherTokens(ordinary.messages), 'suggestions'))
|
||||
.toThrow('超过上下文预算');
|
||||
});
|
||||
|
||||
it.each(['suggestions', 'guided-help'] as const)('recommends one concrete starting point for %s without local expression policy', (intent) => {
|
||||
it.each(['suggestions', 'guided-help'] as const)('passes %s entry facts without local teaching or expression policy', (intent) => {
|
||||
const selected = { ...definition, system_prompt: '云端配置:正文长度随内容,选项保留【我想聊】前缀。' };
|
||||
const compiled = compileTeacherContext(selected, context, [], '帮我看看', [], 8000, intent, teacherReplyInstructions());
|
||||
const current = compiled.messages.at(-1)!.content;
|
||||
expect(current).toContain('推荐一个具体切入点');
|
||||
expect(current).toContain('快捷回复围绕这个切入点');
|
||||
expect(current).toContain(intent === 'suggestions'
|
||||
? '学生通过“帮我看看”主动请求帮助' : '学生主动求助,表示暂时说不清想问什么');
|
||||
expect(compiled.messages[0].content).toContain(selected.system_prompt);
|
||||
const local = compiled.messages.slice(1).map(message => message.content).join('\n');
|
||||
for (const removed of ['400 字', '120 字', '2–3', '简短中文', '学生口吻', 'Alice', '组件', '先这些'])
|
||||
for (const removed of ['400 字', '120 字', '2–3', '简短中文', '学生口吻', 'Alice', '组件', '先这些',
|
||||
'推荐一个具体切入点', '说明为何', '快捷回复围绕', '从构思切入', '整理想法', '理解关系', '承接已确认的共识'])
|
||||
expect(local).not.toContain(removed);
|
||||
});
|
||||
|
||||
@@ -819,9 +842,8 @@ describe('project teacher check-ins', () => {
|
||||
expect(JSON.stringify(messages)).toContain('创建计数器');
|
||||
expect(JSON.stringify(messages)).toContain('变量是什么意思');
|
||||
expect(messages.at(-1)).toMatchObject({ role: 'system' });
|
||||
expect(messages.at(-1).content).toContain('表达方式遵循已配置的要求');
|
||||
expect(messages.at(-1).content).toContain('按照已配置的人设和职责');
|
||||
expect(messages.at(-1).content).toContain('不声称实际运行');
|
||||
expect(messages.at(-1).content).toContain('程序触发的项目进展检查,学生没有在本轮主动提问');
|
||||
expect(messages.at(-1).content).toContain('回应方式遵循当前智能体的云端配置');
|
||||
expect(f.prepareModel).toHaveBeenLastCalledWith(expect.anything(), selected.definition, expect.objectContaining({
|
||||
projectPath: f.created.project.path, source: context, assertCurrent: expect.any(Function),
|
||||
}), { finalOnly: false });
|
||||
@@ -1435,7 +1457,7 @@ describe('project consultations with selected cloud teachers', () => {
|
||||
expect(result.topic?.id).toBe(topic.id);
|
||||
expect(result.topic?.requests[0]).toMatchObject({ intent: 'check-in', sourceConversationId: f.scope.sourceId });
|
||||
const messages = f.run.mock.calls[0][0] as Array<{ role: string; content: string }>;
|
||||
expect(messages.some(message => message.role === 'system' && message.content.includes('不是用户提问'))).toBe(true);
|
||||
expect(messages.some(message => message.role === 'system' && message.content.includes('学生没有在本轮主动提问'))).toBe(true);
|
||||
expect(messages[0].content).not.toContain(TEACHER_BEHAVIOR_PROMPT);
|
||||
expect(f.prepareCloud.mock.calls[0][3]).toMatchObject({ projectPath: f.created.project.path, source: context });
|
||||
expect(f.prepareModel).not.toHaveBeenCalled();
|
||||
|
||||
@@ -30,13 +30,14 @@ describe('published guidance and text reply wiring', () => {
|
||||
expect(system).toContain('启用的教学补充');
|
||||
expect(system).not.toContain('不应加载的资料');
|
||||
expect(compiled.messages.map(message => message.content).join('\n'))
|
||||
.not.toMatch(/你是麦洛的创作老师|Alice|最多120字|不超过400字|2–3|学生自己的口吻/);
|
||||
.not.toMatch(/你是麦洛的创作老师|Alice|最多120字|不超过400字|2–3|学生自己的口吻|推荐一个具体切入点|说明为何|快捷回复围绕|整理想法|理解关系|承接已确认的共识/);
|
||||
expect(definition).toEqual(before);
|
||||
expect(compiled.messages.at(-1)?.role).toBe(intent === 'check-in' ? 'system' : 'user');
|
||||
if (instructions) expect(compiled.messages.at(-2)).toEqual({ role: 'system', content: instructions });
|
||||
if (intent === 'suggestions' || intent === 'guided-help') {
|
||||
expect(compiled.messages.at(-1)?.content).toContain('推荐一个具体切入点');
|
||||
}
|
||||
const entry = compiled.messages.at(-1)?.content;
|
||||
if (intent === 'suggestions') expect(entry).toContain('学生通过“帮我看看”主动请求帮助');
|
||||
if (intent === 'guided-help') expect(entry).toContain('学生主动求助,表示暂时说不清想问什么');
|
||||
if (intent === 'check-in') expect(entry).toContain('程序触发的项目进展检查,学生没有在本轮主动提问');
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -113,6 +113,40 @@ async function fixture(value: TeacherTopic) {
|
||||
}
|
||||
|
||||
describe('passive teacher reply history compatibility', () => {
|
||||
it('retains all four historical shortcuts through read, list and a later save', async () => {
|
||||
const original = topic(archivedCases[4]);
|
||||
const historicalReplies = [
|
||||
'看懂规则|摸头会改变什么?',
|
||||
'想想作用|我们聊聊摸头的作用。',
|
||||
'试试结果|怎样知道变化发生了?',
|
||||
'换个话题|我想先聊别的地方。',
|
||||
];
|
||||
original.requests[0].suggestedQuestions = historicalReplies;
|
||||
const f = await fixture(original);
|
||||
|
||||
const loaded = await f.store.read(original.id);
|
||||
expect(loaded).toEqual(original);
|
||||
expect(loaded.requests[0].suggestedQuestions).toEqual(historicalReplies);
|
||||
expect((await f.store.list()).items).toEqual([{
|
||||
id: original.id, title: original.requests[0].text, updatedAt: original.updatedAt,
|
||||
version: 31, teacherId: 'selected-agent',
|
||||
}]);
|
||||
expect(await new TeacherTopicStore(f.root).read(original.id)).toEqual(original);
|
||||
await f.unchanged();
|
||||
|
||||
loaded.revision++;
|
||||
loaded.requests.push(request({ id: 'new-turn', presentation: 'reply-v1', suggestedQuestions: [],
|
||||
discussionError: undefined, unparsedResponse: undefined }));
|
||||
await f.store.save(loaded);
|
||||
const expected = JSON.parse(JSON.stringify({ ...original,
|
||||
revision: original.revision + 1, requests: [...original.requests, loaded.requests[1]],
|
||||
}));
|
||||
const saved = JSON.parse(await readFile(f.file, 'utf8'));
|
||||
expect(saved).toEqual(expected);
|
||||
expect(saved.requests[0].suggestedQuestions).toEqual(historicalReplies);
|
||||
expect(await new TeacherTopicStore(f.root).read(original.id)).toEqual(expected);
|
||||
});
|
||||
|
||||
it.each(archivedCases)('keeps $name opaque through read, list and a later save', async archive => {
|
||||
const original = topic(archive);
|
||||
const f = await fixture(original);
|
||||
|
||||
@@ -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: [] });
|
||||
@@ -245,10 +263,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|想法板|流程图|整理想法|理解关系|承接|共识|已确认内容|待定想法/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user