diff --git a/src/main/ipc/inboxApi.ts b/src/main/ipc/inboxApi.ts index 6f33776..cbc74cf 100644 --- a/src/main/ipc/inboxApi.ts +++ b/src/main/ipc/inboxApi.ts @@ -269,6 +269,29 @@ export function registerInboxApi(deps: InboxIpcDeps): void { await deps.health.runOnce(); return { ok: true }; }); + + // v0.2.10 Cut C — raw_text 가변 + revision 보존. + // updateRawText: 빈 문자열 reject (trim 후 length===0). 그 외엔 그대로 (newline/space 보존). + // listRevisions: 그대로 반환 (camelCase 이미 hydrate 됨). + // restoreRevision: repo throw → { ok: false } (UI 가 에러 표시). + ipcMain.handle('inbox:update-raw-text', async (_e, id: string, newText: string) => { + if (typeof newText !== 'string' || newText.trim().length === 0) { + return { ok: false as const, reason: 'empty' as const }; + } + deps.repo.updateRawText(id, newText); + return { ok: true as const }; + }); + + ipcMain.handle('inbox:list-revisions', (_e, id: string) => deps.repo.listRevisions(id)); + + ipcMain.handle('inbox:restore-revision', async (_e, id: string, revId: number) => { + try { + deps.repo.restoreRevision(id, revId); + return { ok: true as const }; + } catch (e) { + return { ok: false as const, reason: (e as Error).message }; + } + }); } export function pushNoteUpdated(getWin: () => BrowserWindow | null, note: Note): void { diff --git a/tests/unit/inboxApi-revisions.test.ts b/tests/unit/inboxApi-revisions.test.ts new file mode 100644 index 0000000..9675dfb --- /dev/null +++ b/tests/unit/inboxApi-revisions.test.ts @@ -0,0 +1,99 @@ +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 }).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 { + const repo = { + updateRawText: vi.fn(), + listRevisions: vi.fn(() => []), + restoreRevision: vi.fn(), + findById: vi.fn(), + 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) + } 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 revisions IPC', () => { + beforeEach(() => { + (electron.ipcMain as unknown as { handle: ReturnType }).handle.mockClear(); + }); + + it('inbox:update-raw-text — repo.updateRawText 호출 + ok:true', async () => { + const deps = makeDeps(); + registerInboxApi(deps); + const h = getHandler('inbox:update-raw-text'); + const r = await h({}, 'note-1', 'new text'); + expect(deps.repo.updateRawText).toHaveBeenCalledWith('note-1', 'new text'); + expect(r).toEqual({ ok: true }); + }); + + it('inbox:update-raw-text — 빈 문자열 reject', async () => { + const deps = makeDeps(); + registerInboxApi(deps); + const h = getHandler('inbox:update-raw-text'); + const r = await h({}, 'note-1', ' '); + expect(deps.repo.updateRawText).not.toHaveBeenCalled(); + expect(r).toEqual({ ok: false, reason: 'empty' }); + }); + + it('inbox:list-revisions — repo.listRevisions 결과 반환', async () => { + const deps = makeDeps(); + (deps.repo.listRevisions as ReturnType).mockReturnValue([ + { revId: 1, noteId: 'a', rawText: 'v1', editedAt: 't1', editedBy: 'capture' } + ]); + registerInboxApi(deps); + const h = getHandler('inbox:list-revisions'); + const r = await h({}, 'a'); + expect(r).toEqual([ + { revId: 1, noteId: 'a', rawText: 'v1', editedAt: 't1', editedBy: 'capture' } + ]); + }); + + it('inbox:restore-revision — repo throw 시 ok:false', async () => { + const deps = makeDeps(); + (deps.repo.restoreRevision as ReturnType).mockImplementation(() => { + throw new Error('revision 99 not found for note a'); + }); + registerInboxApi(deps); + const h = getHandler('inbox:restore-revision'); + const r = await h({}, 'a', 99); + expect(r).toEqual({ ok: false, reason: 'revision 99 not found for note a' }); + }); +});