diff --git a/src/main/services/NotificationService.ts b/src/main/services/NotificationService.ts new file mode 100644 index 0000000..0e6435b --- /dev/null +++ b/src/main/services/NotificationService.ts @@ -0,0 +1,33 @@ +import { createHash } from 'node:crypto'; + +export const REWARD_COPIES = [ + '이 생각은 이제 Inkling이 들고 있습니다.', + '나중에 찾을 수 있게 보관했습니다.', + '방금 하나의 업무 기억을 구출했습니다.', + '기록 완료. 이제 잊어도 됩니다.' +] as const; + +export interface NotificationDeps { + isSupported: () => boolean; + send: (body: string) => void; +} + +export class NotificationService { + constructor(private deps: NotificationDeps) {} + + celebrate(noteId: string): void { + if (!this.deps.isSupported()) return; + const idx = this.pick(noteId); + const body = REWARD_COPIES[idx]!; + try { + this.deps.send(body); + } catch { + // Swallow notification errors — capture must not fail because of toast. + } + } + + private pick(noteId: string): number { + const hash = createHash('sha256').update(noteId).digest(); + return hash[0]! % REWARD_COPIES.length; + } +} diff --git a/tests/unit/NotificationService.test.ts b/tests/unit/NotificationService.test.ts new file mode 100644 index 0000000..d639e8d --- /dev/null +++ b/tests/unit/NotificationService.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest'; +import { NotificationService, REWARD_COPIES } from '@main/services/NotificationService.js'; + +describe('NotificationService', () => { + it('rotates copy deterministically by noteId hash', () => { + const fired: string[] = []; + const svc = new NotificationService({ + isSupported: () => true, + send: (body) => { fired.push(body); } + }); + const id1 = '00000000-0000-7000-8000-000000000001'; + svc.celebrate(id1); + svc.celebrate(id1); + expect(fired).toHaveLength(2); + expect(fired[0]).toBe(fired[1]); + expect(REWARD_COPIES).toContain(fired[0]); + }); + + it('different ids select different (eventually all 4)', () => { + const fired: string[] = []; + const svc = new NotificationService({ + isSupported: () => true, + send: (body) => { fired.push(body); } + }); + for (let i = 0; i < 32; i++) { + svc.celebrate(`id-${i}`); + } + const distinct = new Set(fired); + expect(distinct.size).toBe(REWARD_COPIES.length); + }); + + it('skips silently when notifications unsupported', () => { + const fired: string[] = []; + const svc = new NotificationService({ + isSupported: () => false, + send: (body) => { fired.push(body); } + }); + svc.celebrate('id-1'); + expect(fired).toHaveLength(0); + }); +});