import { describe, it, expect, afterEach, vi } from 'vitest' vi.mock('electron', () => ({ BrowserWindow: { getAllWindows: () => [] }, Notification: { isSupported: () => false } })) import { mkdtempSync, rmSync } from 'fs' import { join } from 'path' import { tmpdir } from 'os' import { WritingLogRepository } from '../../src/main/db/repositories/writing-log.repo' import { WritingMilestoneRepository } from '../../src/main/db/repositories/writing-milestone.repo' import { GlobalSettingsService } from '../../src/main/services/global-settings' import { WritingLogService } from '../../src/main/services/writing-log.service' import { GoalNotificationService } from '../../src/main/services/goal-notification.service' import { AchievementService } from '../../src/main/services/achievement.service' import { addDays, localDateString } from '../../src/main/services/writing-date.util' describe('WritingMilestoneRepository', () => { let userDir: string let repo: WritingLogRepository let milestoneRepo: WritingMilestoneRepository afterEach(() => { repo?.close() if (userDir) rmSync(userDir, { recursive: true, force: true }) }) it('UT-ACH-01: unlock streak_7 once', () => { userDir = mkdtempSync(join(tmpdir(), 'bilin-ach-')) repo = new WritingLogRepository(userDir) milestoneRepo = new WritingMilestoneRepository(repo.getDb()) milestoneRepo.unlock('b1', 'streak_7') expect(milestoneRepo.has('b1', 'streak_7')).toBe(true) expect(milestoneRepo.unlock('b1', 'streak_7')).toBeNull() }) }) describe('AchievementService', () => { let userDir: string let settingsDir: string let settings: GlobalSettingsService let writingRepo: WritingLogRepository let writingLogs: WritingLogService let milestoneRepo: WritingMilestoneRepository let notify: GoalNotificationService let achievement: AchievementService afterEach(() => { writingRepo?.close() if (userDir) rmSync(userDir, { recursive: true, force: true }) if (settingsDir) rmSync(settingsDir, { recursive: true, force: true }) }) function setup(goal = 100): void { userDir = mkdtempSync(join(tmpdir(), 'bilin-ach-svc-')) settingsDir = mkdtempSync(join(tmpdir(), 'bilin-ach-settings-')) settings = new GlobalSettingsService(settingsDir) settings.update({ ...settings.get(), dailyWordGoal: goal, enableGoalNotifications: false }) writingRepo = new WritingLogRepository(userDir) writingLogs = new WritingLogService(writingRepo, settings) milestoneRepo = new WritingMilestoneRepository(writingRepo.getDb()) notify = new GoalNotificationService(settings) achievement = new AchievementService(milestoneRepo, writingLogs, settings, notify) writingLogs.setGoalHooks(achievement, notify) } it('UT-ACH-02: streakDays=7 unlocks milestone and notifies once', () => { setup(100) const bookId = 'book-1' const today = localDateString() for (let i = 1; i <= 6; i++) { writingRepo.addToday(bookId, 150, addDays(today, -i)) } writingRepo.addToday(bookId, 120, today) vi.spyOn(notify, 'notifyMilestone') writingLogs.addToday(bookId, 1) expect(milestoneRepo.has(bookId, 'streak_7')).toBe(true) expect(notify.notifyMilestone).toHaveBeenCalledTimes(1) writingLogs.addToday(bookId, 1) expect(notify.notifyMilestone).toHaveBeenCalledTimes(1) }) })