From 3ebd3bc9a564076d702497ca15933c4bb30b24ee Mon Sep 17 00:00:00 2001 From: altair823 Date: Sat, 2 May 2026 03:32:01 +0900 Subject: [PATCH] feat(retry): store retryAllFailed action + failedCount (#2 v0.2.3) --- src/renderer/inbox/store.ts | 23 +++++++++++---- tests/unit/store.aiRetry.test.ts | 50 ++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 tests/unit/store.aiRetry.test.ts diff --git a/src/renderer/inbox/store.ts b/src/renderer/inbox/store.ts index 45d9db3..33f37a7 100644 --- a/src/renderer/inbox/store.ts +++ b/src/renderer/inbox/store.ts @@ -17,6 +17,7 @@ interface InboxState { tagFilter: string | null; expiredCandidates: Note[]; expiredSnoozeUntilMs: number | null; + failedCount: number; loadInitial: () => Promise; refreshMeta: () => Promise; upsertNote: (note: Note) => void; @@ -31,6 +32,7 @@ interface InboxState { trashExpiredBatch: (ids: string[]) => Promise; snoozeExpired: () => void; recheckOllama: () => Promise; + retryAllFailed: () => Promise; } const emptyContinuity: WeeklyContinuity = { @@ -51,29 +53,32 @@ export const useInbox = create((set, get) => ({ tagFilter: null, expiredCandidates: [], expiredSnoozeUntilMs: null, + failedCount: 0, async loadInitial() { set({ loading: true }); - const [notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates] = await Promise.all([ + const [notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount] = await Promise.all([ inboxApi.listNotes({ limit: 50 }), inboxApi.getContinuity(), inboxApi.getPendingCount(), inboxApi.getOllamaStatus(), inboxApi.getTodayCount(), inboxApi.getTrashCount(), - inboxApi.listExpired() + inboxApi.listExpired(), + inboxApi.getFailedCount() ]); - set({ notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, loading: false }); + set({ notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, loading: false }); }, async refreshMeta() { - const [continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates] = await Promise.all([ + const [continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount] = await Promise.all([ inboxApi.getContinuity(), inboxApi.getPendingCount(), inboxApi.getOllamaStatus(), inboxApi.getTodayCount(), inboxApi.getTrashCount(), - inboxApi.listExpired() + inboxApi.listExpired(), + inboxApi.getFailedCount() ]); - set({ continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates }); + set({ continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount }); }, upsertNote(note) { // trashCount 는 server-authoritative. trashNotes 가 cache-loaded (showTrash=true) 일 @@ -172,5 +177,11 @@ export const useInbox = create((set, get) => ({ async recheckOllama() { const status = await inboxApi.ollamaRecheck(); set({ ollamaStatus: status }); + }, + async retryAllFailed() { + await inboxApi.retryAllFailed(); + // 낙관적 갱신: failedCount = 0. AiWorker 처리 진행 중에 PendingBanner 가 N건 노출. + // refreshMeta 가 트리거되면 자연 동기 (worker.onUpdate → main → renderer). + set({ failedCount: 0 }); } })); diff --git a/tests/unit/store.aiRetry.test.ts b/tests/unit/store.aiRetry.test.ts new file mode 100644 index 0000000..dbad513 --- /dev/null +++ b/tests/unit/store.aiRetry.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const mockApi = { + listNotes: vi.fn(async () => []), + listTrash: vi.fn(async () => []), + getTrashCount: vi.fn(async () => 0), + getContinuity: vi.fn(async () => ({ weekStart: '', weekCount: 0, weekTarget: 7, consecutiveCompleteWeeks: 0, showRecoveryToast: false, lastNoteAt: null })), + getPendingCount: vi.fn(async () => 0), + getOllamaStatus: vi.fn(async () => ({ ok: true })), + getTodayCount: vi.fn(async () => 0), + restoreNote: vi.fn(async () => {}), + permanentDeleteNote: vi.fn(async () => ({ confirmed: true })), + emptyTrash: vi.fn(async () => ({ confirmed: true, count: 0 })), + deleteNote: vi.fn(async () => {}), + onNoteUpdated: vi.fn(() => () => {}), + updateAiFields: vi.fn(async () => {}), + setDueDate: vi.fn(async () => {}), + setIntent: vi.fn(async () => {}), + dismissIntent: vi.fn(async () => {}), + listExpired: vi.fn(async () => []), + trashExpiredBatch: vi.fn(async () => ({ trashedCount: 0, confirmed: false })), + ollamaRecheck: vi.fn(async (): Promise<{ ok: boolean; reason?: string }> => ({ ok: true })), + onOllamaStatus: vi.fn(() => () => {}), + retryAllFailed: vi.fn(async () => ({ count: 0 })), + getFailedCount: vi.fn(async () => 0) +}; + +vi.mock('../../src/renderer/inbox/api.js', () => ({ inboxApi: mockApi })); + +describe('useInbox — AI retry (v0.2.3 #2)', () => { + beforeEach(async () => { + const { useInbox } = await import('../../src/renderer/inbox/store.js'); + useInbox.setState({ + notes: [], trashNotes: [], trashCount: 0, showTrash: false, + loading: false, tagFilter: null, pendingCount: 0, todayCount: 0, failedCount: 5, + ollamaStatus: { ok: true }, + continuity: { weekStart: '', weekCount: 0, weekTarget: 7, consecutiveCompleteWeeks: 0, showRecoveryToast: false, lastNoteAt: null }, + expiredCandidates: [], expiredSnoozeUntilMs: null + }); + Object.values(mockApi).forEach((fn) => 'mockClear' in fn && (fn as any).mockClear()); + }); + + it('retryAllFailed action — failedCount=0 reset 후 IPC 호출', async () => { + mockApi.retryAllFailed.mockResolvedValueOnce({ count: 5 }); + const { useInbox } = await import('../../src/renderer/inbox/store.js'); + await useInbox.getState().retryAllFailed(); + expect(mockApi.retryAllFailed).toHaveBeenCalledTimes(1); + expect(useInbox.getState().failedCount).toBe(0); + }); +});