import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { DatabaseSync } from 'node:sqlite' import { mkdtempSync, rmSync } from 'fs' import { join } from 'path' import { tmpdir } from 'os' import { migrate } from '../../src/main/db/migrate' import { ChapterRepository } from '../../src/main/db/repositories/chapter.repo' import { VolumeRepository } from '../../src/main/db/repositories/volume.repo' import { SettingRepository } from '../../src/main/db/repositories/setting.repo' import { BookRegistryService } from '../../src/main/services/book-registry' import { aiContextBuilder } from '../../src/main/services/ai-context-builder.service' import type { SqliteDb } from '../../src/main/db/types' describe('AiContextBuilderService', () => { let userDir: string let registry: BookRegistryService let bookId: string let volId: string let db: SqliteDb beforeEach(() => { userDir = mkdtempSync(join(tmpdir(), 'bilin-ctx-')) registry = new BookRegistryService(userDir) const meta = registry.create({ name: '测试书', category: '玄幻' }) bookId = meta.id db = registry.getDb(bookId) volId = new VolumeRepository(db).create('卷一', 0).id }) afterEach(() => { db?.close() rmSync(userDir, { recursive: true, force: true }) }) it('truncates long chapter content to 2000 chars per block', () => { const chapters = new ChapterRepository(db) const longHtml = `

${'章'.repeat(3000)}

` const chapter = chapters.create(volId, '长章节', 0, longHtml) const result = aiContextBuilder.build( { chapterIds: [chapter.id], outlineIds: [], settingIds: [], inspirationIds: [] }, registry, bookId, '作者' ) expect(result.preview).toHaveLength(1) expect(result.preview[0].excerpt.length).toBeLessThanOrEqual(2001) expect(result.systemPrompt.length).toBeLessThanOrEqual(16 * 1024 + 500) }) it('keeps total context within 16KB', () => { const chapters = new ChapterRepository(db) const ids: string[] = [] for (let i = 0; i < 12; i++) { const ch = chapters.create(volId, `章节${i}`, i, `

${'内容'.repeat(900)}

`) ids.push(ch.id) } const result = aiContextBuilder.build( { chapterIds: ids, outlineIds: [], settingIds: [], inspirationIds: [] }, registry, bookId, '作者' ) const blocksText = result.preview.map((p) => p.excerpt).join('') expect(blocksText.length).toBeLessThanOrEqual(16 * 1024) expect(result.preview.length).toBeLessThan(ids.length) }) it('includes setting in preview', () => { const settings = new SettingRepository(db) const setting = settings.create('character', '主角林远', '少年修士') const result = aiContextBuilder.build( { chapterIds: [], outlineIds: [], settingIds: [setting.id], inspirationIds: [] }, registry, bookId, '作者' ) expect(result.preview[0].title).toBe('主角林远') expect(result.systemPrompt).toContain('主角林远') }) })