전수 audit 후 핵심 root fix 3 + edge cases 5: ROOT - inbox:set-status IPC 가 pushNoteUpdated emit (이전엔 stale → 호출처별 refreshMeta 필요) - upsertNote 가 current view status 인식 (이전엔 잘못된 status 노트 잔류) - store async 함수 try/catch (이전엔 IPC fail 시 무한 loading) EDGE - restoreNote 가 status='active' 도 갱신 - upsertNote trash 판정 deletedAt → status='trashed' - Modal Escape dismiss 통일 (5개 modal) - OnboardingWizard IPC fail fallback (try/catch + skip) - MoveStatusModal overlay 클릭 close Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
164 lines
5.5 KiB
TypeScript
164 lines
5.5 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();
|
|
});
|
|
|
|
it('emits note:updated to renderer after setStatus (v0.3.8 push-based)', async () => {
|
|
const send = vi.fn();
|
|
const win = { webContents: { send }, isDestroyed: () => false } as never;
|
|
const deps = makeDeps();
|
|
deps.getInboxWindow = () => win;
|
|
const updatedNote = { id: 'n1', status: 'completed' };
|
|
mockFindById.mockReturnValue(updatedNote);
|
|
registerInboxApi(deps);
|
|
const handler = handlers['inbox:set-status'];
|
|
if (handler === undefined) throw new Error('handler not registered');
|
|
await handler(null, 'n1', 'completed', null);
|
|
expect(send).toHaveBeenCalledWith('note:updated', updatedNote);
|
|
});
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|