Files
bilin/tests/main/writing-analytics.test.ts
T
bing 4adafa1936 feat: ship v1.0.0 with character graph and writing analytics
Add P7 Cytoscape relationship graph with setting CRUD, P9 cockpit analytics driven by writing_sessions, chapter POV selection, and full test coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 10:48:31 +08:00

110 lines
5.5 KiB
TypeScript

import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest'
import { mkdtempSync, rmSync } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'
import { BookRegistryService } from '../../src/main/services/book-registry'
import { GlobalSettingsService } from '../../src/main/services/global-settings'
import { WritingLogRepository } from '../../src/main/db/repositories/writing-log.repo'
import { WritingLogService } from '../../src/main/services/writing-log.service'
import { WritingSessionRepository } from '../../src/main/db/repositories/writing-session.repo'
import { WritingSessionService } from '../../src/main/services/writing-session.service'
import { PomodoroDailyRepository } from '../../src/main/db/repositories/pomodoro-daily.repo'
import { WritingAnalyticsService, POV_NONE } from '../../src/main/services/writing-analytics.service'
import { trackerFor } from '../../src/main/services/chapter-writing-tracker.service'
import { SettingRepository } from '../../src/main/db/repositories/setting.repo'
import { ChapterRepository } from '../../src/main/db/repositories/chapter.repo'
import { VolumeRepository } from '../../src/main/db/repositories/volume.repo'
import { closeAllBookDbs } from '../../src/main/db/connection'
describe('WritingAnalyticsService', () => {
let booksDir: string
let userDir: string
let settingsDir: string
let registry: BookRegistryService
let writingLogs: WritingLogService
let sessionRepo: WritingSessionRepository
let sessions: WritingSessionService
let pomodoroDaily: PomodoroDailyRepository
let analytics: WritingAnalyticsService
let bookId: string
let logRepo: WritingLogRepository
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-07-08T14:30:00.000Z'))
booksDir = mkdtempSync(join(tmpdir(), 'bilin-analytics-books-'))
userDir = mkdtempSync(join(tmpdir(), 'bilin-analytics-logs-'))
settingsDir = mkdtempSync(join(tmpdir(), 'bilin-analytics-settings-'))
registry = new BookRegistryService(booksDir)
const settings = new GlobalSettingsService(settingsDir)
settings.update({ ...settings.get(), dailyWordGoal: 500 })
logRepo = new WritingLogRepository(userDir)
writingLogs = new WritingLogService(logRepo, settings)
sessionRepo = new WritingSessionRepository(logRepo.getDb())
sessions = new WritingSessionService(sessionRepo)
pomodoroDaily = new PomodoroDailyRepository(logRepo.getDb())
analytics = new WritingAnalyticsService(writingLogs, sessionRepo, pomodoroDaily, registry)
bookId = registry.create({ name: '分析书', category: '测试', createSampleChapter: false }).id
})
afterEach(() => {
vi.useRealTimers()
logRepo?.close()
closeAllBookDbs()
rmSync(booksDir, { recursive: true, force: true })
rmSync(userDir, { recursive: true, force: true })
rmSync(settingsDir, { recursive: true, force: true })
})
it('UT-ANALYTICS-01: getSummary includes today speed from sessions', () => {
const db = registry.getDb(bookId)
const volId = new VolumeRepository(db).create('卷一', 0).id
const chapterRepo = new ChapterRepository(db)
let chapter = chapterRepo.create(volId, '第一章', 0, '<p>测试内容</p>')
const tracker = trackerFor(registry, bookId, writingLogs, sessions)
tracker.recordDelta(chapter.id, chapter.wordCount)
vi.advanceTimersByTime(2 * 60 * 1000)
chapter = chapterRepo.update(chapter.id, { content: '<p>测试内容扩展更多汉字</p>' })
tracker.recordDelta(chapter.id, chapter.wordCount)
const summary = analytics.getSummary(bookId)
expect(summary.todayWords).toBeGreaterThan(0)
expect(summary.todaySpeedWpm).not.toBeNull()
expect(summary.todaySpeedWpm!).toBeGreaterThan(0)
expect(summary.hourlyHeatmap.some((v) => v > 0)).toBe(true)
})
it('UT-ANALYTICS-02: povDistribution groups by povCharacterId', () => {
const db = registry.getDb(bookId)
const settingRepo = new SettingRepository(db)
const hero = settingRepo.create('character', '主角').id
const volId = new VolumeRepository(db).create('卷一', 0).id
const ch1 = new ChapterRepository(db).create(volId, 'A', 0, '<p>一二三四五</p>')
const ch2 = new ChapterRepository(db).create(volId, 'B', 1, '<p>六七八九十</p>')
new ChapterRepository(db).update(ch1.id, { povCharacterId: hero })
const summary = analytics.getSummary(bookId)
const heroRow = summary.povDistribution.find((p) => p.characterId === hero)
const noneRow = summary.povDistribution.find((p) => p.characterId === POV_NONE)
expect(heroRow!.words).toBeGreaterThan(0)
expect(noneRow!.words).toBeGreaterThan(0)
expect(ch2.wordCount).toBeGreaterThan(0)
})
it('IT-ANALYTICS-01: recordDelta feeds session then getSummary', () => {
const db = registry.getDb(bookId)
const volId = new VolumeRepository(db).create('卷一', 0).id
const chapterRepo = new ChapterRepository(db)
let chapter = chapterRepo.create(volId, '章', 0, '<p>一二三四五</p>')
const tracker = trackerFor(registry, bookId, writingLogs, sessions)
tracker.recordDelta(chapter.id, chapter.wordCount)
vi.advanceTimersByTime(2 * 60 * 1000)
chapter = chapterRepo.update(chapter.id, { content: '<p>一二三四五六七八九十</p>' })
tracker.recordDelta(chapter.id, chapter.wordCount)
const rows = sessionRepo.listForBookDate(bookId, '2026-07-08')
expect(rows).toHaveLength(1)
expect(rows[0].wordDelta).toBeGreaterThan(0)
const summary = analytics.getSummary(bookId)
expect(summary.todaySpeedWpm).not.toBeNull()
expect(summary.todaySpeedWpm!).toBeGreaterThan(0)
})
})