111 lines
3.6 KiB
TypeScript
111 lines
3.6 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, test } from 'vitest';
|
|
import { promises as fs } from 'fs';
|
|
import os from 'os';
|
|
import path from 'path';
|
|
import {
|
|
clearKnowledgeCache,
|
|
loadCoursewareKnowledge,
|
|
retrieveChunks,
|
|
tokenizeQuery,
|
|
} from '@/lib/qa/knowledge';
|
|
import { createFileBundleByteStore } from '@/lib/courseware-repo/bundle-store';
|
|
import { packageSampleBundle } from '../bundle/fixtures';
|
|
|
|
describe('tokenizeQuery', () => {
|
|
test('extracts CJK bigrams and ascii words', () => {
|
|
const terms = tokenizeQuery('叶绿体 光合作用 how do plants work');
|
|
expect(terms).toContain('叶绿');
|
|
expect(terms).toContain('绿体');
|
|
expect(terms).toContain('光合');
|
|
expect(terms).toContain('plants');
|
|
expect(terms).toContain('work');
|
|
});
|
|
|
|
test('returns nothing for noise', () => {
|
|
expect(tokenizeQuery('?!?!')).toEqual([]);
|
|
expect(tokenizeQuery('a b')).toEqual([]); // single chars filtered
|
|
});
|
|
});
|
|
|
|
describe('retrieveChunks', () => {
|
|
const knowledge = {
|
|
coursewareId: 'cw-1',
|
|
language: 'zh-CN',
|
|
scenes: [
|
|
{
|
|
sceneId: 's1',
|
|
order: 1,
|
|
title: '光合作用概述',
|
|
type: 'slide' as const,
|
|
text: '叶绿体是光合作用的场所。光反应与暗反应。',
|
|
narration: '今天我们来学习光合作用。',
|
|
},
|
|
{
|
|
sceneId: 's2',
|
|
order: 2,
|
|
title: '细胞呼吸',
|
|
type: 'slide' as const,
|
|
text: '线粒体是有氧呼吸的主要场所。',
|
|
},
|
|
{
|
|
sceneId: 's3',
|
|
order: 3,
|
|
title: '小测验',
|
|
type: 'quiz' as const,
|
|
text: '问:光合作用发生在细胞的哪个结构?',
|
|
},
|
|
],
|
|
};
|
|
|
|
test('scores title matches above body matches', () => {
|
|
const hits = retrieveChunks(knowledge, '光合作用 发生在哪里', 2);
|
|
expect(hits.length).toBeGreaterThan(0);
|
|
// Title match (s1: "光合作用概述" + s3 title "小测验"? s3 text has 光合作用) —
|
|
// s1 must rank first on combined score.
|
|
expect(hits[0].sceneId).toBe('s1');
|
|
});
|
|
|
|
test('respects the limit and drops non-matching scenes', () => {
|
|
const hits = retrieveChunks(knowledge, '线粒体 有氧呼吸', 1);
|
|
expect(hits).toHaveLength(1);
|
|
expect(hits[0].sceneId).toBe('s2');
|
|
});
|
|
|
|
test('empty query returns nothing', () => {
|
|
expect(retrieveChunks(knowledge, '啊 呢')).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('loadCoursewareKnowledge', () => {
|
|
let bytesDir: string;
|
|
beforeAll(async () => {
|
|
bytesDir = await fs.mkdtemp(path.join(os.tmpdir(), 'qa-knowledge-'));
|
|
const packaged = await packageSampleBundle({ coursewareId: 'kw-1' });
|
|
const store = createFileBundleByteStore(bytesDir);
|
|
await store.save(
|
|
'kw-1',
|
|
1,
|
|
new Uint8Array(await packaged.zip.arrayBuffer()),
|
|
);
|
|
});
|
|
afterAll(async () => {
|
|
await fs.rm(bytesDir, { recursive: true, force: true });
|
|
clearKnowledgeCache();
|
|
});
|
|
|
|
test('loads knowledge and quiz packs from the frozen bundle', async () => {
|
|
const store = createFileBundleByteStore(bytesDir);
|
|
const knowledge = await loadCoursewareKnowledge('kw-1', 1, store);
|
|
expect(knowledge).not.toBeNull();
|
|
expect(knowledge!.title).toBe('示例课件:光合作用');
|
|
expect(knowledge!.language).toBe('zh-CN');
|
|
expect(knowledge!.knowledge.scenes).toHaveLength(3);
|
|
expect(knowledge!.quiz.scenes).toHaveLength(1);
|
|
expect(knowledge!.knowledge.scenes[0].narration).toContain('光合作用');
|
|
});
|
|
|
|
test('returns null for an unknown version', async () => {
|
|
expect(await loadCoursewareKnowledge('kw-1', 99)).toBeNull();
|
|
});
|
|
});
|