refactor(v032): recall IPC handle→on + fix sibling test mocks (#36)
- inbox:emitRecallShown / emitRecallSnoozed: ipcMain.handle → on (fire-and-forget honest pattern, return value 의존자 0) - preload: ipcRenderer.invoke → send (matching on the main side) - shared/types: Promise<void> → void on both recall emit methods - store.ts: drop await on emitRecallSnoozed (now void) - inboxApi-*.test.ts: add ipcMain.on to electron mock (broken by above) - tests/unit/recall-ipc.test.ts: new TDD test for handle→on migration Note: #20 CaptureService telemetry .catch debug log skipped — CaptureService has no logger field; adding one would require non-trivial constructor signature change. Reported as CONCERN below. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -280,7 +280,7 @@ export const useInbox = create<InboxState>((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.
|
||||
|
||||
@@ -152,8 +152,8 @@ export interface InboxApi {
|
||||
listRecallCandidate(): Promise<Note | null>;
|
||||
markRecallOpened(id: string): Promise<{ note: Note }>;
|
||||
dismissRecall(id: string): Promise<{ note: Note }>;
|
||||
emitRecallShown(id: string): Promise<void>;
|
||||
emitRecallSnoozed(id: string): Promise<void>;
|
||||
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 전환 요청 구독.
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
65
tests/unit/recall-ipc.test.ts
Normal file
65
tests/unit/recall-ipc.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user