feat: ship v1.2.0 Wave 1 foundation with book settings, chapter management, and focus mode
Deliver schema v9, book/chapter IPC extensions, BookSettingsTab, chapter meta/tags, AI session menu, focus mode and TTS, template context enrich, and Wave 1 E2E coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { mkdtempSync, rmSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
import { openBookDb, closeAllBookDbs } from '../../src/main/db/connection'
|
||||
import { migrate } from '../../src/main/db/migrate'
|
||||
import { ChapterRepository } from '../../src/main/db/repositories/chapter.repo'
|
||||
import { ChapterTagRepository } from '../../src/main/db/repositories/chapter-tag.repo'
|
||||
import { VolumeRepository } from '../../src/main/db/repositories/volume.repo'
|
||||
|
||||
describe('ChapterTagRepository', () => {
|
||||
let dir: string
|
||||
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(join(tmpdir(), 'bilin-tag-'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
closeAllBookDbs()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('IT-TAG-01: set and list chapter tags', () => {
|
||||
const db = openBookDb(join(dir, 'test.sqlite'))
|
||||
migrate(db)
|
||||
const volId = new VolumeRepository(db).create('第一卷', 0).id
|
||||
const chId = new ChapterRepository(db).create(volId, '第一章', 0).id
|
||||
const tags = new ChapterTagRepository(db)
|
||||
tags.set(chId, '高潮', '#ff0000')
|
||||
tags.set(chId, '女主主场')
|
||||
expect(tags.list(chId)).toHaveLength(2)
|
||||
tags.remove(chId, '高潮')
|
||||
expect(tags.list(chId)).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { buildTemplateContext } from '../../src/shared/chapter-template'
|
||||
import type { Chapter, OutlineItem, SettingEntry, Volume } from '../../src/shared/types'
|
||||
|
||||
const volumeId = 'vol-1'
|
||||
const chapterId = 'ch-1'
|
||||
|
||||
const volumes: Volume[] = [
|
||||
{ id: volumeId, name: '第一卷', description: '', sortOrder: 0, createdAt: '', updatedAt: '' }
|
||||
]
|
||||
|
||||
const chapters: Chapter[] = [
|
||||
{
|
||||
id: 'ch-prev',
|
||||
volumeId,
|
||||
title: '序章',
|
||||
content: '',
|
||||
status: 'draft',
|
||||
publishStatus: 'draft',
|
||||
sortOrder: 0,
|
||||
wordCount: 0,
|
||||
cursorOffset: 0,
|
||||
createdAt: '',
|
||||
updatedAt: ''
|
||||
}
|
||||
]
|
||||
|
||||
describe('buildTemplateContext', () => {
|
||||
it('UT-TEMPL-05: characterList from settings', () => {
|
||||
const settings: SettingEntry[] = [
|
||||
{
|
||||
id: 's1',
|
||||
type: 'character',
|
||||
name: '林远',
|
||||
description: '',
|
||||
properties: {},
|
||||
createdAt: '',
|
||||
updatedAt: ''
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
type: 'character',
|
||||
name: '苏晴',
|
||||
description: '',
|
||||
properties: {},
|
||||
createdAt: '',
|
||||
updatedAt: ''
|
||||
}
|
||||
]
|
||||
const ctx = buildTemplateContext({
|
||||
chapterTitle: '新章',
|
||||
penName: '笔名',
|
||||
volumes,
|
||||
chapters,
|
||||
outlines: [],
|
||||
settings,
|
||||
activeVolumeId: volumeId
|
||||
})
|
||||
expect(ctx.characterList).toContain('林远')
|
||||
expect(ctx.characterList).toContain('苏晴')
|
||||
expect(ctx.chapterNumber).toBe(2)
|
||||
expect(ctx.previousChapterTitle).toBe('序章')
|
||||
})
|
||||
|
||||
it('UT-TEMPL-06: outlineItem from linked outline', () => {
|
||||
const outlines: OutlineItem[] = [
|
||||
{
|
||||
id: 'o1',
|
||||
parentId: null,
|
||||
title: '主角觉醒',
|
||||
description: '',
|
||||
status: 'planned',
|
||||
expectedWordCount: null,
|
||||
chapterId,
|
||||
sortOrder: 0,
|
||||
createdAt: '',
|
||||
updatedAt: ''
|
||||
}
|
||||
]
|
||||
const ctx = buildTemplateContext({
|
||||
chapterTitle: '第一章',
|
||||
penName: '笔名',
|
||||
volumes,
|
||||
chapters,
|
||||
outlines,
|
||||
settings: [],
|
||||
activeVolumeId: volumeId,
|
||||
selectedChapterId: chapterId
|
||||
})
|
||||
expect(ctx.outlineItem).toBe('主角觉醒')
|
||||
})
|
||||
})
|
||||
@@ -30,7 +30,7 @@ describe('migrate v2', () => {
|
||||
expect(tableExists(db, 'search_fts')).toBe(true)
|
||||
|
||||
const version = db.prepare('SELECT MAX(version) AS v FROM schema_version').get() as { v: number }
|
||||
expect(version.v).toBe(8)
|
||||
expect(version.v).toBe(9)
|
||||
})
|
||||
|
||||
it('migrates existing v1 database to v2', () => {
|
||||
@@ -47,7 +47,7 @@ describe('migrate v2', () => {
|
||||
|
||||
expect(tableExists(db, 'outline_items')).toBe(true)
|
||||
const version = db.prepare('SELECT MAX(version) AS v FROM schema_version').get() as { v: number }
|
||||
expect(version.v).toBe(8)
|
||||
expect(version.v).toBe(9)
|
||||
|
||||
const cols = db.prepare('PRAGMA table_info(chapters)').all() as { name: string }[]
|
||||
const colNames = cols.map((c) => c.name)
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('migrate v3', () => {
|
||||
expect(tableExists(db, 'ai_messages')).toBe(true)
|
||||
|
||||
const version = db.prepare('SELECT MAX(version) AS v FROM schema_version').get() as { v: number }
|
||||
expect(version.v).toBe(8)
|
||||
expect(version.v).toBe(9)
|
||||
})
|
||||
|
||||
it('migrates existing v2 database to v3', () => {
|
||||
@@ -49,6 +49,6 @@ describe('migrate v3', () => {
|
||||
expect(tables.map((t) => t.name)).toContain('ai_messages')
|
||||
|
||||
const version = db.prepare('SELECT MAX(version) AS v FROM schema_version').get() as { v: number }
|
||||
expect(version.v).toBe(8)
|
||||
expect(version.v).toBe(9)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('migrate v4', () => {
|
||||
migrate(db)
|
||||
|
||||
const version = db.prepare('SELECT MAX(version) AS v FROM schema_version').get() as { v: number }
|
||||
expect(version.v).toBe(8)
|
||||
expect(version.v).toBe(9)
|
||||
expect(tableExists(db, 'interactive_flows')).toBe(true)
|
||||
expect(tableExists(db, 'interactive_scenes')).toBe(true)
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('migrate v4', () => {
|
||||
migrate(db)
|
||||
|
||||
const version = db.prepare('SELECT MAX(version) AS v FROM schema_version').get() as { v: number }
|
||||
expect(version.v).toBe(8)
|
||||
expect(version.v).toBe(9)
|
||||
expect(tableExists(db, 'interactive_flows')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('migrate v5', () => {
|
||||
db = new DatabaseSync(':memory:')
|
||||
migrate(db)
|
||||
const version = db.prepare('SELECT MAX(version) AS v FROM schema_version').get() as { v: number }
|
||||
expect(version.v).toBe(8)
|
||||
expect(version.v).toBe(9)
|
||||
const cols = db.prepare('PRAGMA table_info(interactive_flows)').all() as { name: string }[]
|
||||
expect(cols.some((c) => c.name === 'flow_mode')).toBe(true)
|
||||
expect(cols.some((c) => c.name === 'mode_config_json')).toBe(true)
|
||||
@@ -39,6 +39,6 @@ describe('migrate v5', () => {
|
||||
db.prepare('INSERT INTO schema_version (version) VALUES (4)').run()
|
||||
migrate(db)
|
||||
const version = db.prepare('SELECT MAX(version) AS v FROM schema_version').get() as { v: number }
|
||||
expect(version.v).toBe(8)
|
||||
expect(version.v).toBe(9)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('migrate v6', () => {
|
||||
db = new DatabaseSync(':memory:')
|
||||
migrate(db)
|
||||
const version = db.prepare('SELECT MAX(version) AS v FROM schema_version').get() as { v: number }
|
||||
expect(version.v).toBe(8)
|
||||
expect(version.v).toBe(9)
|
||||
const tables = db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
.all() as { name: string }[]
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('migrate v7', () => {
|
||||
db = new DatabaseSync(':memory:')
|
||||
migrate(db)
|
||||
const version = db.prepare('SELECT MAX(version) AS v FROM schema_version').get() as { v: number }
|
||||
expect(version.v).toBe(8)
|
||||
expect(version.v).toBe(9)
|
||||
const tables = db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='chapter_writing_snapshots'")
|
||||
.get()
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('migrate v8', () => {
|
||||
db = new DatabaseSync(':memory:')
|
||||
migrate(db)
|
||||
const version = db.prepare('SELECT MAX(version) AS v FROM schema_version').get() as { v: number }
|
||||
expect(version.v).toBe(8)
|
||||
expect(version.v).toBe(9)
|
||||
const table = db
|
||||
.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='knowledge_injection_logs'"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest'
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import { migrate } from '../../src/main/db/migrate'
|
||||
import type { SqliteDb } from '../../src/main/db/types'
|
||||
|
||||
describe('migrate v9', () => {
|
||||
let db: SqliteDb
|
||||
afterEach(() => db?.close())
|
||||
|
||||
it('UT-MIG-09: fresh database migrates to v9 with chapter_tags', () => {
|
||||
db = new DatabaseSync(':memory:')
|
||||
migrate(db)
|
||||
const version = db.prepare('SELECT MAX(version) AS v FROM schema_version').get() as { v: number }
|
||||
expect(version.v).toBe(9)
|
||||
const table = db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='chapter_tags'")
|
||||
.get()
|
||||
expect(table).toBeTruthy()
|
||||
const chapterCols = db.prepare('PRAGMA table_info(chapters)').all() as Array<{ name: string }>
|
||||
expect(chapterCols.some((c) => c.name === 'published_at')).toBe(true)
|
||||
expect(chapterCols.some((c) => c.name === 'word_count_threshold')).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user