Files
makelore/tests/unit/teacher-guidance.test.ts

109 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @vitest-environment node
import { describe, expect, it } from 'vitest';
import examples from '../fixtures/teacher-guidance-examples.json';
import { compileTeacherContext, estimateTeacherTokens, teacherHistoryMessages } from '../../electron/coding-teacher/context';
import { teacherReplyInstructions } from '../../electron/coding-teacher/reply';
import { parseTeacherReply } from '../../shared/teacher-reply';
import type { TeacherDefinition, TeacherRequest, TeacherSourceContext } from '../../shared/coding-teacher';
const definition: TeacherDefinition = {
schema_version: 1, teacher_id: 'published-agent', name: '老师', description: '', avatar_id: 'avatar-01',
welcome_message: '一起想一想', suggested_questions: [], system_prompt: '运营发布的老师说明',
skills: [{ id: 'extra', name: '补充', description: '', instructions_markdown: '启用的教学补充', enabled: true },
{ id: 'off', name: '停用', description: '', instructions_markdown: '不应加载的资料', enabled: false }],
model: { model_id: 'configured-model', reasoning_choice: { mode: 'default' } },
limits: { max_input_tokens: 16000, max_output_tokens: 2000 },
};
const source: TeacherSourceContext = {
messages: [{ id: 'source', role: 'user', text: '我想做自己的作品' }],
cursor: { workerGeneration: 1, seq: 1 }, capturedAt: 'now',
};
describe('published guidance and text reply wiring', () => {
it.each(['question', 'suggestions', 'guided-help', 'check-in'] as const)(
'preserves cloud persona and enabled skills for %s without adding a fixed identity or writing style', intent => {
const before = structuredClone(definition);
const instructions = intent === 'check-in' ? undefined : teacherReplyInstructions();
const compiled = compileTeacherContext(definition, source, [], '帮我想一想', [], undefined, intent, instructions);
const system = compiled.messages[0].content;
expect(system).toContain(definition.system_prompt);
expect(system).toContain('启用的教学补充');
expect(system).not.toContain('不应加载的资料');
expect(compiled.messages.map(message => message.content).join('\n'))
.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 });
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('程序触发的项目进展检查,学生没有在本轮主动提问');
}
);
it('does not duplicate a persona that operations publishes', () => {
const persona = '运营发布的独特身份与前缀';
const configured = { ...definition, system_prompt: `\n${persona}\n` };
const compiled = compileTeacherContext(configured, source, [], '继续聊', []);
expect(compiled.messages[0].content.split(persona)).toHaveLength(2);
expect(configured.system_prompt).toBe(`\n${persona}\n`);
});
it('keeps the same configured skills and read-only project access for any published identity', () => {
const configured = { ...definition, teacher_id: 'published-friend', system_prompt: '运营发布的作品体验伙伴。' };
const instructions = teacherReplyInstructions();
const compiled = compileTeacherContext(configured, source, [], '你觉得呢', [], undefined, 'question', instructions, true);
expect(compiled.messages[0].content).toContain(configured.system_prompt);
expect(compiled.messages[0].content).toContain('启用的教学补充');
expect(compiled.messages[0].content).toContain('你可以通过只读工具');
expect(compiled.messages.at(-2)).toEqual({ role: 'system', content: instructions });
});
it('budgets readable history while retaining the latest answer and suggestions, without exposing archived tools or raw failures', () => {
const saved: TeacherRequest = {
id: 'check-in', intent: 'check-in', text: '内部主动触发', response: '刚才的作品有新进展。',
references: [], createdAt: 'now', sourceCursor: source.cursor, sourceCapturedAt: 'now',
includedSourceMessageIds: [], omittedMessages: 0, status: 'completed',
};
const history: TeacherRequest[] = [saved, {
...saved, id: 'latest', intent: 'suggestions', text: '一起聊什么?',
response: '先理解玩家想做什么。' + '需要考虑的作品细节。'.repeat(1200),
suggestedQuestions: ['怎样判断这个体验是否有趣?'],
presentation: 'discussion-v1', discussionSnapshot: { kind: 'ideas', title: '旧组件仅留档' },
unparsedResponse: '不应重新发送的原始损坏协议', replyParseError: '历史解析错误',
}];
const original = structuredClone(history);
const readable = teacherHistoryMessages(history);
expect(readable.map(message => message.id)).toEqual([
'teacher:check-in:assistant', 'teacher:latest:user', 'teacher:latest:assistant',
]);
expect(readable.at(-1)?.text).toContain('怎样判断这个体验是否有趣?');
expect(JSON.stringify(readable)).not.toMatch(/内部主动触发|旧组件仅留档|原始损坏协议/);
const instructions = teacherReplyInstructions();
const fixed = compileTeacherContext(definition, source, [], '接着刚才的问题聊', [], undefined, 'question', instructions, true);
const budget = estimateTeacherTokens(fixed.messages) + 1200;
const compiled = compileTeacherContext(definition, source, history, '接着刚才的问题聊', [], budget, 'question', instructions, true);
const latest = compiled.messages.find(message => message.content.startsWith('[teacher:latest:assistant]'));
expect(latest?.content).toContain('先理解玩家想做什么');
expect(latest?.content).toContain('中间内容已省略');
expect(latest?.content).toContain('怎样判断这个体验是否有趣?');
expect(compiled.omittedMessages).toBe(1);
expect(compiled.truncatedMessages).toBeGreaterThan(0);
expect(compiled.messages.at(-2)).toEqual({ role: 'system', content: instructions });
expect(estimateTeacherTokens(compiled.messages)).toBeLessThanOrEqual(budget);
expect(history).toEqual(original);
expect(teacherHistoryMessages(history)).toEqual(readable);
});
});
describe('historical reply compatibility examples', () => {
const responses = [examples.pet, examples.pet.followup, examples.pet.structure, examples.pet.paused,
examples.pet.finished, examples.garden, examples.garden.followup, examples.shooter, examples.cake];
it.each(responses)('retains text and suggestions while discarding historical component data: $response.reply', ({ response }) => {
expect(parseTeacherReply(JSON.stringify(response))).toEqual({
reply: response.reply, quickReplies: response.quickReplies,
});
});
});