8e5045b882
Harden cockpit dismiss timing and global settings navigation in E2E helpers, tune short-chapter AI prompts for local models, fix electron-updater CJS loading, and add a 10-chapter auto-writing integration test against LM Studio. Co-authored-by: Cursor <cursoragent@cursor.com>
131 lines
4.6 KiB
TypeScript
131 lines
4.6 KiB
TypeScript
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<AutoWritingConfig>(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,
|
|
'<p>林远本是山村少年,偶得仙缘,踏上修仙之路。今日,他将参加青云门的入门考核。</p>',
|
|
'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)
|
|
})
|