83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
import { describe, expect, test, vi } from 'vitest';
|
|
import { buildQaSystemPrompt, runQaAgent, type QaStreamEvent } from '@/lib/qa/agent';
|
|
import type { CoursewareKnowledge, RetrievedChunk } from '@/lib/qa/knowledge';
|
|
|
|
vi.mock('@/lib/ai/llm', () => ({
|
|
streamLLM: (_params: unknown, _source: string) => ({
|
|
textStream: (async function* () {
|
|
yield '你';
|
|
yield '好';
|
|
})(),
|
|
}),
|
|
}));
|
|
|
|
const courseware: CoursewareKnowledge = {
|
|
coursewareId: 'cw-1',
|
|
version: 1,
|
|
language: 'zh-CN',
|
|
title: '示例课件:光合作用',
|
|
knowledge: { coursewareId: 'cw-1', scenes: [] },
|
|
quiz: { scenes: [] },
|
|
};
|
|
|
|
const chunks: RetrievedChunk[] = [
|
|
{
|
|
sceneId: 's1',
|
|
order: 1,
|
|
title: '光合作用概述',
|
|
type: 'slide',
|
|
text: '叶绿体是光合作用的场所。',
|
|
narration: '今天我们来学习光合作用。',
|
|
score: 4,
|
|
},
|
|
];
|
|
|
|
describe('buildQaSystemPrompt', () => {
|
|
test('embeds courseware title, language, chunks and profile', () => {
|
|
const prompt = buildQaSystemPrompt(courseware, chunks, '目标是理解光合作用;初二学生');
|
|
expect(prompt).toContain('示例课件:光合作用');
|
|
expect(prompt).toContain('请使用zh-CN回答');
|
|
expect(prompt).toContain('光合作用概述');
|
|
expect(prompt).toContain('叶绿体是光合作用的场所');
|
|
expect(prompt).toContain('学习者档案');
|
|
expect(prompt).toContain('初二学生');
|
|
});
|
|
|
|
test('omits profile section when absent', () => {
|
|
const prompt = buildQaSystemPrompt(courseware, [], undefined);
|
|
expect(prompt).not.toContain('学习者档案');
|
|
expect(prompt).toContain('(本次未检索到相关课件内容)');
|
|
});
|
|
});
|
|
|
|
describe('runQaAgent', () => {
|
|
test('streams text deltas and a done event (no tools)', async () => {
|
|
const events: QaStreamEvent[] = [];
|
|
for await (const event of runQaAgent(
|
|
{ model: {} as never, courseware, chunks },
|
|
[{ role: 'user', content: '光合作用发生在哪里?' }],
|
|
)) {
|
|
events.push(event);
|
|
}
|
|
|
|
const text = events.filter((e) => e.type === 'text');
|
|
expect(text.map((e) => (e as { delta: string }).delta).join('')).toBe('你好');
|
|
const done = events.find((e) => e.type === 'done') as { type: 'done'; sources: RetrievedChunk[]; toolCalls: number };
|
|
expect(done.sources).toEqual(chunks);
|
|
expect(done.toolCalls).toBe(0);
|
|
});
|
|
|
|
test('bounded context: keeps only the last QA_MAX_CONTEXT_TURNS turns', async () => {
|
|
const turns = Array.from({ length: 20 }, (_, i) => ({
|
|
role: 'user' as const,
|
|
content: `消息${i}`,
|
|
}));
|
|
const events: QaStreamEvent[] = [];
|
|
for await (const event of runQaAgent({ model: {} as never, courseware, chunks: [] }, turns)) {
|
|
events.push(event);
|
|
}
|
|
// Stream ran to completion without error; the mock ignores messages.
|
|
expect(events.some((e) => e.type === 'done')).toBe(true);
|
|
});
|
|
});
|