feat(w5): ship v2.0.0 platform sync, undo persistence, and updates
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, existsSync, readFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
import { LogService } from '../../src/main/services/log.service'
|
||||
|
||||
describe('LogService', () => {
|
||||
let dir: string
|
||||
let logs: LogService
|
||||
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(join(tmpdir(), 'bilin-log-'))
|
||||
logs = new LogService(dir)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('UT-LOG-01: manual error writes to log file', () => {
|
||||
logs.logManualError('UT-LOG-01 test error')
|
||||
const logFile = join(dir, 'logs', 'bilin.log')
|
||||
expect(existsSync(logFile)).toBe(true)
|
||||
const content = readFileSync(logFile, 'utf-8')
|
||||
expect(content).toContain('UT-LOG-01 test error')
|
||||
})
|
||||
|
||||
it('detects crash flag from previous run', () => {
|
||||
logs.markUncleanExit()
|
||||
const next = new LogService(dir)
|
||||
expect(next.hadCrashOnLastRun()).toBe(true)
|
||||
next.clearCrashFlag()
|
||||
expect(next.hadCrashOnLastRun()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -11,7 +11,7 @@ describe('migrate v10', () => {
|
||||
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(10)
|
||||
expect(version.v).toBe(11)
|
||||
const table = db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='timeline_events'")
|
||||
.get()
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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 v11', () => {
|
||||
let db: SqliteDb
|
||||
afterEach(() => db?.close())
|
||||
|
||||
it('UT-MIG-11: fresh database migrates to v11 with undo_stack', () => {
|
||||
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(11)
|
||||
const table = db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='undo_stack'")
|
||||
.get()
|
||||
expect(table).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -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(10)
|
||||
expect(version.v).toBe(11)
|
||||
})
|
||||
|
||||
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(10)
|
||||
expect(version.v).toBe(11)
|
||||
|
||||
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(10)
|
||||
expect(version.v).toBe(11)
|
||||
})
|
||||
|
||||
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(10)
|
||||
expect(version.v).toBe(11)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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(10)
|
||||
expect(version.v).toBe(11)
|
||||
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(10)
|
||||
expect(version.v).toBe(11)
|
||||
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(10)
|
||||
expect(version.v).toBe(11)
|
||||
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(10)
|
||||
expect(version.v).toBe(11)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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(10)
|
||||
expect(version.v).toBe(11)
|
||||
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(10)
|
||||
expect(version.v).toBe(11)
|
||||
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(10)
|
||||
expect(version.v).toBe(11)
|
||||
const table = db
|
||||
.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='knowledge_injection_logs'"
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('migrate v9', () => {
|
||||
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(10)
|
||||
expect(version.v).toBe(11)
|
||||
const table = db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='chapter_tags'")
|
||||
.get()
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { mkdtempSync, rmSync, existsSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
import { SyncService } from '../../src/main/services/sync.service'
|
||||
import { GlobalSettingsService } from '../../src/main/services/global-settings'
|
||||
import { BookRegistryService } from '../../src/main/services/book-registry'
|
||||
import { SYNC_BUNDLE_NAME } from '../../src/main/services/sync-crypto'
|
||||
import { closeAllBookDbs } from '../../src/main/db/connection'
|
||||
|
||||
describe('SyncService', () => {
|
||||
let userDir: string
|
||||
let sync: SyncService
|
||||
|
||||
beforeEach(() => {
|
||||
userDir = mkdtempSync(join(tmpdir(), 'bilin-sync-svc-'))
|
||||
const settings = new GlobalSettingsService(userDir)
|
||||
const registry = new BookRegistryService(userDir)
|
||||
registry.create({ name: '同步测试书', category: '玄幻', createSampleChapter: false })
|
||||
sync = new SyncService(registry, settings, userDir)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
closeAllBookDbs()
|
||||
try {
|
||||
rmSync(userDir, { recursive: true, force: true })
|
||||
} catch {
|
||||
/* windows file lock */
|
||||
}
|
||||
})
|
||||
|
||||
it('IT-SYNC-01: local-folder sync writes encrypted bundle', async () => {
|
||||
const syncDir = mkdtempSync(join(tmpdir(), 'bilin-sync-target-'))
|
||||
try {
|
||||
sync.configure({
|
||||
config: {
|
||||
mode: 'local-folder',
|
||||
localFolderPath: syncDir,
|
||||
schedule: 'manual'
|
||||
},
|
||||
password: 'test-password-1'
|
||||
})
|
||||
const result = await sync.run('test-password-1')
|
||||
expect(result.status).toBe('ok')
|
||||
expect(existsSync(join(syncDir, SYNC_BUNDLE_NAME))).toBe(true)
|
||||
} finally {
|
||||
rmSync(syncDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import { migrate } from '../../src/main/db/migrate'
|
||||
import { UndoStackService } from '../../src/main/services/undo-stack.service'
|
||||
import { VolumeRepository } from '../../src/main/db/repositories/volume.repo'
|
||||
import { ChapterRepository } from '../../src/main/db/repositories/chapter.repo'
|
||||
import type { SqliteDb } from '../../src/main/db/types'
|
||||
|
||||
describe('UndoStackService', () => {
|
||||
let db: SqliteDb
|
||||
let svc: UndoStackService
|
||||
let chapterId: string
|
||||
|
||||
beforeEach(() => {
|
||||
db = new DatabaseSync(':memory:')
|
||||
migrate(db)
|
||||
svc = new UndoStackService(db)
|
||||
const vol = new VolumeRepository(db).create('卷一', 0)
|
||||
chapterId = new ChapterRepository(db).create(vol.id, '第一章', 0).id
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
db.close()
|
||||
})
|
||||
|
||||
it('UT-UNDO-01: push and list undo entries', () => {
|
||||
svc.push(chapterId, { type: 'doc', content: '<p>旧内容</p>' })
|
||||
svc.push(chapterId, { type: 'doc', content: '<p>新内容</p>' })
|
||||
const list = svc.list(chapterId)
|
||||
expect(list).toHaveLength(2)
|
||||
expect(list[0]?.operation.content).toContain('新内容')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { UpdateService } from '../../src/main/services/update.service'
|
||||
|
||||
describe('UpdateService', () => {
|
||||
const prevMock = process.env.BILIN_MOCK_UPDATE
|
||||
const prevVersion = process.env.BILIN_MOCK_UPDATE_VERSION
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.BILIN_MOCK_UPDATE = '1'
|
||||
process.env.BILIN_MOCK_UPDATE_VERSION = '9.9.9'
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (prevMock === undefined) delete process.env.BILIN_MOCK_UPDATE
|
||||
else process.env.BILIN_MOCK_UPDATE = prevMock
|
||||
if (prevVersion === undefined) delete process.env.BILIN_MOCK_UPDATE_VERSION
|
||||
else process.env.BILIN_MOCK_UPDATE_VERSION = prevVersion
|
||||
})
|
||||
|
||||
it('IT-UPDATE-01: mock check returns available version', async () => {
|
||||
const svc = new UpdateService()
|
||||
const info = await svc.check()
|
||||
expect(info?.version).toBe('9.9.9')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user