fix(test): stabilize E2E cockpit flow and add LM Studio novel integration test

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>
This commit is contained in:
2026-07-09 14:03:19 +08:00
parent fe421ffc55
commit 8e5045b882
24 changed files with 425 additions and 70 deletions
@@ -0,0 +1,130 @@
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)
})