diff --git a/src/main/ipc/inboxApi.ts b/src/main/ipc/inboxApi.ts index 7f617c7..79f3cdb 100644 --- a/src/main/ipc/inboxApi.ts +++ b/src/main/ipc/inboxApi.ts @@ -153,8 +153,8 @@ export function registerInboxApi(deps: InboxIpcDeps): void { ipcMain.handle('inbox:listRecallCandidate', () => deps.capture.listRecallCandidate()); ipcMain.handle('inbox:markRecallOpened', (_e, id: string) => deps.capture.markRecallOpened(id)); ipcMain.handle('inbox:dismissRecall', (_e, id: string) => deps.capture.dismissRecall(id)); - ipcMain.handle('inbox:emitRecallShown', (_e, id: string) => deps.capture.emitRecallShown(id)); - ipcMain.handle('inbox:emitRecallSnoozed', (_e, id: string) => deps.capture.emitRecallSnoozed(id)); + ipcMain.on('inbox:emitRecallShown', (_e, id: string) => { void deps.capture.emitRecallShown(id); }); + ipcMain.on('inbox:emitRecallSnoozed', (_e, id: string) => { void deps.capture.emitRecallSnoozed(id); }); ipcMain.handle('inbox:loadOllamaSettings', async () => { const s = await deps.settings.load(); diff --git a/src/preload/index.ts b/src/preload/index.ts index 0d64c2a..ce7be91 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -43,8 +43,8 @@ const api: InklingApi = { listRecallCandidate: () => ipcRenderer.invoke('inbox:listRecallCandidate'), markRecallOpened: (id: string) => ipcRenderer.invoke('inbox:markRecallOpened', id), dismissRecall: (id: string) => ipcRenderer.invoke('inbox:dismissRecall', id), - emitRecallShown: (id: string) => ipcRenderer.invoke('inbox:emitRecallShown', id), - emitRecallSnoozed: (id: string) => ipcRenderer.invoke('inbox:emitRecallSnoozed', id), + emitRecallShown: (id: string) => { ipcRenderer.send('inbox:emitRecallShown', id); }, + emitRecallSnoozed: (id: string) => { ipcRenderer.send('inbox:emitRecallSnoozed', id); }, loadOllamaSettings: () => ipcRenderer.invoke('inbox:loadOllamaSettings'), saveOllamaSettings: (v: { endpoint: string; model: string }) => ipcRenderer.invoke('inbox:saveOllamaSettings', v), // v0.2.7 Task 13 — 외부 (트레이) 에서 view 전환 요청 listener. diff --git a/src/renderer/inbox/store.ts b/src/renderer/inbox/store.ts index 6a9979a..10b1ff3 100644 --- a/src/renderer/inbox/store.ts +++ b/src/renderer/inbox/store.ts @@ -280,7 +280,7 @@ export const useInbox = create((set, get) => ({ // snooze 는 적용하되 emit 만 skip. telemetry 누락 받아들임 (의도적). const candidate = get().recallCandidate; if (candidate) { - await inboxApi.emitRecallSnoozed(candidate.id); + inboxApi.emitRecallSnoozed(candidate.id); } }, // v0.2.11 Cut D — FTS5 search + review aggregate actions. diff --git a/src/shared/types.ts b/src/shared/types.ts index 3c6270a..848b14a 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -152,8 +152,8 @@ export interface InboxApi { listRecallCandidate(): Promise; markRecallOpened(id: string): Promise<{ note: Note }>; dismissRecall(id: string): Promise<{ note: Note }>; - emitRecallShown(id: string): Promise; - emitRecallSnoozed(id: string): Promise; + emitRecallShown(id: string): void; + emitRecallSnoozed(id: string): void; loadOllamaSettings(): Promise<{ endpoint: string; model: string } | null>; saveOllamaSettings(v: { endpoint: string; model: string }): Promise<{ ok: true } | { ok: false; reason: string }>; // v0.2.7 Task 13 — 외부 (트레이 등) 에서 view 전환 요청 구독. diff --git a/tests/unit/inboxApi-openMedia.test.ts b/tests/unit/inboxApi-openMedia.test.ts index 0625653..9e4d00f 100644 --- a/tests/unit/inboxApi-openMedia.test.ts +++ b/tests/unit/inboxApi-openMedia.test.ts @@ -11,7 +11,8 @@ vi.mock('electron', () => ({ ipcMain: { handle: (ch: string, fn: (...args: unknown[]) => unknown) => { handlers[ch] = fn; - } + }, + on: (_ch: string, _fn: unknown) => {} }, dialog: {}, shell: { openPath: mockOpenPath } diff --git a/tests/unit/inboxApi-revisions.test.ts b/tests/unit/inboxApi-revisions.test.ts index 9675dfb..5e7fd3f 100644 --- a/tests/unit/inboxApi-revisions.test.ts +++ b/tests/unit/inboxApi-revisions.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; vi.mock('electron', () => ({ default: { - ipcMain: { handle: vi.fn() } + ipcMain: { handle: vi.fn(), on: vi.fn() } } })); diff --git a/tests/unit/inboxApi-search-review.test.ts b/tests/unit/inboxApi-search-review.test.ts index b318c32..138f0cb 100644 --- a/tests/unit/inboxApi-search-review.test.ts +++ b/tests/unit/inboxApi-search-review.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; -vi.mock('electron', () => ({ default: { ipcMain: { handle: vi.fn() } } })); +vi.mock('electron', () => ({ default: { ipcMain: { handle: vi.fn(), on: vi.fn() } } })); import electron from 'electron'; import { registerInboxApi } from '../../src/main/ipc/inboxApi.js'; import type { InboxIpcDeps } from '../../src/main/ipc/inboxApi.js'; diff --git a/tests/unit/inboxApi-setStatus.test.ts b/tests/unit/inboxApi-setStatus.test.ts index 8d69755..bba5969 100644 --- a/tests/unit/inboxApi-setStatus.test.ts +++ b/tests/unit/inboxApi-setStatus.test.ts @@ -12,7 +12,8 @@ vi.mock('electron', () => ({ ipcMain: { handle: (ch: string, fn: (...args: unknown[]) => unknown) => { handlers[ch] = fn; - } + }, + on: (_ch: string, _fn: unknown) => {} }, dialog: {}, shell: {} diff --git a/tests/unit/recall-ipc.test.ts b/tests/unit/recall-ipc.test.ts new file mode 100644 index 0000000..163d0ed --- /dev/null +++ b/tests/unit/recall-ipc.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Track ipcMain.handle and ipcMain.on calls +const { handleCalls, onCalls } = vi.hoisted(() => ({ + handleCalls: [] as string[], + onCalls: [] as string[] +})); + +vi.mock('electron', () => ({ + default: { + ipcMain: { + handle: (ch: string, _fn: unknown) => { handleCalls.push(ch); }, + on: (ch: string, _fn: unknown) => { onCalls.push(ch); } + }, + dialog: {}, + shell: {} + } +})); + +import { registerInboxApi } from '../../src/main/ipc/inboxApi.js'; +import type { InboxIpcDeps } from '../../src/main/ipc/inboxApi.js'; + +function makeDeps(): InboxIpcDeps { + return { + repo: {} as never, + continuity: {} as never, + capture: { + emitRecallShown: vi.fn(async () => {}), + emitRecallSnoozed: vi.fn(async () => {}) + } as never, + health: {} as never, + intent: {} as never, + getInboxWindow: () => null, + settings: {} as never, + providerHolder: {} as never, + paths: { profileDir: '/profile' } + }; +} + +describe('recall IPC channels (fire-and-forget)', () => { + beforeEach(() => { + handleCalls.length = 0; + onCalls.length = 0; + }); + + it('inbox:emitRecallShown is registered with ipcMain.on (not handle)', () => { + registerInboxApi(makeDeps()); + expect(onCalls).toContain('inbox:emitRecallShown'); + expect(handleCalls).not.toContain('inbox:emitRecallShown'); + }); + + it('inbox:emitRecallSnoozed is registered with ipcMain.on (not handle)', () => { + registerInboxApi(makeDeps()); + expect(onCalls).toContain('inbox:emitRecallSnoozed'); + expect(handleCalls).not.toContain('inbox:emitRecallSnoozed'); + }); + + it('each recall channel is registered exactly once with ipcMain.on', () => { + registerInboxApi(makeDeps()); + const shownCount = onCalls.filter((ch) => ch === 'inbox:emitRecallShown').length; + const snoozedCount = onCalls.filter((ch) => ch === 'inbox:emitRecallSnoozed').length; + expect(shownCount).toBe(1); + expect(snoozedCount).toBe(1); + }); +});