feat: ship v0.3.0 with P2.1 editor polish and P3 AI collaboration
Add chapter reorder, search replace, inspiration capture, and AI chat with context editing, settings, offline handling, and naming checks. Fix dev black screen by keeping process.env reads in the main process only. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
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 = `<p>${'章'.repeat(3000)}</p>`
|
||||
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, `<p>${'内容'.repeat(900)}</p>`)
|
||||
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('主角林远')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user