import { describe, it, expect } from 'vitest' import { DatabaseSync } from 'node:sqlite' import { migrate } from '../../src/main/db/migrate' import { VolumeRepository } from '../../src/main/db/repositories/volume.repo' import { ChapterRepository } from '../../src/main/db/repositories/chapter.repo' import { AiSessionRepository } from '../../src/main/db/repositories/ai-session.repo' import { InteractiveRepository } from '../../src/main/db/repositories/interactive.repo' import { AutoWritingService } from '../../src/main/services/auto-writing.service' import { AiClientService } from '../../src/main/services/ai-client.service' import { DEFAULT_AI_CONFIG, type AutoWritingConfig, type InteractiveFlow } from '../../src/shared/types' import { stripHtml } from '../../src/main/services/word-count' const AI_TEST_CONFIG = { ...DEFAULT_AI_CONFIG, timeoutMs: 300_000, maxTokens: 2048 } const CHAPTER_COUNT = 10 const WORD_MIN = 180 const WORD_MAX = 220 const CHAPTER_GOALS = [ '林远初入修仙门派,通过入门考核', '林远在藏经阁发现神秘古卷', '林远与师兄切磋,初露锋芒', '林远下山历练,遭遇妖兽', '林远救下受伤少女,结下善缘', '林远进入秘境,获得灵草', '林远突破瓶颈,修为精进', '林远卷入门派纷争,被迫站队', '林远查明真相,化解误会', '林远回望来路,展望仙途' ] async function runAutoChapter( svc: AutoWritingService, repo: InteractiveRepository, db: DatabaseSync, sessionId: string, volumeId: string, chapterIndex: number, context: string ): Promise<{ chapterId: string; text: string; wordCount: number }> { const flow = svc.start(sessionId) svc.setGoal(flow.id, { chapterGoal: CHAPTER_GOALS[chapterIndex] ?? `第${chapterIndex + 1}章续写`, wordMin: WORD_MIN, wordMax: WORD_MAX }) await svc.planScenes(flow.id, context) let current: InteractiveFlow = repo.get(flow.id)! for (let guard = 0; guard < 12; guard++) { if (current.step === 'completed') break if (current.step === 'naming_pause' && current.namingPending) { current = await svc.resolveNaming( flow.id, current.namingPending.options[0]!, context, () => {} ) continue } const config = repo.getModeConfig(flow.id) const planLen = config.scenePlan?.length ?? 0 const idx = config.currentPlanIndex ?? 0 if (idx < planLen) { current = await svc.generateNext(flow.id, context, () => {}) continue } if (repo.listScenes(flow.id).length > 0) break throw new Error(`chapter ${chapterIndex + 1}: no scenes generated`) } const scenes = repo.listScenes(flow.id) expect(scenes.length).toBeGreaterThan(0) const { chapterId } = svc.finishChapter(flow.id, volumeId, `第${chapterIndex + 1}章`) const chapter = new ChapterRepository(db).get(chapterId)! const text = stripHtml(chapter.content) return { chapterId, text, wordCount: text.length } } describe('10-chapter novel via auto writing (LM Studio)', () => { it('IT-NOVEL-01: produces 10 chapters ~200 words each', async () => { const db = new DatabaseSync(':memory:') migrate(db) const volId = new VolumeRepository(db).create('第一卷', 0).id const chapters = new ChapterRepository(db) chapters.create( volId, '序章', 0, '

林远本是山村少年,偶得仙缘,踏上修仙之路。今日,他将参加青云门的入门考核。

', 'manual' ) const session = new AiSessionRepository(db).create('十章小说', DEFAULT_AI_CONFIG.model) const svc = new AutoWritingService(db, new AiClientService(() => AI_TEST_CONFIG)) const repo = new InteractiveRepository(db) let context = '林远本是山村少年,偶得仙缘,踏上修仙之路。今日,他将参加青云门的入门考核。' const results: { title: string; wordCount: number }[] = [] for (let i = 0; i < CHAPTER_COUNT; i++) { const { text, wordCount } = await runAutoChapter( svc, repo, db, session.id, volId, i, context ) results.push({ title: `第${i + 1}章`, wordCount }) context = `${context}\n\n${text.slice(-400)}` console.log(`[novel] 第${i + 1}章完成,${wordCount} 字`) } expect(results).toHaveLength(CHAPTER_COUNT) for (const r of results) { expect(r.wordCount).toBeGreaterThan(80) expect(r.wordCount).toBeLessThan(600) } const allChapters = chapters.listByVolume(volId) expect(allChapters.length).toBe(CHAPTER_COUNT + 1) db.close() }, 7_200_000) })