import { describe, it, expect } from 'vitest'; import Database from 'better-sqlite3'; import { runMigrations } from '@main/db/migrations/index.js'; import { ContinuityService } from '@main/services/ContinuityService.js'; function dbWithDates(isoDates: string[]): Database.Database { const db = new Database(':memory:'); runMigrations(db); const insert = db.prepare( `INSERT INTO notes (id, raw_text, ai_status, created_at, updated_at) VALUES (?, ?, 'pending', ?, ?)` ); for (const [i, d] of isoDates.entries()) insert.run(`n${i}`, 'x', d, d); return db; } describe('ContinuityService', () => { it('empty db returns zero counts and no recovery toast', () => { const db = new Database(':memory:'); runMigrations(db); const svc = new ContinuityService(db, () => new Date('2026-04-25T10:00:00+09:00')); const r = svc.get(); expect(r.weekCount).toBe(0); expect(r.weekTarget).toBe(7); expect(r.consecutiveCompleteWeeks).toBe(0); expect(r.showRecoveryToast).toBe(false); expect(r.lastNoteAt).toBeNull(); }); it('counts notes in current KST week (월~일)', () => { const db = dbWithDates([ '2026-04-20T01:00:00+09:00', '2026-04-22T03:00:00+09:00', '2026-04-25T22:00:00+09:00' ]); const svc = new ContinuityService(db, () => new Date('2026-04-25T23:00:00+09:00')); const r = svc.get(); expect(r.weekStart).toBe('2026-04-20'); expect(r.weekCount).toBe(3); }); function isoKst(year: number, month: number, day: number, hour = 10): string { const mm = String(month).padStart(2, '0'); const dd = String(day).padStart(2, '0'); const hh = String(hour).padStart(2, '0'); return `${year}-${mm}-${dd}T${hh}:00:00+09:00`; } it('consecutiveCompleteWeeks counts weeks with >=7 notes ending immediately before current week', () => { const dates: string[] = []; for (let d = 6; d <= 12; d++) dates.push(isoKst(2026, 4, d)); for (let d = 13; d <= 19; d++) dates.push(isoKst(2026, 4, d)); dates.push(isoKst(2026, 4, 20)); dates.push(isoKst(2026, 4, 21)); const db = dbWithDates(dates); const svc = new ContinuityService(db, () => new Date('2026-04-25T12:00:00+09:00')); const r = svc.get(); expect(r.consecutiveCompleteWeeks).toBe(2); expect(r.weekCount).toBe(2); }); it('consecutiveCompleteWeeks includes current week if it already hit 7', () => { const dates: string[] = []; for (let d = 13; d <= 19; d++) dates.push(isoKst(2026, 4, d)); for (let d = 20; d <= 26; d++) dates.push(isoKst(2026, 4, d)); const db = dbWithDates(dates); const svc = new ContinuityService(db, () => new Date('2026-04-26T23:00:00+09:00')); const r = svc.get(); expect(r.weekCount).toBe(7); expect(r.consecutiveCompleteWeeks).toBe(2); }); it('showRecoveryToast=true when last note is >=7 days ago AND a fresh note exists today', () => { const dates = [ '2026-04-15T10:00:00+09:00', '2026-04-25T11:00:00+09:00' ]; const db = dbWithDates(dates); const svc = new ContinuityService(db, () => new Date('2026-04-25T12:00:00+09:00')); expect(svc.get().showRecoveryToast).toBe(true); }); it('showRecoveryToast=false when there are notes within last 7 days', () => { const db = dbWithDates([ '2026-04-22T10:00:00+09:00', '2026-04-25T11:00:00+09:00' ]); const svc = new ContinuityService(db, () => new Date('2026-04-25T12:00:00+09:00')); expect(svc.get().showRecoveryToast).toBe(false); }); it('excludes trashed notes from streak/recovery math (v0.2.3 #4)', () => { const db = dbWithDates([ '2026-04-22T10:00:00+09:00', '2026-04-25T11:00:00+09:00' ]); // 22일 노트를 trash → 25일이 마지막. 22일 미만이라 weekCount 1 이지만 lastNoteAt // 은 25일 (마지막 active) 이어야 함. trashed 노트가 무시되어야 함. db.prepare(`UPDATE notes SET deleted_at='2026-04-26T00:00:00Z' WHERE id='n0'`).run(); const svc = new ContinuityService(db, () => new Date('2026-04-25T12:00:00+09:00')); const r = svc.get(); expect(r.weekCount).toBe(1); expect(r.lastNoteAt).toBe('2026-04-25T02:00:00.000Z'); // KST 11:00 = UTC 02:00 }); });