// @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, }); }); });