feat(v0211): InboxApi.search + reviewAggregate (types + IPC + preload)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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<NoteRevision[]>;
|
||||
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<Note[]>;
|
||||
reviewAggregate(period: ReviewPeriod): Promise<ReviewAggregate>;
|
||||
}
|
||||
|
||||
export interface InklingApi {
|
||||
|
||||
84
tests/unit/inboxApi-search-review.test.ts
Normal file
84
tests/unit/inboxApi-search-review.test.ts
Normal file
@@ -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<typeof vi.fn> }).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> = {}): 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<typeof vi.fn> }).handle.mockClear();
|
||||
});
|
||||
|
||||
it('inbox:search — repo.search 호출 결과 반환', async () => {
|
||||
const deps = makeDeps();
|
||||
(deps.repo.search as ReturnType<typeof vi.fn>).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<typeof vi.fn>).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 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user