import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { Note } from '@shared/types'; const mockApi = { listNotes: vi.fn(async () => [] as Note[]), listTrash: vi.fn(async () => [] as Note[]), 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 () => {}) }; vi.mock('../../src/renderer/inbox/api.js', () => ({ inboxApi: mockApi })); const noteStub = (id: string, deletedAt: string | null = null): Note => ({ id, rawText: 'x', aiTitle: null, aiSummary: null, aiStatus: 'done', aiError: null, aiProvider: null, aiGeneratedAt: null, titleEditedByUser: false, summaryEditedByUser: false, userIntent: null, intentPromptedAt: null, dueDate: null, dueDateEditedByUser: false, deletedAt, lastRecalledAt: null, recallDismissedAt: null, createdAt: '2026-05-01T00:00:00Z', updatedAt: '2026-05-01T00:00:00Z', tags: [], media: [] }); describe('useInbox — trash state (v0.2.3 #4)', () => { 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, ollamaStatus: { ok: true }, continuity: { weekStart: '', weekCount: 0, weekTarget: 7, consecutiveCompleteWeeks: 0, showRecoveryToast: false, lastNoteAt: null } }); Object.values(mockApi).forEach((fn) => 'mockClear' in fn && (fn as any).mockClear()); }); it('toggleShowTrash flips state and triggers loadTrash on enter', async () => { mockApi.listTrash.mockResolvedValueOnce([noteStub('t1', '2026-05-01T00:00:00Z')]); const { useInbox } = await import('../../src/renderer/inbox/store.js'); await useInbox.getState().toggleShowTrash(); expect(useInbox.getState().showTrash).toBe(true); expect(useInbox.getState().trashNotes).toHaveLength(1); expect(mockApi.listTrash).toHaveBeenCalled(); await useInbox.getState().toggleShowTrash(); expect(useInbox.getState().showTrash).toBe(false); }); it('upsertNote routes to trashNotes when deletedAt IS NOT NULL', async () => { const { useInbox } = await import('../../src/renderer/inbox/store.js'); useInbox.getState().upsertNote(noteStub('a', '2026-05-01T00:00:00Z')); expect(useInbox.getState().notes).toHaveLength(0); expect(useInbox.getState().trashNotes).toHaveLength(1); }); it('upsertNote moves note from notes to trashNotes when trashed', async () => { const { useInbox } = await import('../../src/renderer/inbox/store.js'); useInbox.getState().upsertNote(noteStub('a')); expect(useInbox.getState().notes).toHaveLength(1); useInbox.getState().upsertNote(noteStub('a', '2026-05-01T00:00:00Z')); expect(useInbox.getState().notes).toHaveLength(0); expect(useInbox.getState().trashNotes).toHaveLength(1); }); it('restoreNote calls api + moves note from trashNotes to notes (낙관적 갱신)', async () => { const { useInbox } = await import('../../src/renderer/inbox/store.js'); useInbox.getState().upsertNote(noteStub('a', '2026-05-01T00:00:00Z')); expect(useInbox.getState().trashNotes).toHaveLength(1); await useInbox.getState().restoreNote('a'); expect(mockApi.restoreNote).toHaveBeenCalledWith('a'); // main 은 restore 시 pushNoteUpdated 안 보냄 — store 자가 갱신 검증 expect(useInbox.getState().trashNotes).toHaveLength(0); expect(useInbox.getState().notes).toHaveLength(1); expect(useInbox.getState().notes[0]!.deletedAt).toBeNull(); }); it('upsertNote with showTrash=false preserves server trashCount (regression I1)', async () => { const { useInbox } = await import('../../src/renderer/inbox/store.js'); // server 가 trashCount=5 알려줬는데 trashNotes 는 미로드 (showTrash=false 기본) useInbox.setState({ trashCount: 5, trashNotes: [] }); useInbox.getState().upsertNote(noteStub('active-1')); expect(useInbox.getState().trashCount).toBe(5); // server 값 보존 expect(useInbox.getState().notes).toHaveLength(1); }); it('emptyTrash with cancelled confirm leaves trashNotes intact', async () => { mockApi.emptyTrash.mockResolvedValueOnce({ confirmed: false, count: 0 }); const { useInbox } = await import('../../src/renderer/inbox/store.js'); useInbox.getState().upsertNote(noteStub('a', '2026-05-01T00:00:00Z')); await useInbox.getState().emptyTrash(); expect(useInbox.getState().trashNotes).toHaveLength(1); }); });