From 143684ce8ab5a832c5c2c6ae45ae4b0128b2462b Mon Sep 17 00:00:00 2001 From: altair823 Date: Sun, 10 May 2026 00:27:43 +0900 Subject: [PATCH] feat(v0211): InboxApi.search + reviewAggregate (types + IPC + preload) --- src/main/ipc/inboxApi.ts | 20 ++++++ src/preload/index.ts | 3 + src/shared/types.ts | 12 ++++ tests/unit/inboxApi-search-review.test.ts | 84 +++++++++++++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 tests/unit/inboxApi-search-review.test.ts diff --git a/src/main/ipc/inboxApi.ts b/src/main/ipc/inboxApi.ts index cbc74cf..7f617c7 100644 --- a/src/main/ipc/inboxApi.ts +++ b/src/main/ipc/inboxApi.ts @@ -292,6 +292,26 @@ export function registerInboxApi(deps: InboxIpcDeps): void { return { ok: false as const, reason: (e as Error).message }; } }); + + // v0.2.11 Cut D — FTS5 검색 + 회고 aggregate. + ipcMain.handle( + 'inbox:search', + (_e, query: string, opts: { limit?: number; status?: NoteStatus } = {}) => + deps.repo.search(query, opts) + ); + + ipcMain.handle('inbox:review-aggregate', (_e, period: 'daily' | 'weekly' | 'monthly') => { + const VALID = ['daily', 'weekly', 'monthly'] as const; + if (!(VALID as readonly string[]).includes(period)) { + return { + totalCount: 0, + recentNotes: [], + tagCounts: [], + dueProgress: { total: 0, passed: 0, pending: 0 } + }; + } + return deps.repo.reviewAggregate(period); + }); } export function pushNoteUpdated(getWin: () => BrowserWindow | null, note: Note): void { diff --git a/src/preload/index.ts b/src/preload/index.ts index 0606fbe..f59ffe2 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -85,6 +85,9 @@ const api: InklingApi = { updateRawText: (noteId: string, newText: string) => ipcRenderer.invoke('inbox:update-raw-text', noteId, newText), listRevisions: (noteId: string) => ipcRenderer.invoke('inbox:list-revisions', noteId), restoreRevision: (noteId: string, revId: number) => ipcRenderer.invoke('inbox:restore-revision', noteId, revId), + // v0.2.11 Cut D — search + 회고 aggregate. + search: (query, opts) => ipcRenderer.invoke('inbox:search', query, opts ?? {}), + reviewAggregate: (period) => ipcRenderer.invoke('inbox:review-aggregate', period), } }; diff --git a/src/shared/types.ts b/src/shared/types.ts index 82784b9..cc625bf 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -31,6 +31,15 @@ export interface NoteRevision { editedBy: 'user' | 'capture'; } +// v0.2.11 Cut D — 회고 view aggregate. +export type ReviewPeriod = 'daily' | 'weekly' | 'monthly'; +export interface ReviewAggregate { + totalCount: number; + recentNotes: Note[]; + tagCounts: Array<{ tag: string; count: number }>; + dueProgress: { total: number; passed: number; pending: number }; +} + export interface Note { id: string; rawText: string; @@ -170,6 +179,9 @@ export interface InboxApi { updateRawText(noteId: string, newText: string): Promise<{ ok: true } | { ok: false; reason: string }>; listRevisions(noteId: string): Promise; restoreRevision(noteId: string, revId: number): Promise<{ ok: true } | { ok: false; reason: string }>; + // v0.2.11 Cut D — FTS5 search + 회고 aggregate. + search(query: string, opts?: { limit?: number; status?: NoteStatus }): Promise; + reviewAggregate(period: ReviewPeriod): Promise; } export interface InklingApi { diff --git a/tests/unit/inboxApi-search-review.test.ts b/tests/unit/inboxApi-search-review.test.ts new file mode 100644 index 0000000..b318c32 --- /dev/null +++ b/tests/unit/inboxApi-search-review.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +vi.mock('electron', () => ({ default: { ipcMain: { handle: vi.fn() } } })); +import electron from 'electron'; +import { registerInboxApi } from '../../src/main/ipc/inboxApi.js'; +import type { InboxIpcDeps } from '../../src/main/ipc/inboxApi.js'; + +function getHandler(channel: string): (...args: unknown[]) => unknown { + const handle = (electron.ipcMain as unknown as { handle: ReturnType }).handle; + const call = handle.mock.calls.find((c) => c[0] === channel); + if (!call) throw new Error(`channel ${channel} not registered`); + return call[1] as (...args: unknown[]) => unknown; +} + +function makeDeps(overrides: Partial = {}): InboxIpcDeps { + const repo = { + search: vi.fn(() => []), + reviewAggregate: vi.fn(() => ({ totalCount: 0, recentNotes: [], tagCounts: [], dueProgress: { total: 0, passed: 0, pending: 0 } })), + list: vi.fn(), + listByStatus: vi.fn(), + countByStatus: vi.fn(() => 0), + countByAiStatus: vi.fn(() => 0), + countTrashed: vi.fn(() => 0), + countFailed: vi.fn(() => 0), + listTrashed: vi.fn(() => []), + setStatus: vi.fn(), + requeueDisabled: vi.fn(() => 0), + getAllPendingJobs: vi.fn(() => []), + getPendingCount: vi.fn(() => 0), + countToday: vi.fn(() => 0), + findById: vi.fn(), + listRevisions: vi.fn(() => []), + restoreRevision: vi.fn(), + updateRawText: vi.fn() + } as unknown as InboxIpcDeps['repo']; + return { + repo, + continuity: { get: vi.fn() } as unknown as InboxIpcDeps['continuity'], + capture: {} as InboxIpcDeps['capture'], + health: {} as InboxIpcDeps['health'], + intent: {} as InboxIpcDeps['intent'], + getInboxWindow: () => null, + settings: {} as InboxIpcDeps['settings'], + providerHolder: {} as InboxIpcDeps['providerHolder'], + paths: { profileDir: '/tmp' }, + ...overrides + }; +} + +describe('inboxApi search/review IPC', () => { + beforeEach(() => { + (electron.ipcMain as unknown as { handle: ReturnType }).handle.mockClear(); + }); + + it('inbox:search — repo.search 호출 결과 반환', async () => { + const deps = makeDeps(); + (deps.repo.search as ReturnType).mockReturnValue([{ id: 'a' }]); + registerInboxApi(deps); + const h = getHandler('inbox:search'); + const r = await h({}, '회의', { status: 'active', limit: 10 }); + expect(deps.repo.search).toHaveBeenCalledWith('회의', { status: 'active', limit: 10 }); + expect(r).toEqual([{ id: 'a' }]); + }); + + it('inbox:review-aggregate — repo.reviewAggregate 호출 결과 반환', async () => { + const deps = makeDeps(); + const fake = { totalCount: 5, recentNotes: [], tagCounts: [{ tag: 'x', count: 2 }], dueProgress: { total: 1, passed: 1, pending: 0 } }; + (deps.repo.reviewAggregate as ReturnType).mockReturnValue(fake); + registerInboxApi(deps); + const h = getHandler('inbox:review-aggregate'); + const r = await h({}, 'weekly'); + expect(deps.repo.reviewAggregate).toHaveBeenCalledWith('weekly'); + expect(r).toEqual(fake); + }); + + it('inbox:review-aggregate — 잘못된 period reject', async () => { + const deps = makeDeps(); + registerInboxApi(deps); + const h = getHandler('inbox:review-aggregate'); + const r = await h({}, 'yearly'); + expect(deps.repo.reviewAggregate).not.toHaveBeenCalled(); + expect(r).toMatchObject({ totalCount: 0 }); + }); +});