test: complete P4.1 LM Studio coverage and fix auto/wizard handoff

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 21:45:18 +08:00
parent 6a949b54ba
commit 5d3e264cf3
11 changed files with 415 additions and 6 deletions
+70
View File
@@ -0,0 +1,70 @@
import { describe, it, expect } from 'vitest'
import { DatabaseSync } from 'node:sqlite'
import { migrate } from '../../src/main/db/migrate'
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 } from '../../src/shared/types'
const AI_TEST_CONFIG = { ...DEFAULT_AI_CONFIG, timeoutMs: 240_000, maxTokens: 4096 }
const CONTEXT = '林远站在演武场中央,众人目光汇聚,等待入门考核开始。'
describe('AutoWritingService generate', () => {
const service = (db: DatabaseSync) =>
new AutoWritingService(db, new AiClientService(() => AI_TEST_CONFIG))
async function planFirstScene(db: DatabaseSync, svc: AutoWritingService, flowId: string): Promise<void> {
await svc.planScenes(flowId, CONTEXT)
}
it('IT-AUTO-02: generateNext writes non-empty scene', async () => {
const db = new DatabaseSync(':memory:')
migrate(db)
const session = new AiSessionRepository(db).create('t', DEFAULT_AI_CONFIG.model)
const svc = service(db)
const flow = svc.start(session.id)
svc.setGoal(flow.id, {
chapterGoal: '林远通过入门考核,展现天赋震惊众人',
wordMin: 800,
wordMax: 1500
})
await planFirstScene(db, svc, flow.id)
let afterGen = await svc.generateNext(flow.id, CONTEXT, () => {})
if (afterGen.step === 'naming_pause' && afterGen.namingPending) {
afterGen = await svc.resolveNaming(
flow.id,
afterGen.namingPending.options[0]!,
CONTEXT,
() => {}
)
}
const scenes = new InteractiveRepository(db).listScenes(flow.id)
expect(scenes.length).toBeGreaterThanOrEqual(1)
expect(scenes[0]!.contentHtml.replace(/<[^>]+>/g, '').trim().length).toBeGreaterThan(20)
db.close()
}, 240_000)
it('IT-AUTO-03: pause and resume update step', async () => {
const db = new DatabaseSync(':memory:')
migrate(db)
const session = new AiSessionRepository(db).create('t', DEFAULT_AI_CONFIG.model)
const svc = service(db)
const flow = svc.start(session.id)
svc.setGoal(flow.id, {
chapterGoal: '林远通过入门考核',
wordMin: 800,
wordMax: 1500
})
await planFirstScene(db, svc, flow.id)
const paused = svc.pause(flow.id)
expect(paused.step).toBe('paused')
const resumed = svc.resume(flow.id)
expect(resumed.step).toBe('auto_generate')
db.close()
}, 240_000)
})
+31
View File
@@ -0,0 +1,31 @@
import { describe, it, expect } from 'vitest'
import { DatabaseSync } from 'node:sqlite'
import { migrate } from '../../src/main/db/migrate'
import { AiSessionRepository } from '../../src/main/db/repositories/ai-session.repo'
import { AutoWritingService } from '../../src/main/services/auto-writing.service'
import { AiClientService } from '../../src/main/services/ai-client.service'
import { DEFAULT_AI_CONFIG } from '../../src/shared/types'
const AI_TEST_CONFIG = { ...DEFAULT_AI_CONFIG, timeoutMs: 240_000, maxTokens: 4096 }
const CONTEXT = '林远站在演武场中央,众人目光汇聚,等待入门考核开始。'
describe('AutoWritingService.planScenes', () => {
const service = (db: DatabaseSync) =>
new AutoWritingService(db, new AiClientService(() => AI_TEST_CONFIG))
it('IT-AUTO-01: planScenes returns >=2 scenes', async () => {
const db = new DatabaseSync(':memory:')
migrate(db)
const session = new AiSessionRepository(db).create('t', DEFAULT_AI_CONFIG.model)
const svc = service(db)
const flow = svc.start(session.id)
svc.setGoal(flow.id, {
chapterGoal: '林远通过入门考核,展现天赋震惊众人',
wordMin: 800,
wordMax: 1500
})
const plan = await svc.planScenes(flow.id, CONTEXT)
expect(plan.scenes.length).toBeGreaterThanOrEqual(2)
db.close()
}, 360_000)
})
+89
View File
@@ -0,0 +1,89 @@
import { describe, it, expect } from 'vitest'
import { DatabaseSync } from 'node:sqlite'
import { migrate } from '../../src/main/db/migrate'
import { AiSessionRepository } from '../../src/main/db/repositories/ai-session.repo'
import { InteractiveRepository } from '../../src/main/db/repositories/interactive.repo'
import { WizardWritingService } from '../../src/main/services/wizard-writing.service'
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'
const AI_TEST_CONFIG = { ...DEFAULT_AI_CONFIG, timeoutMs: 300_000, maxTokens: 4096 }
const CONTEXT = '林远站在演武场中央,众人目光汇聚,等待入门考核开始。'
describe('WizardWritingService.startGenerate', () => {
const wizard = (db: DatabaseSync) =>
new WizardWritingService(db, new AiClientService(() => AI_TEST_CONFIG))
const auto = (db: DatabaseSync) =>
new AutoWritingService(db, new AiClientService(() => AI_TEST_CONFIG))
async function setupWizardFlow(svc: WizardWritingService, sessionId: string): Promise<string> {
let flow = svc.start(sessionId)
flow = svc.setGoal(flow.id, {
chapterGoal: '林远通过入门考核,展现天赋',
wordMin: 600,
wordMax: 1200
})
flow = svc.setRhythm(flow.id, 'tense')
flow = svc.setPov(flow.id)
flow = svc.confirmContext(
flow.id,
{ chapterIds: [], outlineIds: [], settingIds: [], inspirationIds: [] },
CONTEXT
)
return flow.id
}
async function continueAfterPartialGenerate(
wizSvc: WizardWritingService,
autoSvc: AutoWritingService,
repo: InteractiveRepository,
flowId: string,
initial: InteractiveFlow
): Promise<InteractiveFlow> {
let flow = initial
for (let guard = 0; guard < 8 && flow.flowMode !== 'interactive'; guard++) {
if (flow.step === 'naming_pause' && flow.namingPending) {
flow = await autoSvc.resolveNaming(
flowId,
flow.namingPending.options[0]!,
CONTEXT,
() => {}
)
continue
}
const config = repo.getModeConfig<AutoWritingConfig>(flowId)
const planLen = config.scenePlan?.length ?? 0
const idx = config.currentPlanIndex ?? 0
if (idx < planLen) {
flow = await autoSvc.generateNext(flowId, CONTEXT, () => {})
continue
}
if (flow.scenes.length > 0) {
return wizSvc.handoffToInteractive(flowId)
}
break
}
return flow
}
it('IT-WIZ-02: startGenerate handoffs to interactive with scenes', async () => {
const db = new DatabaseSync(':memory:')
migrate(db)
const repo = new InteractiveRepository(db)
const session = new AiSessionRepository(db).create('t', DEFAULT_AI_CONFIG.model)
const wizSvc = wizard(db)
const autoSvc = auto(db)
const flowId = await setupWizardFlow(wizSvc, session.id)
const initial = await wizSvc.startGenerate(flowId, CONTEXT, () => {})
const flow =
initial.flowMode === 'interactive'
? initial
: await continueAfterPartialGenerate(wizSvc, autoSvc, repo, flowId, initial)
expect(flow.flowMode).toBe('interactive')
expect(flow.scenes.length).toBeGreaterThanOrEqual(1)
db.close()
}, 600_000)
})
+40
View File
@@ -0,0 +1,40 @@
import { describe, it, expect } from 'vitest'
import { DatabaseSync } from 'node:sqlite'
import { migrate } from '../../src/main/db/migrate'
import { AiSessionRepository } from '../../src/main/db/repositories/ai-session.repo'
import { WizardWritingService } from '../../src/main/services/wizard-writing.service'
import { AiClientService } from '../../src/main/services/ai-client.service'
import { DEFAULT_AI_CONFIG } from '../../src/shared/types'
const AI_TEST_CONFIG = { ...DEFAULT_AI_CONFIG, timeoutMs: 240_000, maxTokens: 4096 }
const CONTEXT = '林远站在演武场中央,众人目光汇聚,等待入门考核开始。'
describe('WizardWritingService.buildPreview', () => {
const service = (db: DatabaseSync) =>
new WizardWritingService(db, new AiClientService(() => AI_TEST_CONFIG))
it('IT-WIZ-01: buildPreview returns non-empty HTML', async () => {
const db = new DatabaseSync(':memory:')
migrate(db)
const session = new AiSessionRepository(db).create('t', DEFAULT_AI_CONFIG.model)
const svc = service(db)
let flow = svc.start(session.id)
flow = svc.setGoal(flow.id, {
chapterGoal: '林远通过入门考核,展现天赋震惊众人',
wordMin: 800,
wordMax: 1500
})
flow = svc.setRhythm(flow.id, 'mixed')
flow = svc.setPov(flow.id)
flow = svc.confirmContext(
flow.id,
{ chapterIds: [], outlineIds: [], settingIds: [], inspirationIds: [] },
CONTEXT
)
const preview = await svc.buildPreview(flow.id, CONTEXT, '林远')
expect(preview).toMatch(/<p>/)
expect(preview.replace(/<[^>]+>/g, '').trim().length).toBeGreaterThan(10)
db.close()
}, 240_000)
})