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, '
测试内容
') const tracker = trackerFor(registry, bookId, writingLogs, sessions) tracker.recordDelta(chapter.id, chapter.wordCount) vi.advanceTimersByTime(2 * 60 * 1000) chapter = chapterRepo.update(chapter.id, { content: '测试内容扩展更多汉字
' }) 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, '一二三四五
') const ch2 = new ChapterRepository(db).create(volId, 'B', 1, '六七八九十
') 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, '一二三四五
') const tracker = trackerFor(registry, bookId, writingLogs, sessions) tracker.recordDelta(chapter.id, chapter.wordCount) vi.advanceTimersByTime(2 * 60 * 1000) chapter = chapterRepo.update(chapter.id, { content: '一二三四五六七八九十
' }) 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) }) })