feat: ship v0.8.0 with writing logs and AI knowledge extraction

Deliver P5.2 writing day logs, cockpit heatmap, streak tracking, and chapter knowledge auto-extraction with merge review.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 18:26:29 +08:00
parent f331ddae86
commit a39e06ff07
51 changed files with 1436 additions and 89 deletions
+115
View File
@@ -0,0 +1,115 @@
import { describe, it, expect, afterEach } from 'vitest'
import { DatabaseSync } from 'node:sqlite'
import { mkdtempSync, rmSync } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'
import { migrate } from '../../src/main/db/migrate'
import { ChapterRepository } from '../../src/main/db/repositories/chapter.repo'
import { VolumeRepository } from '../../src/main/db/repositories/volume.repo'
import { WritingLogRepository } from '../../src/main/db/repositories/writing-log.repo'
import { GlobalSettingsService } from '../../src/main/services/global-settings'
import { WritingLogService } from '../../src/main/services/writing-log.service'
import { ChapterWritingTracker } from '../../src/main/services/chapter-writing-tracker.service'
import { addDays, localDateString } from '../../src/main/services/writing-date.util'
import type { SqliteDb } from '../../src/main/db/types'
describe('WritingLogService', () => {
let userDir: string
let settingsDir: string
let settings: GlobalSettingsService
let repo: WritingLogRepository
let service: WritingLogService
afterEach(() => {
repo?.close()
if (userDir) rmSync(userDir, { recursive: true, force: true })
if (settingsDir) rmSync(settingsDir, { recursive: true, force: true })
})
function setup(goal = 1000): void {
userDir = mkdtempSync(join(tmpdir(), 'bilin-wlog-'))
settingsDir = mkdtempSync(join(tmpdir(), 'bilin-wlog-settings-'))
settings = new GlobalSettingsService(settingsDir)
settings.update({ ...settings.get(), dailyWordGoal: goal })
repo = new WritingLogRepository(userDir)
service = new WritingLogService(repo, settings)
}
it('UT-LOG-01: addToday accumulates +200', () => {
setup()
const bookId = 'book-1'
expect(service.addToday(bookId, 200)).toBe(200)
expect(service.addToday(bookId, 200)).toBe(400)
expect(service.getStats(bookId).todayWords).toBe(400)
})
it('UT-LOG-02: negative delta clamps at 0', () => {
setup()
const bookId = 'book-1'
service.addToday(bookId, 100)
expect(service.addToday(bookId, -200)).toBe(0)
})
it('UT-LOG-04: streak counts consecutive goal-met days', () => {
setup(100)
const bookId = 'book-1'
const today = localDateString()
repo.addToday(bookId, 150, addDays(today, -2))
repo.addToday(bookId, 120, addDays(today, -1))
repo.addToday(bookId, 80, today)
expect(service.getStats(bookId).streakDays).toBe(2)
})
})
describe('ChapterWritingTracker', () => {
let db: SqliteDb
let userDir: string
let settingsDir: string
let settings: GlobalSettingsService
let writingLogs: WritingLogService
let repo: WritingLogRepository
afterEach(() => {
db?.close()
repo?.close()
if (userDir) rmSync(userDir, { recursive: true, force: true })
if (settingsDir) rmSync(settingsDir, { recursive: true, force: true })
})
it('UT-LOG-03: supplementOnFinish fills gap', () => {
db = new DatabaseSync(':memory:')
migrate(db)
userDir = mkdtempSync(join(tmpdir(), 'bilin-tracker-'))
settingsDir = mkdtempSync(join(tmpdir(), 'bilin-tracker-settings-'))
settings = new GlobalSettingsService(settingsDir)
repo = new WritingLogRepository(userDir)
writingLogs = new WritingLogService(repo, settings)
const bookId = 'book-1'
const volId = new VolumeRepository(db).create('卷一', 0).id
const chapter = new ChapterRepository(db).create(volId, '第一章', 0, '<p>一二三四五六七八九十</p>')
const tracker = new ChapterWritingTracker(db, bookId, writingLogs)
tracker.recordDelta(chapter.id, 5)
tracker.supplementOnFinish(chapter.id, chapter.wordCount)
expect(writingLogs.getStats(bookId).todayWords).toBe(chapter.wordCount)
})
it('IT-LOG-01: recordDelta tracks chapter updates', () => {
db = new DatabaseSync(':memory:')
migrate(db)
userDir = mkdtempSync(join(tmpdir(), 'bilin-itlog-'))
settingsDir = mkdtempSync(join(tmpdir(), 'bilin-itlog-settings-'))
settings = new GlobalSettingsService(settingsDir)
repo = new WritingLogRepository(userDir)
writingLogs = new WritingLogService(repo, settings)
const bookId = 'book-1'
const volId = new VolumeRepository(db).create('卷一', 0).id
const chapterRepo = new ChapterRepository(db)
const ch = chapterRepo.create(volId, 'A', 0, '<p>ab</p>')
const tracker = new ChapterWritingTracker(db, bookId, writingLogs)
const updated = chapterRepo.update(ch.id, { content: '<p>abcd</p>' })
tracker.recordDelta(ch.id, updated.wordCount)
const updated2 = chapterRepo.update(ch.id, { content: '<p>abcdef</p>' })
tracker.recordDelta(ch.id, updated2.wordCount)
expect(writingLogs.getStats(bookId).todayWords).toBe(updated2.wordCount)
})
})