Files
inkling/tests/unit/inboxApi-setStatus.test.ts
altair823 4db7a0bce0 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>
2026-05-10 14:23:19 +09:00

150 lines
4.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
const { handlers, mockSetStatus, mockFindById, mockGenerateRaw } = vi.hoisted(() => ({
handlers: {} as Record<string, (...args: unknown[]) => unknown>,
mockSetStatus: vi.fn(),
mockFindById: vi.fn(),
mockGenerateRaw: vi.fn()
}));
vi.mock('electron', () => ({
default: {
ipcMain: {
handle: (ch: string, fn: (...args: unknown[]) => unknown) => {
handlers[ch] = fn;
},
on: (_ch: string, _fn: unknown) => {}
},
dialog: {},
shell: {}
}
}));
import { registerInboxApi } from '../../src/main/ipc/inboxApi';
function makeDeps(): Parameters<typeof registerInboxApi>[0] {
// Minimal stub — `inbox:set-status` 핸들러는 deps.repo.setStatus 만 참조.
// `ai:classify-status` 는 deps.repo.findById + deps.providerHolder.get() 사용.
const provider = {
name: 'mock',
generate: vi.fn(),
healthCheck: vi.fn(async () => ({ ok: true })),
generateRaw: mockGenerateRaw
};
return {
repo: {
setStatus: mockSetStatus,
findById: mockFindById,
list: vi.fn(),
listByStatus: vi.fn(),
countByStatus: vi.fn(() => 0)
} as never,
continuity: {} as never,
capture: {} as never,
health: {} as never,
intent: {} as never,
getInboxWindow: () => null,
settings: {} as never,
providerHolder: { get: () => provider } as never,
paths: { profileDir: '/profile' }
};
}
describe('inbox:set-status IPC', () => {
beforeEach(() => {
Object.keys(handlers).forEach((k) => delete handlers[k]);
mockSetStatus.mockReset();
});
it('forwards valid status + reason to repo.setStatus', async () => {
registerInboxApi(makeDeps());
const handler = handlers['inbox:set-status'];
if (handler === undefined) throw new Error('handler not registered');
const r = await handler(null, 'n1', 'completed', '결재 끝');
expect(r).toEqual({ ok: true });
expect(mockSetStatus).toHaveBeenCalledWith('n1', 'completed', '결재 끝');
});
it('forwards null reason as-is', async () => {
registerInboxApi(makeDeps());
const handler = handlers['inbox:set-status'];
if (handler === undefined) throw new Error('handler not registered');
const r = await handler(null, 'n1', 'archived', null);
expect(r).toEqual({ ok: true });
expect(mockSetStatus).toHaveBeenCalledWith('n1', 'archived', null);
});
it('rejects invalid status without calling repo', async () => {
registerInboxApi(makeDeps());
const handler = handlers['inbox:set-status'];
if (handler === undefined) throw new Error('handler not registered');
const r = (await handler(null, 'n1', 'invalid', null)) as { ok: boolean; reason?: string };
expect(r.ok).toBe(false);
expect(r.reason).toBe('invalid status');
expect(mockSetStatus).not.toHaveBeenCalled();
});
});
describe('ai:classify-status IPC', () => {
beforeEach(() => {
Object.keys(handlers).forEach((k) => delete handlers[k]);
mockFindById.mockReset();
mockGenerateRaw.mockReset();
});
it('uses classifyStatus with note rawText/summary', async () => {
mockFindById.mockReturnValue({
id: 'n1',
rawText: 'meeting notes',
aiSummary: 's'
});
mockGenerateRaw.mockResolvedValue(
'{"recommended":"completed","rationale":"끝남"}'
);
registerInboxApi(makeDeps());
const handler = handlers['ai:classify-status'];
if (handler === undefined) throw new Error('handler not registered');
const r = (await handler(null, 'n1', '결재')) as {
recommended: string;
rationale: string;
};
expect(r.recommended).toBe('completed');
expect(r.rationale).toBe('끝남');
// prompt 에 rawText / summary / reason 포함
const prompt = mockGenerateRaw.mock.calls[0]?.[0] as string;
expect(prompt).toContain('meeting notes');
expect(prompt).toContain('결재');
});
it('returns archived fallback when note not found', async () => {
mockFindById.mockReturnValue(null);
registerInboxApi(makeDeps());
const handler = handlers['ai:classify-status'];
if (handler === undefined) throw new Error('handler not registered');
const r = (await handler(null, 'missing', '결재')) as {
recommended: string;
rationale: string;
};
expect(r.recommended).toBe('archived');
expect(r.rationale.length).toBeGreaterThan(0);
expect(mockGenerateRaw).not.toHaveBeenCalled();
});
it('returns archived fallback when AI throws', async () => {
mockFindById.mockReturnValue({
id: 'n1',
rawText: 't',
aiSummary: null
});
mockGenerateRaw.mockRejectedValue(new Error('network'));
registerInboxApi(makeDeps());
const handler = handlers['ai:classify-status'];
if (handler === undefined) throw new Error('handler not registered');
const r = (await handler(null, 'n1', 'r')) as {
recommended: string;
rationale: string;
};
expect(r.recommended).toBe('archived');
});
});