109 lines
7.2 KiB
TypeScript
109 lines
7.2 KiB
TypeScript
// @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,
|
||
});
|
||
});
|
||
});
|